diff --git a/broker/sse.go b/broker/sse.go
index 3b4ab79..0ff5da6 100644
--- a/broker/sse.go
+++ b/broker/sse.go
@@ -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)
diff --git a/components/addbotbtn.html b/components/addbotbtn.html
new file mode 100644
index 0000000..889647b
--- /dev/null
+++ b/components/addbotbtn.html
@@ -0,0 +1,10 @@
+{{ define "addbot" }}
+
+
+
+
+
+
+
+
+{{end}}
diff --git a/components/room.html b/components/room.html
index 0ef6702..9945eaf 100644
--- a/components/room.html
+++ b/components/room.html
@@ -27,6 +27,12 @@
{{if .Room.IsRunning}}
{{template "cardcounter" .Room}}
{{end}}
+
+
+ {{if and (eq .State.Username .Room.CreatorName) (not .Room.IsRunning)}}
+ {{template "addbot" .}}
+ {{end}}
+
{{template "teamlist" .Room.BlueTeam}}
diff --git a/config/config.go b/config/config.go
index f15b65e..416137d 100644
--- a/config/config.go
+++ b/config/config.go
@@ -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"
diff --git a/handlers/actions.go b/handlers/actions.go
index 383dfa6..0c70821 100644
--- a/handlers/actions.go
+++ b/handlers/actions.go
@@ -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
+ }
}
diff --git a/handlers/elements.go b/handlers/elements.go
index 39eaabd..a601062 100644
--- a/handlers/elements.go
+++ b/handlers/elements.go
@@ -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, "")
+}
diff --git a/handlers/handlers.go b/handlers/handlers.go
index b8baf26..2c65eba 100644
--- a/handlers/handlers.go
+++ b/handlers/handlers.go
@@ -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{
diff --git a/llmapi/main.go b/llmapi/main.go
new file mode 100644
index 0000000..879cad1
--- /dev/null
+++ b/llmapi/main.go
@@ -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
+}
diff --git a/main.go b/main.go
index c545629..cba93e9 100644
--- a/main.go
+++ b/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)
diff --git a/models/main.go b/models/main.go
index 34c5d1a..b785f2d 100644
--- a/models/main.go
+++ b/models/main.go
@@ -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 {
diff --git a/todos.md b/todos.md
index 1ce9847..146cb08 100644
--- a/todos.md
+++ b/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);