Merge branch 'master' into enha/sse-try
This commit is contained in:
		| @@ -1,7 +1,6 @@ | ||||
| body{ | ||||
|     background-color: #0C1616FF; | ||||
|     color: #8896b2; | ||||
|     max-width: 1000px; | ||||
|     min-width: 0px; | ||||
|     margin: 2em auto !important; | ||||
|     margin-left: auto; | ||||
|   | ||||
										
											Binary file not shown.
										
									
								
							| @@ -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 { | ||||
|   | ||||
| @@ -18,7 +18,7 @@ | ||||
|   if (!window.actionHistoryScrollSet) { | ||||
|     htmx.onLoad(function(target) { | ||||
|       if (target.id === 'actionHistoryContainer') { | ||||
|         target.scrollTop = target.scrollHeight; | ||||
|         target.scrollToBottom(); | ||||
|       } | ||||
|     }); | ||||
|     window.actionHistoryScrollSet = true; | ||||
|   | ||||
| @@ -1,6 +1,7 @@ | ||||
| {{define "room"}} | ||||
| <div id="room-interier" class=space-y-2> | ||||
|   <div id="meta"> | ||||
|   <div id="headwrapper" class="grid grid-cols-1 md:grid-cols-5 md:gap-4"> | ||||
|   <div id="meta" class="md:col-span-1 border-2 rounded-lg text-center space-y-2"> | ||||
|     <p>Hello {{.State.Username}};</p> | ||||
|     <p>Room created by {{.Room.CreatorName}};</p> | ||||
|     <p>Room link:</p> | ||||
| @@ -16,13 +17,13 @@ | ||||
|     {{end}} | ||||
|     <p> | ||||
|       {{if eq .State.Team ""}} | ||||
|       join the team! | ||||
|         you don't have a role! join the team -> | ||||
|       {{else}} | ||||
|       you're on the team <span class="text-{{.State.Team}}-500">{{.State.Team}}</span>! | ||||
|       {{end}} | ||||
|     </p> | ||||
|   </div> | ||||
|   <hr /> | ||||
|   <div id="infopatch"   class="md:col-span-3"> | ||||
|   {{if .Room.IsRunning}} | ||||
|     <p>Turn of the <span class="text-{{.Room.TeamTurn}}-500">{{.Room.TeamTurn}}</span> team</p> | ||||
| {{template "turntimer" .Room}} | ||||
| @@ -48,7 +49,18 @@ | ||||
|     <!-- Right Panel --> | ||||
|     {{template "teamlist" .Room.RedTeam}} | ||||
|   </div> | ||||
|   </div> | ||||
|   </div> | ||||
|   <hr/> | ||||
|   <div class="grid grid-cols-1 md:grid-cols-5 md:gap-4"> | ||||
|     <div hx-get="/actionhistory" hx-trigger="sse:backlog_{{.Room.ID}}" class="md:col-span-1"> | ||||
|       {{template "actionhistory" .Room.ActionHistory}} | ||||
|     </div> | ||||
|     <div id="cardtable" class="md:col-span-3"> | ||||
|       {{template "cardtable" .Room}} | ||||
|     </div> | ||||
|     <div class="hidden md:block md:col-span-1"></div> <!-- Spacer --> | ||||
|   </div> | ||||
|   <div id="systembox" class="overflow-y-auto max-h-96 border-2 border-gray-300 p-4 rounded-lg space-y-2"> | ||||
|     bot thought: <br> | ||||
|     <ul> | ||||
| @@ -57,13 +69,6 @@ | ||||
|     {{end}} | ||||
|     </ul> | ||||
|   </div> | ||||
|   <div hx-get="/actionhistory" hx-trigger="sse:backlog_{{.Room.ID}}"> | ||||
|   {{template "actionhistory" .Room.ActionHistory}} | ||||
|   </div> | ||||
|   <hr/> | ||||
|   <div id="cardtable"> | ||||
|     {{template "cardtable" .Room}} | ||||
|   </div> | ||||
|   <div> | ||||
|     {{if .Room.IsRunning}} | ||||
|     {{if and (eq .State.Role "guesser") (eq .State.Team .Room.TeamTurn)}} | ||||
| @@ -74,7 +79,7 @@ | ||||
|     {{end}} | ||||
|   </div> | ||||
|   <div> | ||||
|     {{if and (eq .State.Username .Room.CreatorName) (.Room.IsRunning)}} | ||||
|     {{if and (eq .State.Username .Room.CreatorName) (.Room.BotFailed)}} | ||||
|       <button hx-get="/renotify-bot" hx-swap="none" class="bg-gray-100 text-black px-1 py-1 rounded">Btn in case llm call failed</button> | ||||
|     {{end}} | ||||
|   </div> | ||||
|   | ||||
| @@ -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 { | ||||
|   | ||||
| @@ -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) | ||||
|   | ||||
| @@ -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 { | ||||
|   | ||||
| @@ -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;` | ||||
| 	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") | ||||
| } | ||||
|   | ||||
| @@ -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 '' | ||||
| ); | ||||
|   | ||||
| @@ -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) { | ||||
|   | ||||
| @@ -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) { | ||||
|   | ||||
							
								
								
									
										10
									
								
								todos.md
									
									
									
									
									
								
							
							
						
						
									
										10
									
								
								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; + | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 Grail Finder
					Grail Finder