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