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) { | ||||
| // 	w.Header().Set("Content-Type", "text/event-stream") | ||||
| // 	w.Header().Set("Cache-Control", "no-cache") | ||||
| // 	w.Header().Set("Connection", "keep-alive") | ||||
| // 	w.Header().Set("Access-Control-Allow-Origin", "*") | ||||
| // 	// Each connection registers its own message channel with the Broker's connections registry | ||||
| // 	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() | ||||
| // 	} | ||||
| // } | ||||
| var Notifier *Broker | ||||
|  | ||||
| // for use in different packages | ||||
| func init() { | ||||
| 	Notifier = NewBroker() | ||||
| 	go Notifier.Listen() | ||||
| } | ||||
|  | ||||
| func (broker *Broker) ServeHTTP(w http.ResponseWriter, r *http.Request) { | ||||
| 	// 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}} | ||||
|     {{template "cardcounter" .Room}} | ||||
|   {{end}} | ||||
|  | ||||
|   <div id="addbot"> | ||||
|     {{if and (eq .State.Username .Room.CreatorName) (not .Room.IsRunning)}} | ||||
|       {{template "addbot" .}} | ||||
|     {{end}} | ||||
|   </div> | ||||
|   <div class="flex justify-center"> | ||||
|     <!-- Left Panel --> | ||||
|     {{template "teamlist" .Room.BlueTeam}} | ||||
|   | ||||
| @@ -12,6 +12,7 @@ type Config struct { | ||||
| 	SessionLifetime int          `toml:"SESSION_LIFETIME_SECONDS"` | ||||
| 	DBURI           string       `toml:"DBURI"` | ||||
| 	CookieSecret    string       `toml:"COOKIE_SECRET"` | ||||
| 	LLMConfig       LLMConfig    `toml:"LLM"` | ||||
| } | ||||
|  | ||||
| type ServerConfig struct { | ||||
| @@ -19,6 +20,11 @@ type ServerConfig struct { | ||||
| 	Port string `toml:"PORT"` | ||||
| } | ||||
|  | ||||
| type LLMConfig struct { | ||||
| 	URL   string `toml:"LLM_URL"` | ||||
| 	TOKEN string `toml:"LLM_TOKEN"` | ||||
| } | ||||
|  | ||||
| func LoadConfigOrDefault(fn string) *Config { | ||||
| 	if fn == "" { | ||||
| 		fn = "config.toml" | ||||
|   | ||||
| @@ -260,5 +260,8 @@ func loadCards(room *models.Room) { | ||||
| 		fmt.Println("failed to load cards", "error", err) | ||||
| 	} | ||||
| 	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 ( | ||||
| 	"errors" | ||||
| 	"golias/llmapi" | ||||
| 	"golias/models" | ||||
| 	"html/template" | ||||
| 	"net/http" | ||||
| @@ -55,7 +56,7 @@ func HandleShowColor(w http.ResponseWriter, r *http.Request) { | ||||
| 		abortWithError(w, "wait for the clue") | ||||
| 		return | ||||
| 	} | ||||
| 	color, exists := roundWords[word] | ||||
| 	color, exists := fi.Room.WCMap[word] | ||||
| 	log.Debug("got show-color request", "word", word, "color", color) | ||||
| 	if !exists { | ||||
| 		abortWithError(w, "word is not found") | ||||
| @@ -63,7 +64,7 @@ func HandleShowColor(w http.ResponseWriter, r *http.Request) { | ||||
| 	} | ||||
| 	cardword := models.WordCard{ | ||||
| 		Word:     word, | ||||
| 		Color:    models.StrToWordColor(color), | ||||
| 		Color:    color, | ||||
| 		Revealed: true, | ||||
| 	} | ||||
| 	fi.Room.RevealSpecificWord(word) | ||||
| @@ -71,14 +72,14 @@ func HandleShowColor(w http.ResponseWriter, r *http.Request) { | ||||
| 	action := models.Action{ | ||||
| 		Actor:      fi.State.Username, | ||||
| 		ActorColor: string(fi.State.Team), | ||||
| 		WordColor:  color, | ||||
| 		WordColor:  string(color), | ||||
| 		Action:     "guessed", | ||||
| 		Word:       word, | ||||
| 	} | ||||
| 	fi.Room.ActionHistory = append(fi.Room.ActionHistory, action) | ||||
| 	// if opened card is of color of opp team, change turn | ||||
| 	oppositeColor := fi.Room.GetOppositeTeamColor() | ||||
| 	switch color { | ||||
| 	switch string(color) { | ||||
| 	case "black": | ||||
| 		// game over | ||||
| 		fi.Room.IsRunning = false | ||||
| @@ -122,3 +123,21 @@ func HandleActionHistory(w http.ResponseWriter, r *http.Request) { | ||||
| 	} | ||||
| 	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 | ||||
| 	cfg = config.LoadConfigOrDefault("") | ||||
| 	Notifier = broker.NewBroker() | ||||
| 	go Notifier.Listen() | ||||
| 	Notifier = broker.Notifier | ||||
| 	// go Notifier.Listen() | ||||
| } | ||||
|  | ||||
| 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 /word/show-color", handlers.HandleShowColor) | ||||
| 	mux.HandleFunc("POST /check/name", handlers.HandleNameCheck) | ||||
| 	mux.HandleFunc("GET /add-bot", handlers.HandleAddBot) | ||||
| 	// sse | ||||
| 	mux.Handle("GET /sub/sse", handlers.Notifier) | ||||
| 	slog.Info("Listening", "addr", port) | ||||
|   | ||||
| @@ -41,7 +41,7 @@ type Team struct { | ||||
| type Action struct { | ||||
| 	Actor      string | ||||
| 	ActorColor string | ||||
| 	Action     string // clue | guess | ||||
| 	Action     WordColor // clue | guess | ||||
| 	Word       string | ||||
| 	WordColor  string | ||||
| 	Number     string // for clue | ||||
| @@ -60,6 +60,7 @@ type Room struct { | ||||
| 	RedTeam       Team | ||||
| 	BlueTeam      Team | ||||
| 	Cards         []WordCard | ||||
| 	WCMap         map[string]WordColor | ||||
| 	Result        uint8 // 0 for unknown; 1 is win for red; 2 if for blue; | ||||
| 	BlueCounter   uint8 | ||||
| 	RedCounter    uint8 | ||||
| @@ -169,9 +170,9 @@ func (r *Room) RevealSpecificWord(word string) { | ||||
| } | ||||
|  | ||||
| type WordCard struct { | ||||
| 	Word     string | ||||
| 	Color    WordColor | ||||
| 	Revealed bool | ||||
| 	Word     string    `json:"word"` | ||||
| 	Color    WordColor `json:"color"` | ||||
| 	Revealed bool      `json:"revealed"` | ||||
| } | ||||
|  | ||||
| type GameSettings struct { | ||||
|   | ||||
							
								
								
									
										5
									
								
								todos.md
									
									
									
									
									
								
							
							
						
						
									
										5
									
								
								todos.md
									
									
									
									
									
								
							| @@ -5,6 +5,7 @@ | ||||
| - mark cards (instead of opening them (right click?); | ||||
| - 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 | ||||
| - clue sse update; | ||||
| @@ -18,6 +19,6 @@ | ||||
|  | ||||
| ### issues | ||||
| - 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; | ||||
| - 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