Fix: show color; add bot [WIP]
This commit is contained in:
		| @@ -39,29 +39,13 @@ func NewBroker() (broker *Broker) { | |||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
| // func (broker *Broker) ServeHTTP(w http.ResponseWriter, r *http.Request) { | var Notifier *Broker | ||||||
| // 	w.Header().Set("Content-Type", "text/event-stream") |  | ||||||
| // 	w.Header().Set("Cache-Control", "no-cache") | // for use in different packages | ||||||
| // 	w.Header().Set("Connection", "keep-alive") | func init() { | ||||||
| // 	w.Header().Set("Access-Control-Allow-Origin", "*") | 	Notifier = NewBroker() | ||||||
| // 	// Each connection registers its own message channel with the Broker's connections registry | 	go Notifier.Listen() | ||||||
| // 	messageChan := make(NotifierChan) | } | ||||||
| // 	// Signal the broker that we have a new connection |  | ||||||
| // 	broker.newClients <- messageChan |  | ||||||
| // 	// Remove this client from the map of connected clients |  | ||||||
| // 	// when this handler exits. |  | ||||||
| // 	defer func() { |  | ||||||
| // 		broker.closingClients <- messageChan |  | ||||||
| // 	}() |  | ||||||
| // 	// c.Stream(func(w io.Writer) bool { |  | ||||||
| // 	for { |  | ||||||
| // 		// Emit Server Sent Events compatible |  | ||||||
| // 		event := <-messageChan |  | ||||||
| // 		fmt.Fprintf(w, "event:%s; data:%s\n", event.EventName, event.Payload) |  | ||||||
| // 		// c.SSEvent(event.EventName, event.Payload) |  | ||||||
| // 		w.(http.Flusher).Flush() |  | ||||||
| // 	} |  | ||||||
| // } |  | ||||||
|  |  | ||||||
| func (broker *Broker) ServeHTTP(w http.ResponseWriter, r *http.Request) { | func (broker *Broker) ServeHTTP(w http.ResponseWriter, r *http.Request) { | ||||||
| 	// Headers (keep these as-is) | 	// Headers (keep these as-is) | ||||||
|   | |||||||
							
								
								
									
										10
									
								
								components/addbotbtn.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								components/addbotbtn.html
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,10 @@ | |||||||
|  | {{ define "addbot" }} | ||||||
|  | <div> | ||||||
|  |   <button hx-get="/add-bot?team=blue&role=mime" hx-target="#addbot" class="bg-blue-400 text-black px-4 py-2 rounded">Add Bot Mime</button> | ||||||
|  |   <button hx-get="/add-bot?team=red&role=mime" hx-target="#addbot" class="bg-red-400 text-black px-4 py-2 rounded">Add Bot Mime</button> | ||||||
|  | </div> | ||||||
|  | <div> | ||||||
|  |   <button hx-get="/add-bot?team=blue&role=guesser" hx-target="#addbot" class="bg-blue-300 text-black px-4 py-2 rounded">Add Bot Guesser</button> | ||||||
|  |   <button hx-get="/add-bot?team=red&role=guesser" hx-target="#addbot" class="bg-red-300 text-black px-4 py-2 rounded">Add Bot Guesser</button> | ||||||
|  | </div> | ||||||
|  | {{end}} | ||||||
| @@ -27,6 +27,12 @@ | |||||||
|   {{if .Room.IsRunning}} |   {{if .Room.IsRunning}} | ||||||
|     {{template "cardcounter" .Room}} |     {{template "cardcounter" .Room}} | ||||||
|   {{end}} |   {{end}} | ||||||
|  |  | ||||||
|  |   <div id="addbot"> | ||||||
|  |     {{if and (eq .State.Username .Room.CreatorName) (not .Room.IsRunning)}} | ||||||
|  |       {{template "addbot" .}} | ||||||
|  |     {{end}} | ||||||
|  |   </div> | ||||||
|   <div class="flex justify-center"> |   <div class="flex justify-center"> | ||||||
|     <!-- Left Panel --> |     <!-- Left Panel --> | ||||||
|     {{template "teamlist" .Room.BlueTeam}} |     {{template "teamlist" .Room.BlueTeam}} | ||||||
|   | |||||||
| @@ -12,6 +12,7 @@ type Config struct { | |||||||
| 	SessionLifetime int          `toml:"SESSION_LIFETIME_SECONDS"` | 	SessionLifetime int          `toml:"SESSION_LIFETIME_SECONDS"` | ||||||
| 	DBURI           string       `toml:"DBURI"` | 	DBURI           string       `toml:"DBURI"` | ||||||
| 	CookieSecret    string       `toml:"COOKIE_SECRET"` | 	CookieSecret    string       `toml:"COOKIE_SECRET"` | ||||||
|  | 	LLMConfig       LLMConfig    `toml:"LLM"` | ||||||
| } | } | ||||||
|  |  | ||||||
| type ServerConfig struct { | type ServerConfig struct { | ||||||
| @@ -19,6 +20,11 @@ type ServerConfig struct { | |||||||
| 	Port string `toml:"PORT"` | 	Port string `toml:"PORT"` | ||||||
| } | } | ||||||
|  |  | ||||||
|  | type LLMConfig struct { | ||||||
|  | 	URL   string `toml:"LLM_URL"` | ||||||
|  | 	TOKEN string `toml:"LLM_TOKEN"` | ||||||
|  | } | ||||||
|  |  | ||||||
| func LoadConfigOrDefault(fn string) *Config { | func LoadConfigOrDefault(fn string) *Config { | ||||||
| 	if fn == "" { | 	if fn == "" { | ||||||
| 		fn = "config.toml" | 		fn = "config.toml" | ||||||
|   | |||||||
| @@ -260,5 +260,8 @@ func loadCards(room *models.Room) { | |||||||
| 		fmt.Println("failed to load cards", "error", err) | 		fmt.Println("failed to load cards", "error", err) | ||||||
| 	} | 	} | ||||||
| 	room.Cards = cards | 	room.Cards = cards | ||||||
|  | 	room.WCMap = make(map[string]models.WordColor) | ||||||
|  | 	for _, card := range room.Cards { | ||||||
|  | 		room.WCMap[card.Word] = card.Color | ||||||
|  | 	} | ||||||
| } | } | ||||||
|   | |||||||
| @@ -2,6 +2,7 @@ package handlers | |||||||
|  |  | ||||||
| import ( | import ( | ||||||
| 	"errors" | 	"errors" | ||||||
|  | 	"golias/llmapi" | ||||||
| 	"golias/models" | 	"golias/models" | ||||||
| 	"html/template" | 	"html/template" | ||||||
| 	"net/http" | 	"net/http" | ||||||
| @@ -55,7 +56,7 @@ func HandleShowColor(w http.ResponseWriter, r *http.Request) { | |||||||
| 		abortWithError(w, "wait for the clue") | 		abortWithError(w, "wait for the clue") | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 	color, exists := roundWords[word] | 	color, exists := fi.Room.WCMap[word] | ||||||
| 	log.Debug("got show-color request", "word", word, "color", color) | 	log.Debug("got show-color request", "word", word, "color", color) | ||||||
| 	if !exists { | 	if !exists { | ||||||
| 		abortWithError(w, "word is not found") | 		abortWithError(w, "word is not found") | ||||||
| @@ -63,7 +64,7 @@ func HandleShowColor(w http.ResponseWriter, r *http.Request) { | |||||||
| 	} | 	} | ||||||
| 	cardword := models.WordCard{ | 	cardword := models.WordCard{ | ||||||
| 		Word:     word, | 		Word:     word, | ||||||
| 		Color:    models.StrToWordColor(color), | 		Color:    color, | ||||||
| 		Revealed: true, | 		Revealed: true, | ||||||
| 	} | 	} | ||||||
| 	fi.Room.RevealSpecificWord(word) | 	fi.Room.RevealSpecificWord(word) | ||||||
| @@ -71,14 +72,14 @@ func HandleShowColor(w http.ResponseWriter, r *http.Request) { | |||||||
| 	action := models.Action{ | 	action := models.Action{ | ||||||
| 		Actor:      fi.State.Username, | 		Actor:      fi.State.Username, | ||||||
| 		ActorColor: string(fi.State.Team), | 		ActorColor: string(fi.State.Team), | ||||||
| 		WordColor:  color, | 		WordColor:  string(color), | ||||||
| 		Action:     "guessed", | 		Action:     "guessed", | ||||||
| 		Word:       word, | 		Word:       word, | ||||||
| 	} | 	} | ||||||
| 	fi.Room.ActionHistory = append(fi.Room.ActionHistory, action) | 	fi.Room.ActionHistory = append(fi.Room.ActionHistory, action) | ||||||
| 	// if opened card is of color of opp team, change turn | 	// if opened card is of color of opp team, change turn | ||||||
| 	oppositeColor := fi.Room.GetOppositeTeamColor() | 	oppositeColor := fi.Room.GetOppositeTeamColor() | ||||||
| 	switch color { | 	switch string(color) { | ||||||
| 	case "black": | 	case "black": | ||||||
| 		// game over | 		// game over | ||||||
| 		fi.Room.IsRunning = false | 		fi.Room.IsRunning = false | ||||||
| @@ -122,3 +123,21 @@ func HandleActionHistory(w http.ResponseWriter, r *http.Request) { | |||||||
| 	} | 	} | ||||||
| 	tmpl.ExecuteTemplate(w, "actionhistory", fi.Room.ActionHistory) | 	tmpl.ExecuteTemplate(w, "actionhistory", fi.Room.ActionHistory) | ||||||
| } | } | ||||||
|  |  | ||||||
|  | func HandleAddBot(w http.ResponseWriter, r *http.Request) { | ||||||
|  | 	// get team; // get role; make up a name | ||||||
|  | 	team := r.URL.Query().Get("team") | ||||||
|  | 	role := r.URL.Query().Get("role") | ||||||
|  | 	fi, err := getFullInfoByCtx(r.Context()) | ||||||
|  | 	if err != nil { | ||||||
|  | 		abortWithError(w, err.Error()) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 	bot, err := llmapi.NewBot(role, team, "bot1", fi.Room.ID, cfg) | ||||||
|  | 	if err != nil { | ||||||
|  | 		abortWithError(w, err.Error()) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 	bot.StartBot() | ||||||
|  | 	notify(models.NotifyRoomUpdatePrefix+fi.Room.ID, "") | ||||||
|  | } | ||||||
|   | |||||||
| @@ -25,8 +25,8 @@ func init() { | |||||||
| 	})) | 	})) | ||||||
| 	memcache = cache.MemCache | 	memcache = cache.MemCache | ||||||
| 	cfg = config.LoadConfigOrDefault("") | 	cfg = config.LoadConfigOrDefault("") | ||||||
| 	Notifier = broker.NewBroker() | 	Notifier = broker.Notifier | ||||||
| 	go Notifier.Listen() | 	// go Notifier.Listen() | ||||||
| } | } | ||||||
|  |  | ||||||
| var roundWords = map[string]string{ | var roundWords = map[string]string{ | ||||||
|   | |||||||
							
								
								
									
										200
									
								
								llmapi/main.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										200
									
								
								llmapi/main.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,200 @@ | |||||||
|  | package llmapi | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"encoding/json" | ||||||
|  | 	"errors" | ||||||
|  | 	"fmt" | ||||||
|  | 	"golias/config" | ||||||
|  | 	"golias/models" | ||||||
|  | 	"golias/pkg/cache" | ||||||
|  | 	"io" | ||||||
|  | 	"log/slog" | ||||||
|  | 	"net/http" | ||||||
|  | 	"strings" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | // TODO: config for url and token | ||||||
|  | // completion prompt | ||||||
|  | // MIME: llm needs to know all the cards, colors and previous actions | ||||||
|  | // GUESSER: llm needs to know all the cards and previous actions | ||||||
|  |  | ||||||
|  | type Bot struct { | ||||||
|  | 	Role    string // gueeser | mime | ||||||
|  | 	Team    string | ||||||
|  | 	cfg     *config.Config | ||||||
|  | 	RoomID  string // can we get a room from here? | ||||||
|  | 	BotName string | ||||||
|  | 	log     *slog.Logger | ||||||
|  | 	// channels for communicaton | ||||||
|  | 	SignalsCh chan bool | ||||||
|  | 	DoneCh    chan bool | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // StartBot | ||||||
|  | func (b *Bot) StartBot() { | ||||||
|  | 	for { | ||||||
|  | 		select { | ||||||
|  | 		case <-b.SignalsCh: | ||||||
|  | 			// get room cards and actions | ||||||
|  | 			room, err := getRoomByID(b.RoomID) | ||||||
|  | 			if err != nil { | ||||||
|  | 				b.log.Error("bot loop", "error", err) | ||||||
|  | 				return | ||||||
|  | 			} | ||||||
|  | 			// form prompt | ||||||
|  | 			prompt := b.BuildPrompt(room) | ||||||
|  | 			b.log.Debug("got prompt", "prompt", prompt) | ||||||
|  | 			// call llm | ||||||
|  | 			if err := b.CallLLM(prompt); err != nil { | ||||||
|  | 				b.log.Error("bot loop", "error", err) | ||||||
|  | 				return | ||||||
|  | 			} | ||||||
|  | 		// parse response | ||||||
|  | 		// if mime -> give clue | ||||||
|  | 		// if guesser -> open card (does opening one card prompting new loop?) | ||||||
|  | 		// send notification to sse broker | ||||||
|  | 		case <-b.DoneCh: | ||||||
|  | 			b.log.Debug("got done signal", "bot-name", b.BotName) | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // EndBot | ||||||
|  |  | ||||||
|  | func NewBot(role, team, name, roomID string, cfg *config.Config) (*Bot, error) { | ||||||
|  | 	bot := &Bot{ | ||||||
|  | 		Role:    role, | ||||||
|  | 		RoomID:  roomID, | ||||||
|  | 		BotName: name, | ||||||
|  | 		Team:    team, | ||||||
|  | 		cfg:     cfg, | ||||||
|  | 	} | ||||||
|  | 	// add to room | ||||||
|  | 	room, err := getRoomByID(bot.RoomID) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  | 	// check if not running | ||||||
|  | 	if role == "mime" && room.IsRunning { | ||||||
|  | 		return nil, errors.New("cannot join after game started") | ||||||
|  | 	} | ||||||
|  | 	room.PlayerList = append(room.PlayerList, name) | ||||||
|  | 	switch team { | ||||||
|  | 	case "red": | ||||||
|  | 		if role == "mime" { | ||||||
|  | 			room.RedTeam.Mime = name | ||||||
|  | 		} else if role == "guesser" { | ||||||
|  | 			room.RedTeam.Guessers = append(room.RedTeam.Guessers, name) | ||||||
|  | 		} else { | ||||||
|  | 			return nil, fmt.Errorf("uknown role: %s", role) | ||||||
|  | 		} | ||||||
|  | 	case "blue": | ||||||
|  | 		if role == "mime" { | ||||||
|  | 			room.BlueTeam.Mime = name | ||||||
|  | 		} else if role == "guesser" { | ||||||
|  | 			room.BlueTeam.Guessers = append(room.BlueTeam.Guessers, name) | ||||||
|  | 		} else { | ||||||
|  | 			return nil, fmt.Errorf("uknown role: %s", role) | ||||||
|  | 		} | ||||||
|  | 	default: | ||||||
|  | 		return nil, fmt.Errorf("uknown team: %s", team) | ||||||
|  | 	} | ||||||
|  | 	if err := saveRoom(room); err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  | 	go bot.StartBot() // run bot routine | ||||||
|  | 	return bot, nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func getRoomByID(roomID string) (*models.Room, error) { | ||||||
|  | 	roomBytes, err := cache.MemCache.Get(models.CacheRoomPrefix + roomID) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  | 	resp := &models.Room{} | ||||||
|  | 	if err := json.Unmarshal(roomBytes, &resp); err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  | 	return resp, nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func saveRoom(room *models.Room) error { | ||||||
|  | 	key := models.CacheRoomPrefix + room.ID | ||||||
|  | 	data, err := json.Marshal(room) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 	cache.MemCache.Set(key, data) | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | 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 | ||||||
|  | 	} | ||||||
|  | 	if b.Role == models.UserRoleGuesser { | ||||||
|  | 		copiedCards := []models.WordCard{} | ||||||
|  | 		copy(copiedCards, room.Cards) | ||||||
|  | 		for i, card := range copiedCards { | ||||||
|  | 			if !card.Revealed { | ||||||
|  | 				copiedCards[i].Color = models.WordColorUknown | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 		toText["cards"] = copiedCards | ||||||
|  | 	} | ||||||
|  | 	data, err := json.MarshalIndent(toText, "", "  ") | ||||||
|  | 	if err != nil { | ||||||
|  | 		// log | ||||||
|  | 		return "" | ||||||
|  | 	} | ||||||
|  | 	return string(data) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (b *Bot) CallLLM(prompt string) error { | ||||||
|  | 	method := "POST" | ||||||
|  | 	payload := strings.NewReader(fmt.Sprintf(`{ | ||||||
|  |   "model": "deepseek-chat", | ||||||
|  |   "prompt": "%s", | ||||||
|  |   "echo": false, | ||||||
|  |   "frequency_penalty": 0, | ||||||
|  |   "logprobs": 0, | ||||||
|  |   "max_tokens": 1024, | ||||||
|  |   "presence_penalty": 0, | ||||||
|  |   "stop": null, | ||||||
|  |   "stream": false, | ||||||
|  |   "stream_options": null, | ||||||
|  |   "suffix": null, | ||||||
|  |   "temperature": 1, | ||||||
|  |   "top_p": 1 | ||||||
|  | }`, prompt)) | ||||||
|  | 	client := &http.Client{} | ||||||
|  | 	req, err := http.NewRequest(method, b.cfg.LLMConfig.URL, payload) | ||||||
|  | 	if err != nil { | ||||||
|  | 		fmt.Println(err) | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 	req.Header.Add("Content-Type", "application/json") | ||||||
|  | 	req.Header.Add("Accept", "application/json") | ||||||
|  | 	req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", b.cfg.LLMConfig.TOKEN)) | ||||||
|  | 	res, err := client.Do(req) | ||||||
|  | 	if err != nil { | ||||||
|  | 		fmt.Println(err) | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 	defer res.Body.Close() | ||||||
|  | 	body, err := io.ReadAll(res.Body) | ||||||
|  | 	if err != nil { | ||||||
|  | 		fmt.Println(err) | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 	fmt.Println(string(body)) | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
							
								
								
									
										1
									
								
								main.go
									
									
									
									
									
								
							
							
						
						
									
										1
									
								
								main.go
									
									
									
									
									
								
							| @@ -37,6 +37,7 @@ func ListenToRequests(port string) error { | |||||||
| 	mux.HandleFunc("GET /room/hideform", handlers.HandleHideCreateForm) | 	mux.HandleFunc("GET /room/hideform", handlers.HandleHideCreateForm) | ||||||
| 	mux.HandleFunc("GET /word/show-color", handlers.HandleShowColor) | 	mux.HandleFunc("GET /word/show-color", handlers.HandleShowColor) | ||||||
| 	mux.HandleFunc("POST /check/name", handlers.HandleNameCheck) | 	mux.HandleFunc("POST /check/name", handlers.HandleNameCheck) | ||||||
|  | 	mux.HandleFunc("GET /add-bot", handlers.HandleAddBot) | ||||||
| 	// sse | 	// sse | ||||||
| 	mux.Handle("GET /sub/sse", handlers.Notifier) | 	mux.Handle("GET /sub/sse", handlers.Notifier) | ||||||
| 	slog.Info("Listening", "addr", port) | 	slog.Info("Listening", "addr", port) | ||||||
|   | |||||||
| @@ -41,7 +41,7 @@ type Team struct { | |||||||
| type Action struct { | type Action struct { | ||||||
| 	Actor      string | 	Actor      string | ||||||
| 	ActorColor string | 	ActorColor string | ||||||
| 	Action     string // clue | guess | 	Action     WordColor // clue | guess | ||||||
| 	Word       string | 	Word       string | ||||||
| 	WordColor  string | 	WordColor  string | ||||||
| 	Number     string // for clue | 	Number     string // for clue | ||||||
| @@ -60,6 +60,7 @@ type Room struct { | |||||||
| 	RedTeam       Team | 	RedTeam       Team | ||||||
| 	BlueTeam      Team | 	BlueTeam      Team | ||||||
| 	Cards         []WordCard | 	Cards         []WordCard | ||||||
|  | 	WCMap         map[string]WordColor | ||||||
| 	Result        uint8 // 0 for unknown; 1 is win for red; 2 if for blue; | 	Result        uint8 // 0 for unknown; 1 is win for red; 2 if for blue; | ||||||
| 	BlueCounter   uint8 | 	BlueCounter   uint8 | ||||||
| 	RedCounter    uint8 | 	RedCounter    uint8 | ||||||
| @@ -169,9 +170,9 @@ func (r *Room) RevealSpecificWord(word string) { | |||||||
| } | } | ||||||
|  |  | ||||||
| type WordCard struct { | type WordCard struct { | ||||||
| 	Word     string | 	Word     string    `json:"word"` | ||||||
| 	Color    WordColor | 	Color    WordColor `json:"color"` | ||||||
| 	Revealed bool | 	Revealed bool      `json:"revealed"` | ||||||
| } | } | ||||||
|  |  | ||||||
| type GameSettings struct { | type GameSettings struct { | ||||||
|   | |||||||
							
								
								
									
										5
									
								
								todos.md
									
									
									
									
									
								
							
							
						
						
									
										5
									
								
								todos.md
									
									
									
									
									
								
							| @@ -5,6 +5,7 @@ | |||||||
| - mark cards (instead of opening them (right click?); | - mark cards (instead of opening them (right click?); | ||||||
| - invite link; | - invite link; | ||||||
| - login with invite link; | - login with invite link; | ||||||
|  | - add html icons of whos turn it is (like an image of big ? when mime is thinking); | ||||||
|  |  | ||||||
| #### sse points | #### sse points | ||||||
| - clue sse update; | - clue sse update; | ||||||
| @@ -18,6 +19,6 @@ | |||||||
|  |  | ||||||
| ### issues | ### issues | ||||||
| - after the game started (isrunning) players should be able join guessers, but not switch team, or join as a mime; | - after the game started (isrunning) players should be able join guessers, but not switch team, or join as a mime; | ||||||
| - do not let wrong team press buttons; |  | ||||||
| - cleanup backlog after new game is started; | - cleanup backlog after new game is started; | ||||||
| - guesser: word is not found | - guessers should not be able to open more cards, than mime gave them +1; | ||||||
|  | - sse hangs / fails connection which causes to wait for cards to open a few seconds (on local machine); | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user
	 Grail Finder
					Grail Finder