diff --git a/assets/style.css b/assets/style.css index 613e333..db799d7 100644 --- a/assets/style.css +++ b/assets/style.css @@ -1,7 +1,6 @@ body{ background-color: #0C1616FF; color: #8896b2; - max-width: 1000px; min-width: 0px; margin: 2em auto !important; margin-left: auto; diff --git a/assets/style.css.gz b/assets/style.css.gz index e4b3e9f..5f04e9a 100644 Binary files a/assets/style.css.gz and b/assets/style.css.gz differ diff --git a/broker/sse.go b/broker/sse.go index 7822442..0679f83 100644 --- a/broker/sse.go +++ b/broker/sse.go @@ -10,7 +10,7 @@ import ( // the amount of time to wait when pushing a message to // a slow client or a client that closed after `range clients` started. -const patience time.Duration = time.Second * 1 +// const patience time.Duration = time.Second * 1 type ( NotificationEvent struct { diff --git a/components/actionhistory.html b/components/actionhistory.html index c914b59..8a3d6f9 100644 --- a/components/actionhistory.html +++ b/components/actionhistory.html @@ -18,7 +18,7 @@ if (!window.actionHistoryScrollSet) { htmx.onLoad(function(target) { if (target.id === 'actionHistoryContainer') { - target.scrollTop = target.scrollHeight; + target.scrollToBottom(); } }); window.actionHistoryScrollSet = true; diff --git a/components/room.html b/components/room.html index 297cf07..c459bd1 100644 --- a/components/room.html +++ b/components/room.html @@ -1,6 +1,7 @@ {{define "room"}}
-
+
+

Hello {{.State.Username}};

Room created by {{.Room.CreatorName}};

Room link:

@@ -16,13 +17,13 @@ {{end}}

{{if eq .State.Team ""}} - join the team! + you don't have a role! join the team -> {{else}} you're on the team {{.State.Team}}! {{end}}

-
+
{{if .Room.IsRunning}}

Turn of the {{.Room.TeamTurn}} team

{{template "turntimer" .Room}} @@ -48,7 +49,18 @@ {{template "teamlist" .Room.RedTeam}}
+
+

+
+
+ {{template "actionhistory" .Room.ActionHistory}} +
+
+ {{template "cardtable" .Room}} +
+ +
bot thought:
-
- {{template "actionhistory" .Room.ActionHistory}} -
-
-
- {{template "cardtable" .Room}} -
{{if .Room.IsRunning}} {{if and (eq .State.Role "guesser") (eq .State.Team .Room.TeamTurn)}} @@ -74,7 +79,7 @@ {{end}}
- {{if and (eq .State.Username .Room.CreatorName) (.Room.IsRunning)}} + {{if and (eq .State.Username .Room.CreatorName) (.Room.BotFailed)}} {{end}}
diff --git a/handlers/elements.go b/handlers/elements.go index b6d2ca1..8867332 100644 --- a/handlers/elements.go +++ b/handlers/elements.go @@ -42,8 +42,9 @@ func HandleShowColor(w http.ResponseWriter, r *http.Request) { return } fi, err := getFullInfoByCtx(ctx) - if err != nil { - abortWithError(w, err.Error()) + if err != nil || fi == nil { + log.Error("failed to fetch fi", "error", err) + http.Redirect(w, r, "/", 302) return } if err := validateMove(fi, models.UserRoleGuesser); err != nil { @@ -206,8 +207,9 @@ func HandleMarkCard(w http.ResponseWriter, r *http.Request) { return } fi, err := getFullInfoByCtx(ctx) - if err != nil { - abortWithError(w, err.Error()) + if err != nil || fi == nil { + log.Error("failed to fetch fi", "error", err) + http.Redirect(w, r, "/", 302) return } if err := validateMove(fi, models.UserRoleGuesser); err != nil { @@ -274,8 +276,9 @@ func HandleMarkCard(w http.ResponseWriter, r *http.Request) { func HandleActionHistory(w http.ResponseWriter, r *http.Request) { fi, err := getFullInfoByCtx(r.Context()) - if err != nil { - abortWithError(w, err.Error()) + if err != nil || fi == nil { + log.Error("failed to fetch fi", "error", err) + http.Redirect(w, r, "/", 302) return } tmpl, err := template.ParseGlob("components/*.html") @@ -293,8 +296,9 @@ func HandleAddBot(w http.ResponseWriter, r *http.Request) { team := r.URL.Query().Get("team") role := r.URL.Query().Get("role") fi, err := getFullInfoByCtx(r.Context()) - if err != nil { - abortWithError(w, err.Error()) + if err != nil || fi == nil { + log.Error("failed to fetch fi", "error", err) + http.Redirect(w, r, "/", 302) return } var botname string @@ -319,8 +323,9 @@ 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()) + if err != nil || fi == nil { + log.Error("failed to fetch fi", "error", err) + http.Redirect(w, r, "/", 302) return } if err := llmapi.RemoveBot(botName, fi.Room); err != nil { diff --git a/handlers/game.go b/handlers/game.go index 11a286c..a34e657 100644 --- a/handlers/game.go +++ b/handlers/game.go @@ -72,8 +72,9 @@ func HandleJoinTeam(w http.ResponseWriter, r *http.Request) { } // get username fi, err := getFullInfoByCtx(r.Context()) - if err != nil { - abortWithError(w, err.Error()) + if err != nil || fi == nil { + log.Error("failed to fetch fi", "error", err) + http.Redirect(w, r, "/", 302) return } if fi.Room == nil { @@ -111,8 +112,9 @@ func HandleJoinTeam(w http.ResponseWriter, r *http.Request) { func HandleEndTurn(w http.ResponseWriter, r *http.Request) { // get username fi, err := getFullInfoByCtx(r.Context()) - if err != nil { - abortWithError(w, err.Error()) + if err != nil || fi == nil { + log.Error("failed to fetch fi", "error", err) + http.Redirect(w, r, "/", 302) return } // check if one who pressed it is from the team who has the turn @@ -143,8 +145,9 @@ func HandleEndTurn(w http.ResponseWriter, r *http.Request) { func HandleStartGame(w http.ResponseWriter, r *http.Request) { fi, err := getFullInfoByCtx(r.Context()) - if err != nil { - abortWithError(w, err.Error()) + if err != nil || fi == nil { + log.Error("failed to fetch fi", "error", err) + http.Redirect(w, r, "/", 302) return } // check if enough players @@ -293,8 +296,9 @@ func HandleGiveClue(w http.ResponseWriter, r *http.Request) { clue := r.PostFormValue("clue") num := r.PostFormValue("number") fi, err := getFullInfoByCtx(r.Context()) - if err != nil { - abortWithError(w, err.Error()) + if err != nil || fi == nil { + log.Error("failed to fetch fi", "error", err) + http.Redirect(w, r, "/", 302) return } guessLimitU64, err := strconv.ParseUint(num, 10, 8) @@ -360,8 +364,9 @@ func HandleGiveClue(w http.ResponseWriter, r *http.Request) { func HandleRenotifyBot(w http.ResponseWriter, r *http.Request) { fi, err := getFullInfoByCtx(r.Context()) - if err != nil { - abortWithError(w, err.Error()) + if err != nil || fi == nil { + log.Error("failed to fetch fi", "error", err) + http.Redirect(w, r, "/", 302) return } notifyBotIfNeeded(fi.Room) diff --git a/handlers/handlers.go b/handlers/handlers.go index 20b9398..f7b56c0 100644 --- a/handlers/handlers.go +++ b/handlers/handlers.go @@ -75,8 +75,9 @@ func HandleExit(w http.ResponseWriter, r *http.Request) { return } fi, err := getFullInfoByCtx(r.Context()) - if err != nil { - abortWithError(w, err.Error()) + if err != nil || fi == nil { + log.Error("failed to fetch fi", "error", err) + http.Redirect(w, r, "/", 302) return } if fi.Room.IsRunning { diff --git a/llmapi/main.go b/llmapi/main.go index 8d18853..0742069 100644 --- a/llmapi/main.go +++ b/llmapi/main.go @@ -25,10 +25,10 @@ var ( DoneChanMap = make(map[string]chan bool) mapMutex = &sync.RWMutex{} // got prompt: control character (\\u0000-\\u001F) found while parsing a string at line 4 column 0 - // MimePrompt = `we are playing alias;\nyou are a mime (player who gives a clue of one noun word and number of cards you expect them to open) of the %s team (people who would guess by your clue want open the %s cards);\nplease return your clue, number of cards to open and what words you mean them to find using that clue in json like:\n{\n\"clue\": \"one-word-noun\",\n\"number\": \"number-from-0-to-9\",\n\"words_I_mean_my_team_to_open\": [\"this\", \"that\", ...]\n}\nthe team who openes all their cards first wins.\nplease return json only.\nunopen Blue cards left: %d;\nunopen Red cards left: %d;\nhere is the game info in json:\n%s` - // GuesserPrompt = `we are playing alias;\nyou are to guess words of the %s team (you want open %s cards) by given clue and a number of meant guesses;\nplease return your guesses and words that could be meant by the clue, but you do not wish to open yet, in json like:\n{\n\"guesses\": [\"word1\", \"word2\", ...],\n\"could_be\": [\"this\", \"that\", ...]\n}\nthe team who openes all their cards first wins.\nplease return json only.\nunopen Blue cards left: %d;\nunopen Red cards left: %d;\nhere is the cards (and other info), you need to choose revealed==false words:\n%s` - GuesserSimplePrompt = `we are playing game of alias;\n you were given a clue: \"%s\";\nplease return your guess and words that could be meant by the clue, but you do not wish to open yet, in json like:\n{\n\"guess\": \"most_relevant_word_to_the_clue\",\n\"could_be\": [\"this\", \"that\", ...]\n}\nhere is the words that you can choose from:\n%v` - MimeSimplePrompt = `we are playing alias;\nyou are to give one word clue and a number of words you mean your team to open; your team words: %v;\nhere are the words of opposite team you want to avoid: %v;\nand here is a black word that is critical not to pick: %s;\nplease return your clue, number of cards to open and what words you mean them to find using that clue in json like:\n{\n\"clue\": \"one-word-noun\",\n\"number\": \"number-from-0-to-9-as-string\",\n\"words_I_mean_my_team_to_open\": [\"this\", \"that\", ...]\n}\nplease return json only.\nunopen Blue cards left: %d;\nunopen Red cards left: %d;` + GuesserSimplePrompt = `we are playing game of alias;\n you were given a clue: \"%s\";\nplease return your guess and words that could be meant by the clue, but you do not wish to open yet, in json like:\n{\n\"guess\": \"most_relevant_word_to_the_clue\",\n\"could_be\": [\"this\", \"that\", ...]\n}\nhere is the words that you can choose from:\n%v` + MimeSimplePrompt = `we are playing alias;\nyou are to give one word clue and a number of words you mean your team to open; your team words: %v;\nhere are the words of opposite team you want to avoid: %v;\nand here is a black word that is critical not to pick: %s;\nplease return your clue, number of cards to open and what words you mean them to find using that clue in json like:\n{\n\"clue\": \"one-word-noun\",\n\"number\": \"number-from-0-to-9-as-string\",\n\"words_I_mean_my_team_to_open\": [\"this\", \"that\", ...]\n}\nplease return json only.` + GuesserSimplePromptRU = `мы играем в alias;\n тебе дана подсказка (clue): \"%s\";\nпожалуйста, верни свою догадку (guess), а также слова, что тоже подходят к подсказке, но ты меньше в них уверен, в формате json; пример:\n{\n\"guess\": \"отгадка\",\n\"could_be\": [\"слово1\", \"слово2\", ...]\n}\nвот список слов из которых нужно выбрать:\n%v` + MimeSimplePromptRU = `мы играем в alias;\nтебе нужно дать подсказку одним словом и число слов, что ты подразумевал этой подсказкой; слова твоей комманды: %v;\nслова противоположной комманды, что ты хочешь избежать: %v;\nи вот ЧЕРНОЕ СЛОВО, открыв которое твоя комманда проиграет игру: %s;\nпожалуйста, верни подсказку (одним словом) и количество слов, что ты подразумеваешь в формате json; пример:\n{\n\"clue\": \"подсказка\",\n\"number\": \"число-от-0-до-9-as-string\",\n\"words_I_mean_my_team_to_open\": [\"слово1\", \"слово2\", ...]\n}\nпожалуйста верни только json.` ) func convertToSliceOfStrings(value any) ([]string, error) { @@ -188,6 +188,11 @@ func (b *Bot) BotMove() { b.log.Error("bot loop", "error", err) return } + if room.BotFailed { + if err := repo.RoomUnSetBotFailed(context.Background(), room.ID); err != nil { + b.log.Error("failed to unset bot failed bool", "error", err) + } + } // eventName := models.NotifyBacklogPrefix + room.ID eventName := models.NotifyRoomUpdatePrefix + room.ID eventPayload := "" @@ -231,6 +236,9 @@ func (b *Bot) BotMove() { b.log.Warn("failed to write to journal", "entry", lj) } b.log.Error("bot loop", "error", err) + if err := repo.RoomSetBotFailed(context.Background(), room.ID); err != nil { + b.log.Error("failed to set bot failed bool", "error", err) + } return } tempMap, err := b.LLMParser.ParseBytes(llmResp) @@ -545,6 +553,9 @@ func (b *Bot) BuildSimpleGuesserPrompt(room *models.Room) string { } words[i] = card.Word } + if strings.EqualFold(room.Settings.Language, "ru") { + return fmt.Sprintf(MimeSimplePromptRU, clueAction.Word, words) + } return fmt.Sprintf(GuesserSimplePrompt, clueAction.Word, words) } @@ -573,34 +584,20 @@ func (b *Bot) BuildSimpleMimePrompt(room *models.Room) string { theirwords = append(theirwords, card.Word) } } - return fmt.Sprintf(MimeSimplePrompt, ourwords, theirwords, blackWord, room.BlueCounter, room.RedCounter) + if strings.EqualFold(room.Settings.Language, "ru") { + return fmt.Sprintf(MimeSimplePromptRU, ourwords, theirwords, blackWord) + } + return fmt.Sprintf(MimeSimplePrompt, ourwords, theirwords, blackWord) } func (b *Bot) BuildPrompt(room *models.Room) string { if b.Role == "" { return "" } - // toText := make(map[string]any) - // toText["backlog"] = room.ActionHistory - // // mime sees all colors; - // // guesser sees only revealed ones - // if b.Role == models.UserRoleMime { - // toText["cards"] = room.Cards - // } - // data, err := json.Marshal(toText) - // if err != nil { - // b.log.Error("failed to marshal", "error", err) - // return "" - // } - // Escape the JSON string for inclusion in another JSON field - // escapedData := strings.ReplaceAll(string(data), `"`, `\\"`) if b.Role == models.UserRoleMime { - // return fmt.Sprintf(MimeSimplePrompt, b.Team, b.Team, room.BlueCounter, room.RedCounter, escapedData) - // return fmt.Sprintf(MimePrompt, b.Team, b.Team, room.BlueCounter, room.RedCounter, escapedData) return b.BuildSimpleMimePrompt(room) } if b.Role == models.UserRoleGuesser { - // return fmt.Sprintf(GuesserPrompt, b.Team, b.Team, room.BlueCounter, room.RedCounter, escapedData) return b.BuildSimpleGuesserPrompt(room) } return "" @@ -666,16 +663,5 @@ func (b *Bot) CallLLM(prompt string) ([]byte, error) { b.log.Debug("llm resp", "body", string(body), "url", b.cfg.LLMConfig.URL, "attempt", attempt) return body, nil } - entry := fmt.Sprintf("bot '%s' exceeded attempts to call llm;", b.BotName) - lj := models.Journal{ - Entry: entry, - Username: b.BotName, - RoomID: b.RoomID, - } - if err := repo.JournalCreate(context.Background(), &lj); err != nil { - b.log.Warn("failed to write to journal", "entry", lj) - } - // notify room - // This line should not be reached because each error path returns in the loop. return nil, errors.New("unknown error in retry loop") } diff --git a/migrations/001_initial_schema.up.sql b/migrations/001_initial_schema.up.sql index bae52bd..d5c696a 100644 --- a/migrations/001_initial_schema.up.sql +++ b/migrations/001_initial_schema.up.sql @@ -13,6 +13,7 @@ CREATE TABLE rooms ( mime_done BOOLEAN NOT NULL DEFAULT FALSE, is_running BOOLEAN NOT NULL DEFAULT FALSE, is_over BOOLEAN NOT NULL DEFAULT FALSE, + bot_failed BOOLEAN NOT NULL DEFAULT FALSE, team_won TEXT NOT NULL DEFAULT '', room_link TEXT NOT NULL DEFAULT '' ); diff --git a/models/main.go b/models/main.go index 8bab780..e53d395 100644 --- a/models/main.go +++ b/models/main.go @@ -178,6 +178,8 @@ type Room struct { BotMap map[string]BotPlayer `db:"-"` LogJournal []Journal `db:"-"` Settings GameSettings `db:"-"` + // + BotFailed bool `db:"bot_failed"` } func (r *Room) FindColor(word string) (WordColor, bool) { diff --git a/repos/rooms.go b/repos/rooms.go index 87b901a..343f984 100644 --- a/repos/rooms.go +++ b/repos/rooms.go @@ -15,6 +15,20 @@ type RoomsRepo interface { RoomCreate(ctx context.Context, room *models.Room) error RoomDeleteByID(ctx context.Context, id string) error RoomUpdate(ctx context.Context, room *models.Room) error + RoomSetBotFailed(ctx context.Context, roomID string) error + RoomUnSetBotFailed(ctx context.Context, roomID string) error +} + +func (p *RepoProvider) RoomSetBotFailed(ctx context.Context, roomID string) error { + db := getDB(ctx, p.DB) + _, err := db.ExecContext(ctx, "UPDATE rooms SET bot_failed = true WHERE id = ?", roomID) + return err +} + +func (p *RepoProvider) RoomUnSetBotFailed(ctx context.Context, roomID string) error { + db := getDB(ctx, p.DB) + _, err := db.ExecContext(ctx, "UPDATE rooms SET bot_failed = false WHERE id = ?", roomID) + return err } func (p *RepoProvider) RoomList(ctx context.Context) ([]*models.Room, error) { diff --git a/todos.md b/todos.md index 3f1fb02..e725224 100644 --- a/todos.md +++ b/todos.md @@ -21,8 +21,8 @@ - redo card .revealed use: it should mean that card is revealed for everybody, while mime should be able to see cards as is; + - better styles and fluff; - common auth system between sites; -- signup vs login; -- passwords (to room and to login); +- signup vs login; + +- passwords (to room and to login); + === - show in backlog (and with that in prompt to llm) how many cards are left to open, also additional comment: if guess was right; - gameover to backlog; @@ -33,7 +33,7 @@ - possibly turn markings into parts of names of users (first three letters?); + - at game creation list languages and support them at backend; + - sql ping goroutine with reconnect on fail; + -- player stats: played games, lost, won, rating elo, opened opposite words, opened white words, opened black words. +- player stats: played games, lost, won, rating elo, opened opposite words, opened white words, opened black words. + - at the end of the game, all colors should be revealed; - tracing; @@ -91,5 +91,5 @@ - mime sees the clue input out of turn; (eh) - there is a problem of two timers, they both could switch turn, but it is not easy to stop them from llmapi or handlers. + - journal still does not work; + -- lose/win game; then exit room (while being the creator), then press to stats -> cannot find session in db, although cookie in place and session in db; -- exit endpoints delets player from db; +- lose/win game; then exit room (while being the creator), then press to stats -> cannot find session in db, although cookie in place and session in db; + +- exit endpoints delets player from db; +