diff --git a/components/addbotbtn.html b/components/addbotbtn.html
index a817654..a98e456 100644
--- a/components/addbotbtn.html
+++ b/components/addbotbtn.html
@@ -1,18 +1,31 @@
{{ define "addbot" }}
+{{$botName := ""}}
+ {{$botName = .Room.FindBotByTeamRole "blue" "mime"}}
{{ if eq .Room.BlueTeam.Mime "" }}
Add Bot Mime
+ {{ else if ne $botName "" }}
+ Remove {{$botName}}
{{ end }}
+ {{$botName = .Room.FindBotByTeamRole "red" "mime"}}
{{ if eq .Room.RedTeam.Mime "" }}
Add Bot Mime
+ {{ else if ne $botName "" }}
+ Remove {{$botName}}
{{ end }}
+ {{$botName = .Room.FindBotByTeamRole "blue" "guesser"}}
{{ if not .Room.BlueTeam.Guessers }}
Add Bot Guesser
+ {{ else if ne $botName "" }}
+ Remove {{$botName}}
{{ end }}
+ {{$botName = .Room.FindBotByTeamRole "red" "guesser"}}
{{ if not .Room.RedTeam.Guessers }}
Add Bot Guesser
+ {{ else if ne $botName "" }}
+ Remove {{$botName}}
{{ end }}
{{end}}
diff --git a/components/cardword.html b/components/cardword.html
index 8011ed1..c94637f 100644
--- a/components/cardword.html
+++ b/components/cardword.html
@@ -1,16 +1,16 @@
{{define "cardword"}}
{{if .Revealed}}
{{if eq .Color "amber"}}
- {{.Word}}
{{else}}
- {{.Word}}
{{end}}
{{else}}
-
{{.Word}}
diff --git a/handlers/elements.go b/handlers/elements.go
index 9d9a3e9..656451f 100644
--- a/handlers/elements.go
+++ b/handlers/elements.go
@@ -194,3 +194,18 @@ func HandleAddBot(w http.ResponseWriter, r *http.Request) {
// go bot.StartBot()
notify(models.NotifyRoomUpdatePrefix+fi.Room.ID, "")
}
+
+func HandleRemoveBot(w http.ResponseWriter, r *http.Request) {
+ botName := r.URL.Query().Get("bot")
+ log.Debug("got remove-bot request", "bot_name", botName)
+ fi, err := getFullInfoByCtx(r.Context())
+ if err != nil {
+ abortWithError(w, err.Error())
+ return
+ }
+ if err := llmapi.RemoveBot(botName, fi.Room); err != nil {
+ abortWithError(w, err.Error())
+ return
+ }
+ notify(models.NotifyRoomUpdatePrefix+fi.Room.ID, "")
+}
diff --git a/handlers/game.go b/handlers/game.go
index 7002638..cb5fc92 100644
--- a/handlers/game.go
+++ b/handlers/game.go
@@ -8,6 +8,7 @@ import (
"html/template"
"net/http"
"strconv"
+ "strings"
)
func HandleCreateRoom(w http.ResponseWriter, r *http.Request) {
@@ -248,12 +249,20 @@ func HandleGiveClue(w http.ResponseWriter, r *http.Request) {
abortWithError(w, "your team already has a clue")
return
}
+ // check if the clue is the same as one of the existing words
+ for _, card := range fi.Room.Cards {
+ if strings.EqualFold(card.Word, clue) {
+ msg := fmt.Sprintf("cannot use existing word (%s) as a clue", clue)
+ abortWithError(w, msg)
+ return
+ }
+ }
// ===
action := models.Action{
Actor: fi.State.Username,
ActorColor: string(fi.State.Team),
WordColor: string(fi.State.Team),
- Action: "gave clue",
+ Action: models.ActionTypeClue,
Word: clue,
Number: num,
}
diff --git a/llmapi/main.go b/llmapi/main.go
index 480f349..082c96b 100644
--- a/llmapi/main.go
+++ b/llmapi/main.go
@@ -77,7 +77,7 @@ func (b *Bot) checkGuess(word string, room *models.Room) error {
Actor: b.BotName,
ActorColor: b.Team,
WordColor: string(color),
- Action: "guessed",
+ Action: models.ActionTypeGuess,
Word: word,
}
room.ActionHistory = append(room.ActionHistory, action)
@@ -115,105 +115,107 @@ func (b *Bot) checkGuess(word string, room *models.Room) error {
return nil
}
+func (b *Bot) BotMove() {
+ // botJournalName := models.NotifyJournalPrefix + b.RoomID
+ b.log.Debug("got signal", "bot-team", b.Team, "bot-role", b.Role)
+ // get room cards and actions
+ room, err := getRoomByID(b.RoomID)
+ if err != nil {
+ b.log.Error("bot loop", "error", err)
+ return
+ }
+ eventName := models.NotifyBacklogPrefix + room.ID
+ eventPayload := ""
+ defer func() { // save room
+ if err := saveRoom(room); err != nil {
+ b.log.Error("failed to save room", "error", err)
+ return
+ }
+ broker.Notifier.Notifier <- broker.NotificationEvent{
+ EventName: eventName,
+ Payload: eventPayload,
+ }
+ }()
+ // form prompt
+ prompt := b.BuildPrompt(room)
+ b.log.Debug("got prompt", "prompt", prompt)
+ room.LogJournal = append(room.LogJournal, b.BotName+" got prompt: "+prompt)
+ // call llm
+ llmResp, err := b.CallLLM(prompt)
+ if err != nil {
+ room.LogJournal = append(room.LogJournal, b.BotName+" send call got error: "+err.Error())
+ b.log.Error("bot loop", "error", err)
+ return
+ }
+ tempMap, err := b.LLMParser.ParseBytes(llmResp)
+ if err != nil {
+ room.LogJournal = append(room.LogJournal, b.BotName+" parse resp got error: "+err.Error())
+ b.log.Error("bot loop", "error", err, "resp", string(llmResp))
+ return
+ }
+ switch b.Role {
+ case models.UserRoleMime:
+ mimeResp := MimeResp{}
+ b.log.Info("mime resp log", "mimeResp", tempMap)
+ mimeResp.Clue = tempMap["clue"].(string)
+ mimeResp.Number = tempMap["number"].(string)
+ action := models.Action{
+ Actor: b.BotName,
+ ActorColor: b.Team,
+ WordColor: b.Team,
+ Action: models.ActionTypeClue,
+ Word: mimeResp.Clue,
+ Number: mimeResp.Number,
+ }
+ room.ActionHistory = append(room.ActionHistory, action)
+ room.MimeDone = true
+ eventPayload = mimeResp.Clue + mimeResp.Number
+ guessLimitU64, err := strconv.ParseUint(mimeResp.Number, 10, 8)
+ if err != nil {
+ b.log.Warn("failed to parse bot given limit", "mimeResp", mimeResp, "bot_name", b.BotName)
+ }
+ room.ThisTurnLimit = uint8(guessLimitU64)
+ case models.UserRoleGuesser:
+ // // deprecated
+ // if err := b.checkGuesses(tempMap, room); err != nil {
+ // b.log.Warn("failed to check guess", "mimeResp", tempMap, "bot_name", b.BotName)
+ // continue
+ // }
+ guess, ok := tempMap["guess"].(string)
+ if !ok || guess == "" {
+ b.log.Warn("failed to parse guess", "mimeResp", tempMap, "bot_name", b.BotName)
+ }
+ if err := b.checkGuess(guess, room); err != nil {
+ b.log.Warn("failed to check guess", "mimeResp", tempMap, "bot_name", b.BotName, "guess", guess, "error", err)
+ msg := fmt.Sprintf("failed to check guess", "mimeResp", tempMap, "bot_name", b.BotName, "guess", guess, "error", err)
+ room.LogJournal = append(room.LogJournal, msg)
+ }
+ b.log.Info("mime resp log", "guesserResp", tempMap)
+ couldBe, err := convertToSliceOfStrings(tempMap["could_be"])
+ if err != nil {
+ b.log.Warn("failed to parse could_be", "bot_resp", tempMap, "bot_name", b.BotName)
+ }
+ room.LogJournal = append(room.LogJournal, fmt.Sprintf("%s also considered this: %v", b.BotName, couldBe))
+ eventName = models.NotifyRoomUpdatePrefix + room.ID
+ eventPayload = ""
+ // TODO: needs to decide if it wants to open the next cardword or end turn
+ // or end turn on limit
+ default:
+ b.log.Error("unexpected role", "role", b.Role, "resp-map", tempMap)
+ return
+ }
+ if botName := room.WhichBotToMove(); botName != "" {
+ b.log.Debug("notifying bot", "name", botName)
+ SignalChanMap[botName] <- true
+ }
+}
+
// StartBot
func (b *Bot) StartBot() {
for {
select {
case <-SignalChanMap[b.BotName]:
- func() {
- // botJournalName := models.NotifyJournalPrefix + b.RoomID
- b.log.Debug("got signal", "bot-team", b.Team, "bot-role", b.Role)
- // get room cards and actions
- room, err := getRoomByID(b.RoomID)
- if err != nil {
- b.log.Error("bot loop", "error", err)
- return
- }
- eventName := models.NotifyBacklogPrefix + room.ID
- eventPayload := ""
- defer func() { // save room
- if err := saveRoom(room); err != nil {
- b.log.Error("failed to save room", "error", err)
- return
- }
- broker.Notifier.Notifier <- broker.NotificationEvent{
- EventName: eventName,
- Payload: eventPayload,
- }
- }()
- // form prompt
- prompt := b.BuildPrompt(room)
- b.log.Debug("got prompt", "prompt", prompt)
- room.LogJournal = append(room.LogJournal, b.BotName+" got prompt: "+prompt)
- // call llm
- llmResp, err := b.CallLLM(prompt)
- if err != nil {
- room.LogJournal = append(room.LogJournal, b.BotName+" send call got error: "+err.Error())
- b.log.Error("bot loop", "error", err)
- return
- }
- tempMap, err := b.LLMParser.ParseBytes(llmResp)
- if err != nil {
- room.LogJournal = append(room.LogJournal, b.BotName+" parse resp got error: "+err.Error())
- b.log.Error("bot loop", "error", err, "resp", string(llmResp))
- return
- }
- switch b.Role {
- case models.UserRoleMime:
- mimeResp := MimeResp{}
- b.log.Info("mime resp log", "mimeResp", tempMap)
- mimeResp.Clue = tempMap["clue"].(string)
- mimeResp.Number = tempMap["number"].(string)
- action := models.Action{
- Actor: b.BotName,
- ActorColor: b.Team,
- WordColor: b.Team,
- Action: "gave clue",
- Word: mimeResp.Clue,
- Number: mimeResp.Number,
- }
- room.ActionHistory = append(room.ActionHistory, action)
- room.MimeDone = true
- eventPayload = mimeResp.Clue + mimeResp.Number
- guessLimitU64, err := strconv.ParseUint(mimeResp.Number, 10, 8)
- if err != nil {
- b.log.Warn("failed to parse bot given limit", "mimeResp", mimeResp, "bot_name", b.BotName)
- }
- room.ThisTurnLimit = uint8(guessLimitU64)
- case models.UserRoleGuesser:
- // // deprecated
- // if err := b.checkGuesses(tempMap, room); err != nil {
- // b.log.Warn("failed to check guess", "mimeResp", tempMap, "bot_name", b.BotName)
- // continue
- // }
- guess, ok := tempMap["guess"].(string)
- if !ok || guess == "" {
- b.log.Warn("failed to parse guess", "mimeResp", tempMap, "bot_name", b.BotName)
- }
- if err := b.checkGuess(guess, room); err != nil {
- b.log.Warn("failed to check guess", "mimeResp", tempMap, "bot_name", b.BotName, "guess", guess, "error", err)
- msg := fmt.Sprintf("failed to check guess", "mimeResp", tempMap, "bot_name", b.BotName, "guess", guess, "error", err)
- room.LogJournal = append(room.LogJournal, msg)
- }
- b.log.Info("mime resp log", "guesserResp", tempMap)
- couldBe, err := convertToSliceOfStrings(tempMap["could_be"])
- if err != nil {
- b.log.Warn("failed to parse could_be", "bot_resp", tempMap, "bot_name", b.BotName)
- }
- room.LogJournal = append(room.LogJournal, fmt.Sprintf("%s also considered this: %v", b.BotName, couldBe))
- eventName = models.NotifyRoomUpdatePrefix + room.ID
- eventPayload = ""
- // TODO: needs to decide if it wants to open the next cardword or end turn
- // or end turn on limit
- default:
- b.log.Error("unexpected role", "role", b.Role, "resp-map", tempMap)
- return
- }
- if botName := room.WhichBotToMove(); botName != "" {
- b.log.Debug("notifying bot", "name", botName)
- SignalChanMap[botName] <- true
- }
- }()
+ b.BotMove()
case <-DoneChanMap[b.BotName]:
b.log.Debug("got done signal", "bot-name", b.BotName)
return
@@ -221,6 +223,18 @@ func (b *Bot) StartBot() {
}
}
+func RemoveBot(botName string, room *models.Room) error {
+ // channels
+ DoneChanMap[botName] <- true
+ close(DoneChanMap[botName])
+ close(SignalChanMap[botName])
+ // maps
+ delete(room.BotMap, botName)
+ delete(DoneChanMap, botName)
+ delete(SignalChanMap, botName)
+ return saveRoom(room)
+}
+
// EndBot
func NewBot(role, team, name, roomID string, cfg *config.Config, recovery bool) (*Bot, error) {
diff --git a/llmapi/parser.go b/llmapi/parser.go
index 8fc897b..55f3457 100644
--- a/llmapi/parser.go
+++ b/llmapi/parser.go
@@ -160,8 +160,10 @@ func (p *openRouterParser) ParseBytes(body []byte) (map[string]any, error) {
}
func (p *openRouterParser) MakePayload(prompt string) io.Reader {
+ // "model": "deepseek/deepseek-chat-v3-0324:free",
+ // TODO: set list of models an option to pick on the frontend
strPayload := fmt.Sprintf(`{
- "model": "deepseek/deepseek-chat-v3-0324:free",
+ "model": "google/gemini-2.0-flash-exp:free",
"messages": [
{
"role": "user",
diff --git a/main.go b/main.go
index 97be504..69154f7 100644
--- a/main.go
+++ b/main.go
@@ -47,6 +47,7 @@ func ListenToRequests(port string) *http.Server {
mux.HandleFunc("GET /word/show-color", handlers.HandleShowColor)
mux.HandleFunc("POST /check/name", handlers.HandleNameCheck)
mux.HandleFunc("GET /add-bot", handlers.HandleAddBot)
+ mux.HandleFunc("GET /remove-bot", handlers.HandleRemoveBot)
// special
mux.HandleFunc("GET /renotify-bot", handlers.HandleRenotifyBot)
// sse
diff --git a/models/main.go b/models/main.go
index 88c4b03..ed2a552 100644
--- a/models/main.go
+++ b/models/main.go
@@ -97,6 +97,16 @@ type Room struct {
LogJournal []string
}
+// FindBotByTeamRole returns bot name if found; otherwise empty string
+func (r *Room) FindBotByTeamRole(team, role string) string {
+ for bn, b := range r.BotMap {
+ if b.Role == StrToUserRole(role) && b.Team == StrToUserTeam(team) {
+ return bn
+ }
+ }
+ return ""
+}
+
func (r *Room) FetchLastClue() (*Action, error) {
for i := len(r.ActionHistory) - 1; i >= 0; i-- {
if r.ActionHistory[i].Action == string(ActionTypeClue) {
diff --git a/todos.md b/todos.md
index afa3e46..6d0132c 100644
--- a/todos.md
+++ b/todos.md
@@ -18,6 +18,7 @@
- clear indication that model (llm) is thinking / answered;
- instead of guessing all words at ones, ask only for 1 word to be open.
- ways to remove bots from teams;
+- check if clue word is the same as one of the cards and return err if it is; +
#### sse points
- clue sse update;