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 "" }} + {{ else if ne $botName "" }} + {{ end }} + {{$botName = .Room.FindBotByTeamRole "red" "mime"}} {{ if eq .Room.RedTeam.Mime "" }} + {{ else if ne $botName "" }} + {{ end }}
+ {{$botName = .Room.FindBotByTeamRole "blue" "guesser"}}
{{ if not .Room.BlueTeam.Guessers }} + {{ else if ne $botName "" }} + {{ end }} + {{$botName = .Room.FindBotByTeamRole "red" "guesser"}} {{ if not .Room.RedTeam.Guessers }} + {{ else if ne $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;