Fix: show color; add bot [WIP]

This commit is contained in:
Grail Finder
2025-05-20 13:10:09 +03:00
parent 24f940f175
commit 2342c56aed
11 changed files with 267 additions and 36 deletions

View File

@ -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
View 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}}

View File

@ -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}}

View File

@ -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"

View File

@ -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
}
} }

View File

@ -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, "")
}

View File

@ -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
View 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
}

View File

@ -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)

View File

@ -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 {

View File

@ -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);