Compare commits
17 Commits
3cb43d5129
...
enha/sse-t
Author | SHA1 | Date | |
---|---|---|---|
![]() |
817d69c425 | ||
![]() |
acc3f11ee3 | ||
![]() |
9fc36eb7ea | ||
![]() |
123d6c240f | ||
![]() |
6951ec0535 | ||
![]() |
ad44dc0642 | ||
![]() |
155aa1b2cb | ||
![]() |
757586ea22 | ||
![]() |
8f9865db3f | ||
![]() |
a934d07be3 | ||
![]() |
acf1386c73 | ||
![]() |
f01fc12510 | ||
![]() |
329f849a72 | ||
![]() |
134b7b6262 | ||
![]() |
ea27d35254 | ||
![]() |
9a949757f2 | ||
![]() |
7beccb84a2 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -4,3 +4,4 @@ gralias
|
|||||||
store.json
|
store.json
|
||||||
config.toml
|
config.toml
|
||||||
gralias.db
|
gralias.db
|
||||||
|
traces.log
|
||||||
|
@@ -1,7 +1,6 @@
|
|||||||
body{
|
body{
|
||||||
background-color: #0C1616FF;
|
background-color: #0C1616FF;
|
||||||
color: #8896b2;
|
color: #8896b2;
|
||||||
max-width: 1000px;
|
|
||||||
min-width: 0px;
|
min-width: 0px;
|
||||||
margin: 2em auto !important;
|
margin: 2em auto !important;
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
|
Binary file not shown.
@@ -1,15 +1,17 @@
|
|||||||
package broker
|
package broker
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"os"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
// the amount of time to wait when pushing a message to
|
// the amount of time to wait when pushing a message to
|
||||||
// a slow client or a client that closed after `range clients` started.
|
// a slow client or a client that closed after `range clients` started.
|
||||||
const patience time.Duration = time.Second * 1
|
// const patience time.Duration = time.Second * 1
|
||||||
|
|
||||||
type (
|
type (
|
||||||
NotificationEvent struct {
|
NotificationEvent struct {
|
||||||
@@ -20,11 +22,8 @@ type (
|
|||||||
Broker struct {
|
Broker struct {
|
||||||
// Events are pushed to this channel by the main events-gathering routine
|
// Events are pushed to this channel by the main events-gathering routine
|
||||||
Notifier NotifierChan
|
Notifier NotifierChan
|
||||||
// New client connections
|
log *slog.Logger
|
||||||
newClients chan NotifierChan
|
addClient chan NotifierChan
|
||||||
// Closed client connections
|
|
||||||
closingClients chan NotifierChan
|
|
||||||
// Client connections registry
|
|
||||||
clients map[NotifierChan]struct{}
|
clients map[NotifierChan]struct{}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -32,10 +31,13 @@ type (
|
|||||||
func NewBroker() (broker *Broker) {
|
func NewBroker() (broker *Broker) {
|
||||||
// Instantiate a broker
|
// Instantiate a broker
|
||||||
return &Broker{
|
return &Broker{
|
||||||
Notifier: make(NotifierChan, 1),
|
Notifier: make(NotifierChan, 100),
|
||||||
newClients: make(chan NotifierChan),
|
log: slog.New(slog.NewJSONHandler(os.Stderr, &slog.HandlerOptions{
|
||||||
closingClients: make(chan NotifierChan),
|
Level: slog.LevelDebug,
|
||||||
clients: make(map[NotifierChan]struct{}),
|
AddSource: true,
|
||||||
|
})),
|
||||||
|
addClient: make(chan NotifierChan, 10),
|
||||||
|
clients: map[NotifierChan]struct{}{},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -44,7 +46,6 @@ var Notifier *Broker
|
|||||||
// for use in different packages
|
// for use in different packages
|
||||||
func init() {
|
func init() {
|
||||||
Notifier = NewBroker()
|
Notifier = NewBroker()
|
||||||
go Notifier.Listen()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (broker *Broker) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
func (broker *Broker) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
@@ -59,29 +60,38 @@ func (broker *Broker) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
w.Header().Set("Access-Control-Allow-Origin", origin)
|
w.Header().Set("Access-Control-Allow-Origin", origin)
|
||||||
w.Header().Set("Access-Control-Allow-Credentials", "true")
|
w.Header().Set("Access-Control-Allow-Credentials", "true")
|
||||||
messageChan := make(NotifierChan)
|
|
||||||
broker.newClients <- messageChan
|
|
||||||
defer func() { broker.closingClients <- messageChan }()
|
|
||||||
ctx := r.Context()
|
ctx := r.Context()
|
||||||
|
msgChan := make(NotifierChan, 10)
|
||||||
|
broker.addClient <- msgChan
|
||||||
// browser can close sse on its own; ping every 2s to prevent
|
// browser can close sse on its own; ping every 2s to prevent
|
||||||
heartbeat := time.NewTicker(2 * time.Second)
|
heartbeat := time.NewTicker(8 * time.Second)
|
||||||
defer heartbeat.Stop()
|
defer heartbeat.Stop()
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
|
broker.log.Debug("broker: got ctx done")
|
||||||
// Client disconnected
|
// Client disconnected
|
||||||
return
|
return
|
||||||
case event := <-messageChan:
|
case event := <-broker.Notifier:
|
||||||
|
broker.log.Debug("got event", "event", event)
|
||||||
|
for i := 0; i < 10; i++ { // Repeat 3 times
|
||||||
_, err := fmt.Fprintf(w, "event: %s\ndata: %s\n\n", event.EventName, event.Payload)
|
_, err := fmt.Fprintf(w, "event: %s\ndata: %s\n\n", event.EventName, event.Payload)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println(err)
|
broker.log.Error("write failed", "error", err)
|
||||||
// Client disconnected
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
w.(http.Flusher).Flush()
|
w.(http.Flusher).Flush()
|
||||||
|
// Short delay between sends (non-blocking)
|
||||||
|
select {
|
||||||
|
case <-time.After(20 * time.Millisecond): // Adjust delay as needed
|
||||||
|
case <-ctx.Done():
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
case <-heartbeat.C:
|
case <-heartbeat.C:
|
||||||
// Send SSE heartbeat comment
|
// Send SSE heartbeat comment
|
||||||
if _, err := w.Write([]byte(": heartbeat\n\n")); err != nil {
|
if _, err := fmt.Fprint(w, ":\n\n"); err != nil {
|
||||||
|
broker.log.Error("failed to write heartbeat", "error", err)
|
||||||
return // Client disconnected
|
return // Client disconnected
|
||||||
}
|
}
|
||||||
w.(http.Flusher).Flush()
|
w.(http.Flusher).Flush()
|
||||||
@@ -89,31 +99,14 @@ func (broker *Broker) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Listen for new notifications and redistribute them to clients
|
func (broker *Broker) Listen(ctx context.Context) {
|
||||||
func (broker *Broker) Listen() {
|
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case s := <-broker.newClients:
|
case <-ctx.Done():
|
||||||
// A new client has connected.
|
return
|
||||||
// Register their message channel
|
case clientChan := <-broker.addClient:
|
||||||
broker.clients[s] = struct{}{}
|
// mutex
|
||||||
slog.Info("Client added", "clients listening", len(broker.clients))
|
broker.clients[clientChan] = struct{}{}
|
||||||
case s := <-broker.closingClients:
|
|
||||||
// A client has dettached and we want to
|
|
||||||
// stop sending them messages.
|
|
||||||
delete(broker.clients, s)
|
|
||||||
slog.Info("Client removed", "clients listening", len(broker.clients))
|
|
||||||
case event := <-broker.Notifier:
|
|
||||||
// We got a new event from the outside!
|
|
||||||
// Send event to all connected clients
|
|
||||||
for clientMessageChan := range broker.clients {
|
|
||||||
select {
|
|
||||||
case clientMessageChan <- event:
|
|
||||||
case <-time.After(patience):
|
|
||||||
delete(broker.clients, clientMessageChan)
|
|
||||||
slog.Info("Client was removed", "clients listening", len(broker.clients))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -18,7 +18,7 @@
|
|||||||
if (!window.actionHistoryScrollSet) {
|
if (!window.actionHistoryScrollSet) {
|
||||||
htmx.onLoad(function(target) {
|
htmx.onLoad(function(target) {
|
||||||
if (target.id === 'actionHistoryContainer') {
|
if (target.id === 'actionHistoryContainer') {
|
||||||
target.scrollTop = target.scrollHeight;
|
target.scrollToBottom();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
window.actionHistoryScrollSet = true;
|
window.actionHistoryScrollSet = true;
|
||||||
|
@@ -13,6 +13,12 @@
|
|||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="ancestor" hx-ext="sse" sse-connect="/sub/sse">
|
<div id="ancestor" hx-ext="sse" sse-connect="/sub/sse">
|
||||||
|
<script type="text/javascript">
|
||||||
|
document.body.addEventListener('htmx:sseError', function (e) {
|
||||||
|
// do something before the event data is swapped in
|
||||||
|
console.log(e)
|
||||||
|
})
|
||||||
|
</script>
|
||||||
<div id="main-content">
|
<div id="main-content">
|
||||||
{{template "main" .}}
|
{{template "main" .}}
|
||||||
</div>
|
</div>
|
||||||
|
@@ -22,6 +22,7 @@
|
|||||||
{{template "roomlist" .List}}
|
{{template "roomlist" .List}}
|
||||||
</div>
|
</div>
|
||||||
{{else}}
|
{{else}}
|
||||||
|
<div id="sse-listener" sse-connect="/sub/sse" hx-trigger="sse:roomupdate_{{.State.RoomID}}" hx-get="/room" hx-target="#room-interier" hx-swap="none" style="display:none;"></div>
|
||||||
<div id="room">
|
<div id="room">
|
||||||
{{template "room" .}}
|
{{template "room" .}}
|
||||||
</div>
|
</div>
|
||||||
|
@@ -1,6 +1,7 @@
|
|||||||
{{define "room"}}
|
{{define "room"}}
|
||||||
<div id="interier" hx-get="/" hx-trigger="sse:roomupdate_{{.State.RoomID}}" class=space-y-2>
|
<div id="room-interier" class=space-y-2>
|
||||||
<div id="meta">
|
<div id="headwrapper" class="grid grid-cols-1 md:grid-cols-5 md:gap-4">
|
||||||
|
<div id="meta" class="md:col-span-1 border-2 rounded-lg text-center space-y-2">
|
||||||
<p>Hello {{.State.Username}};</p>
|
<p>Hello {{.State.Username}};</p>
|
||||||
<p>Room created by {{.Room.CreatorName}};</p>
|
<p>Room created by {{.Room.CreatorName}};</p>
|
||||||
<p>Room link:</p>
|
<p>Room link:</p>
|
||||||
@@ -16,13 +17,13 @@
|
|||||||
{{end}}
|
{{end}}
|
||||||
<p>
|
<p>
|
||||||
{{if eq .State.Team ""}}
|
{{if eq .State.Team ""}}
|
||||||
join the team!
|
you don't have a role! join the team ->
|
||||||
{{else}}
|
{{else}}
|
||||||
you're on the team <span class="text-{{.State.Team}}-500">{{.State.Team}}</span>!
|
you're on the team <span class="text-{{.State.Team}}-500">{{.State.Team}}</span>!
|
||||||
{{end}}
|
{{end}}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<hr />
|
<div id="infopatch" class="md:col-span-3">
|
||||||
{{if .Room.IsRunning}}
|
{{if .Room.IsRunning}}
|
||||||
<p>Turn of the <span class="text-{{.Room.TeamTurn}}-500">{{.Room.TeamTurn}}</span> team</p>
|
<p>Turn of the <span class="text-{{.Room.TeamTurn}}-500">{{.Room.TeamTurn}}</span> team</p>
|
||||||
{{template "turntimer" .Room}}
|
{{template "turntimer" .Room}}
|
||||||
@@ -48,7 +49,18 @@
|
|||||||
<!-- Right Panel -->
|
<!-- Right Panel -->
|
||||||
{{template "teamlist" .Room.RedTeam}}
|
{{template "teamlist" .Room.RedTeam}}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<hr/>
|
<hr/>
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-5 md:gap-4">
|
||||||
|
<div hx-get="/actionhistory" class="md:col-span-1">
|
||||||
|
{{template "actionhistory" .Room.ActionHistory}}
|
||||||
|
</div>
|
||||||
|
<div id="cardtable" class="md:col-span-3">
|
||||||
|
{{template "cardtable" .Room}}
|
||||||
|
</div>
|
||||||
|
<div class="hidden md:block md:col-span-1"></div> <!-- Spacer -->
|
||||||
|
</div>
|
||||||
<div id="systembox" class="overflow-y-auto max-h-96 border-2 border-gray-300 p-4 rounded-lg space-y-2">
|
<div id="systembox" class="overflow-y-auto max-h-96 border-2 border-gray-300 p-4 rounded-lg space-y-2">
|
||||||
bot thought: <br>
|
bot thought: <br>
|
||||||
<ul>
|
<ul>
|
||||||
@@ -57,13 +69,6 @@
|
|||||||
{{end}}
|
{{end}}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div hx-get="/actionhistory" hx-trigger="sse:backlog_{{.Room.ID}}">
|
|
||||||
{{template "actionhistory" .Room.ActionHistory}}
|
|
||||||
</div>
|
|
||||||
<hr/>
|
|
||||||
<div id="cardtable">
|
|
||||||
{{template "cardtable" .Room}}
|
|
||||||
</div>
|
|
||||||
<div>
|
<div>
|
||||||
{{if .Room.IsRunning}}
|
{{if .Room.IsRunning}}
|
||||||
{{if and (eq .State.Role "guesser") (eq .State.Team .Room.TeamTurn)}}
|
{{if and (eq .State.Role "guesser") (eq .State.Team .Room.TeamTurn)}}
|
||||||
@@ -74,7 +79,7 @@
|
|||||||
{{end}}
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
{{if and (eq .State.Username .Room.CreatorName) (.Room.IsRunning)}}
|
{{if and (eq .State.Username .Room.CreatorName) (.Room.BotFailed)}}
|
||||||
<button hx-get="/renotify-bot" hx-swap="none" class="bg-gray-100 text-black px-1 py-1 rounded">Btn in case llm call failed</button>
|
<button hx-get="/renotify-bot" hx-swap="none" class="bg-gray-100 text-black px-1 py-1 rounded">Btn in case llm call failed</button>
|
||||||
{{end}}
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
|
@@ -25,6 +25,7 @@
|
|||||||
<thead class="bg-gray-800 text-white">
|
<thead class="bg-gray-800 text-white">
|
||||||
<tr>
|
<tr>
|
||||||
<th class="py-2 px-4">Player</th>
|
<th class="py-2 px-4">Player</th>
|
||||||
|
<th class="py-2 px-4">Rating</th>
|
||||||
<th class="py-2 px-4">Games Played</th>
|
<th class="py-2 px-4">Games Played</th>
|
||||||
<th class="py-2 px-4">Games Won</th>
|
<th class="py-2 px-4">Games Won</th>
|
||||||
<th class="py-2 px-4">Games Lost</th>
|
<th class="py-2 px-4">Games Lost</th>
|
||||||
@@ -36,6 +37,7 @@
|
|||||||
{{range .}}
|
{{range .}}
|
||||||
<tr>
|
<tr>
|
||||||
<td class="py-2 px-4 border">{{.Username}}</td>
|
<td class="py-2 px-4 border">{{.Username}}</td>
|
||||||
|
<td class="py-2 px-4 border">{{.Rating}}</td>
|
||||||
<td class="py-2 px-4 border">{{.GamesPlayed}}</td>
|
<td class="py-2 px-4 border">{{.GamesPlayed}}</td>
|
||||||
<td class="py-2 px-4 border">{{.GamesWon}}</td>
|
<td class="py-2 px-4 border">{{.GamesWon}}</td>
|
||||||
<td class="py-2 px-4 border">{{.GamesLost}}</td>
|
<td class="py-2 px-4 border">{{.GamesLost}}</td>
|
||||||
|
8
docker-compose.yml
Normal file
8
docker-compose.yml
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
version: '3.8'
|
||||||
|
services:
|
||||||
|
jaeger:
|
||||||
|
image: jaegertracing/all-in-one:latest
|
||||||
|
ports:
|
||||||
|
- "6831:6831/udp"
|
||||||
|
- "14268:14268"
|
||||||
|
- "16686:16686"
|
13
go.mod
13
go.mod
@@ -12,6 +12,19 @@ require (
|
|||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
|
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||||
|
github.com/go-logr/logr v1.4.3 // indirect
|
||||||
|
github.com/go-logr/stdr v1.2.2 // indirect
|
||||||
|
github.com/google/uuid v1.6.0 // indirect
|
||||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
|
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
|
||||||
|
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.62.0 // indirect
|
||||||
|
go.opentelemetry.io/otel v1.37.0 // indirect
|
||||||
|
go.opentelemetry.io/otel/exporters/jaeger v1.17.0 // indirect
|
||||||
|
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.37.0 // indirect
|
||||||
|
go.opentelemetry.io/otel/metric v1.37.0 // indirect
|
||||||
|
go.opentelemetry.io/otel/sdk v1.37.0 // indirect
|
||||||
|
go.opentelemetry.io/otel/trace v1.37.0 // indirect
|
||||||
|
golang.org/x/sys v0.33.0 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
)
|
)
|
||||||
|
27
go.sum
27
go.sum
@@ -4,8 +4,17 @@ github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg
|
|||||||
github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
|
github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
|
||||||
|
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||||
|
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||||
|
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
||||||
|
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||||
|
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||||
|
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||||
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
|
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
|
||||||
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
|
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
|
||||||
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o=
|
github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o=
|
||||||
github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY=
|
github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY=
|
||||||
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
|
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
|
||||||
@@ -19,6 +28,24 @@ github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU=
|
|||||||
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
|
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
|
||||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||||
|
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
|
||||||
|
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
|
||||||
|
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.62.0 h1:Hf9xI/XLML9ElpiHVDNwvqI0hIFlzV8dgIr35kV1kRU=
|
||||||
|
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.62.0/go.mod h1:NfchwuyNoMcZ5MLHwPrODwUF1HWCXWrL31s8gSAdIKY=
|
||||||
|
go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ=
|
||||||
|
go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I=
|
||||||
|
go.opentelemetry.io/otel/exporters/jaeger v1.17.0 h1:D7UpUy2Xc2wsi1Ras6V40q806WM07rqoCWzXu7Sqy+4=
|
||||||
|
go.opentelemetry.io/otel/exporters/jaeger v1.17.0/go.mod h1:nPCqOnEH9rNLKqH/+rrUjiMzHJdV1BlpKcTwRTyKkKI=
|
||||||
|
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.37.0 h1:SNhVp/9q4Go/XHBkQ1/d5u9P/U+L1yaGPoi0x+mStaI=
|
||||||
|
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.37.0/go.mod h1:tx8OOlGH6R4kLV67YaYO44GFXloEjGPZuMjEkaaqIp4=
|
||||||
|
go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE=
|
||||||
|
go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E=
|
||||||
|
go.opentelemetry.io/otel/sdk v1.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI=
|
||||||
|
go.opentelemetry.io/otel/sdk v1.37.0/go.mod h1:VredYzxUvuo2q3WRcDnKDjbdvmO0sCzOvVAiY+yUkAg=
|
||||||
|
go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4=
|
||||||
|
go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0=
|
||||||
|
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
|
||||||
|
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
@@ -42,15 +42,15 @@ func HandleShowColor(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
fi, err := getFullInfoByCtx(ctx)
|
fi, err := getFullInfoByCtx(ctx)
|
||||||
if err != nil {
|
if err != nil || fi == nil {
|
||||||
abortWithError(w, err.Error())
|
log.Error("failed to fetch fi", "error", err)
|
||||||
|
http.Redirect(w, r, "/", 302)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if err := validateMove(fi, models.UserRoleGuesser); err != nil {
|
if err := validateMove(fi, models.UserRoleGuesser); err != nil {
|
||||||
abortWithError(w, err.Error())
|
abortWithError(w, err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// color, exists := fi.Room.WCMap[word]
|
|
||||||
color, exists := fi.Room.FindColor(word)
|
color, exists := fi.Room.FindColor(word)
|
||||||
if !exists {
|
if !exists {
|
||||||
abortWithError(w, "word is not found")
|
abortWithError(w, "word is not found")
|
||||||
@@ -207,8 +207,9 @@ func HandleMarkCard(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
fi, err := getFullInfoByCtx(ctx)
|
fi, err := getFullInfoByCtx(ctx)
|
||||||
if err != nil {
|
if err != nil || fi == nil {
|
||||||
abortWithError(w, err.Error())
|
log.Error("failed to fetch fi", "error", err)
|
||||||
|
http.Redirect(w, r, "/", 302)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if err := validateMove(fi, models.UserRoleGuesser); err != nil {
|
if err := validateMove(fi, models.UserRoleGuesser); err != nil {
|
||||||
@@ -275,8 +276,9 @@ func HandleMarkCard(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
func HandleActionHistory(w http.ResponseWriter, r *http.Request) {
|
func HandleActionHistory(w http.ResponseWriter, r *http.Request) {
|
||||||
fi, err := getFullInfoByCtx(r.Context())
|
fi, err := getFullInfoByCtx(r.Context())
|
||||||
if err != nil {
|
if err != nil || fi == nil {
|
||||||
abortWithError(w, err.Error())
|
log.Error("failed to fetch fi", "error", err)
|
||||||
|
http.Redirect(w, r, "/", 302)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
tmpl, err := template.ParseGlob("components/*.html")
|
tmpl, err := template.ParseGlob("components/*.html")
|
||||||
@@ -294,8 +296,9 @@ func HandleAddBot(w http.ResponseWriter, r *http.Request) {
|
|||||||
team := r.URL.Query().Get("team")
|
team := r.URL.Query().Get("team")
|
||||||
role := r.URL.Query().Get("role")
|
role := r.URL.Query().Get("role")
|
||||||
fi, err := getFullInfoByCtx(r.Context())
|
fi, err := getFullInfoByCtx(r.Context())
|
||||||
if err != nil {
|
if err != nil || fi == nil {
|
||||||
abortWithError(w, err.Error())
|
log.Error("failed to fetch fi", "error", err)
|
||||||
|
http.Redirect(w, r, "/", 302)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
var botname string
|
var botname string
|
||||||
@@ -320,8 +323,9 @@ func HandleRemoveBot(w http.ResponseWriter, r *http.Request) {
|
|||||||
botName := r.URL.Query().Get("bot")
|
botName := r.URL.Query().Get("bot")
|
||||||
log.Debug("got remove-bot request", "bot_name", botName)
|
log.Debug("got remove-bot request", "bot_name", botName)
|
||||||
fi, err := getFullInfoByCtx(r.Context())
|
fi, err := getFullInfoByCtx(r.Context())
|
||||||
if err != nil {
|
if err != nil || fi == nil {
|
||||||
abortWithError(w, err.Error())
|
log.Error("failed to fetch fi", "error", err)
|
||||||
|
http.Redirect(w, r, "/", 302)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if err := llmapi.RemoveBot(botName, fi.Room); err != nil {
|
if err := llmapi.RemoveBot(botName, fi.Room); err != nil {
|
||||||
@@ -330,3 +334,19 @@ func HandleRemoveBot(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
notify(models.NotifyRoomUpdatePrefix+fi.Room.ID, "")
|
notify(models.NotifyRoomUpdatePrefix+fi.Room.ID, "")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func HandleGetRoom(w http.ResponseWriter, r *http.Request) {
|
||||||
|
fi, err := getFullInfoByCtx(r.Context())
|
||||||
|
if err != nil {
|
||||||
|
abortWithError(w, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
tmpl, err := template.ParseGlob("components/*.html")
|
||||||
|
if err != nil {
|
||||||
|
abortWithError(w, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := tmpl.ExecuteTemplate(w, "room", fi); err != nil {
|
||||||
|
log.Error("failed to execute template", "error", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@@ -72,8 +72,13 @@ func HandleJoinTeam(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
// get username
|
// get username
|
||||||
fi, err := getFullInfoByCtx(r.Context())
|
fi, err := getFullInfoByCtx(r.Context())
|
||||||
if err != nil {
|
if err != nil || fi == nil {
|
||||||
abortWithError(w, err.Error())
|
log.Error("failed to fetch fi", "error", err)
|
||||||
|
http.Redirect(w, r, "/", 302)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if fi.Room == nil {
|
||||||
|
http.Redirect(w, r, "/", 302)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if fi.Room.IsRunning && role == "mime" {
|
if fi.Room.IsRunning && role == "mime" {
|
||||||
@@ -107,8 +112,9 @@ func HandleJoinTeam(w http.ResponseWriter, r *http.Request) {
|
|||||||
func HandleEndTurn(w http.ResponseWriter, r *http.Request) {
|
func HandleEndTurn(w http.ResponseWriter, r *http.Request) {
|
||||||
// get username
|
// get username
|
||||||
fi, err := getFullInfoByCtx(r.Context())
|
fi, err := getFullInfoByCtx(r.Context())
|
||||||
if err != nil {
|
if err != nil || fi == nil {
|
||||||
abortWithError(w, err.Error())
|
log.Error("failed to fetch fi", "error", err)
|
||||||
|
http.Redirect(w, r, "/", 302)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// check if one who pressed it is from the team who has the turn
|
// check if one who pressed it is from the team who has the turn
|
||||||
@@ -139,8 +145,9 @@ func HandleEndTurn(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
func HandleStartGame(w http.ResponseWriter, r *http.Request) {
|
func HandleStartGame(w http.ResponseWriter, r *http.Request) {
|
||||||
fi, err := getFullInfoByCtx(r.Context())
|
fi, err := getFullInfoByCtx(r.Context())
|
||||||
if err != nil {
|
if err != nil || fi == nil {
|
||||||
abortWithError(w, err.Error())
|
log.Error("failed to fetch fi", "error", err)
|
||||||
|
http.Redirect(w, r, "/", 302)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// check if enough players
|
// check if enough players
|
||||||
@@ -289,8 +296,9 @@ func HandleGiveClue(w http.ResponseWriter, r *http.Request) {
|
|||||||
clue := r.PostFormValue("clue")
|
clue := r.PostFormValue("clue")
|
||||||
num := r.PostFormValue("number")
|
num := r.PostFormValue("number")
|
||||||
fi, err := getFullInfoByCtx(r.Context())
|
fi, err := getFullInfoByCtx(r.Context())
|
||||||
if err != nil {
|
if err != nil || fi == nil {
|
||||||
abortWithError(w, err.Error())
|
log.Error("failed to fetch fi", "error", err)
|
||||||
|
http.Redirect(w, r, "/", 302)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
guessLimitU64, err := strconv.ParseUint(num, 10, 8)
|
guessLimitU64, err := strconv.ParseUint(num, 10, 8)
|
||||||
@@ -356,8 +364,9 @@ func HandleGiveClue(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
func HandleRenotifyBot(w http.ResponseWriter, r *http.Request) {
|
func HandleRenotifyBot(w http.ResponseWriter, r *http.Request) {
|
||||||
fi, err := getFullInfoByCtx(r.Context())
|
fi, err := getFullInfoByCtx(r.Context())
|
||||||
if err != nil {
|
if err != nil || fi == nil {
|
||||||
abortWithError(w, err.Error())
|
log.Error("failed to fetch fi", "error", err)
|
||||||
|
http.Redirect(w, r, "/", 302)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
notifyBotIfNeeded(fi.Room)
|
notifyBotIfNeeded(fi.Room)
|
||||||
|
@@ -75,8 +75,9 @@ func HandleExit(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
fi, err := getFullInfoByCtx(r.Context())
|
fi, err := getFullInfoByCtx(r.Context())
|
||||||
if err != nil {
|
if err != nil || fi == nil {
|
||||||
abortWithError(w, err.Error())
|
log.Error("failed to fetch fi", "error", err)
|
||||||
|
http.Redirect(w, r, "/", 302)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if fi.Room.IsRunning {
|
if fi.Room.IsRunning {
|
||||||
|
@@ -57,20 +57,20 @@ func updateStatsOnGameOver(ctx context.Context, room *models.Room) {
|
|||||||
if player.Role == models.UserRoleMime {
|
if player.Role == models.UserRoleMime {
|
||||||
stats.PlayedAsMime++
|
stats.PlayedAsMime++
|
||||||
if stats.PlayedAsMime > 0 {
|
if stats.PlayedAsMime > 0 {
|
||||||
gamesWonAsMime := stats.MimeWinrate * float64(stats.PlayedAsMime-1)
|
gamesWonAsMime := stats.MimeWinrate * float32(stats.PlayedAsMime-1)
|
||||||
if player.Team == room.TeamWon {
|
if player.Team == room.TeamWon {
|
||||||
gamesWonAsMime++
|
gamesWonAsMime++
|
||||||
}
|
}
|
||||||
stats.MimeWinrate = gamesWonAsMime / float64(stats.PlayedAsMime)
|
stats.MimeWinrate = gamesWonAsMime / float32(stats.PlayedAsMime)
|
||||||
}
|
}
|
||||||
} else if player.Role == models.UserRoleGuesser {
|
} else if player.Role == models.UserRoleGuesser {
|
||||||
stats.PlayedAsGuesser++
|
stats.PlayedAsGuesser++
|
||||||
if stats.PlayedAsGuesser > 0 {
|
if stats.PlayedAsGuesser > 0 {
|
||||||
gamesWonAsGuesser := stats.GuesserWinrate * float64(stats.PlayedAsGuesser-1)
|
gamesWonAsGuesser := stats.GuesserWinrate * float32(stats.PlayedAsGuesser-1)
|
||||||
if player.Team == room.TeamWon {
|
if player.Team == room.TeamWon {
|
||||||
gamesWonAsGuesser++
|
gamesWonAsGuesser++
|
||||||
}
|
}
|
||||||
stats.GuesserWinrate = gamesWonAsGuesser / float64(stats.PlayedAsGuesser)
|
stats.GuesserWinrate = gamesWonAsGuesser / float32(stats.PlayedAsGuesser)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if err := repo.UpdatePlayerStats(ctx, stats); err != nil {
|
if err := repo.UpdatePlayerStats(ctx, stats); err != nil {
|
||||||
|
@@ -25,10 +25,10 @@ var (
|
|||||||
DoneChanMap = make(map[string]chan bool)
|
DoneChanMap = make(map[string]chan bool)
|
||||||
mapMutex = &sync.RWMutex{}
|
mapMutex = &sync.RWMutex{}
|
||||||
// got prompt: control character (\\u0000-\\u001F) found while parsing a string at line 4 column 0
|
// got prompt: control character (\\u0000-\\u001F) found while parsing a string at line 4 column 0
|
||||||
// MimePrompt = `we are playing alias;\nyou are a mime (player who gives a clue of one noun word and number of cards you expect them to open) of the %s team (people who would guess by your clue want open the %s cards);\nplease return your clue, number of cards to open and what words you mean them to find using that clue in json like:\n{\n\"clue\": \"one-word-noun\",\n\"number\": \"number-from-0-to-9\",\n\"words_I_mean_my_team_to_open\": [\"this\", \"that\", ...]\n}\nthe team who openes all their cards first wins.\nplease return json only.\nunopen Blue cards left: %d;\nunopen Red cards left: %d;\nhere is the game info in json:\n%s`
|
|
||||||
// GuesserPrompt = `we are playing alias;\nyou are to guess words of the %s team (you want open %s cards) by given clue and a number of meant guesses;\nplease return your guesses and words that could be meant by the clue, but you do not wish to open yet, in json like:\n{\n\"guesses\": [\"word1\", \"word2\", ...],\n\"could_be\": [\"this\", \"that\", ...]\n}\nthe team who openes all their cards first wins.\nplease return json only.\nunopen Blue cards left: %d;\nunopen Red cards left: %d;\nhere is the cards (and other info), you need to choose revealed==false words:\n%s`
|
|
||||||
GuesserSimplePrompt = `we are playing game of alias;\n you were given a clue: \"%s\";\nplease return your guess and words that could be meant by the clue, but you do not wish to open yet, in json like:\n{\n\"guess\": \"most_relevant_word_to_the_clue\",\n\"could_be\": [\"this\", \"that\", ...]\n}\nhere is the words that you can choose from:\n%v`
|
GuesserSimplePrompt = `we are playing game of alias;\n you were given a clue: \"%s\";\nplease return your guess and words that could be meant by the clue, but you do not wish to open yet, in json like:\n{\n\"guess\": \"most_relevant_word_to_the_clue\",\n\"could_be\": [\"this\", \"that\", ...]\n}\nhere is the words that you can choose from:\n%v`
|
||||||
MimeSimplePrompt = `we are playing alias;\nyou are to give one word clue and a number of words you mean your team to open; your team words: %v;\nhere are the words of opposite team you want to avoid: %v;\nand here is a black word that is critical not to pick: %s;\nplease return your clue, number of cards to open and what words you mean them to find using that clue in json like:\n{\n\"clue\": \"one-word-noun\",\n\"number\": \"number-from-0-to-9-as-string\",\n\"words_I_mean_my_team_to_open\": [\"this\", \"that\", ...]\n}\nplease return json only.\nunopen Blue cards left: %d;\nunopen Red cards left: %d;`
|
MimeSimplePrompt = `we are playing alias;\nyou are to give one word clue and a number of words you mean your team to open; your team words: %v;\nhere are the words of opposite team you want to avoid: %v;\nand here is a black word that is critical not to pick: %s;\nplease return your clue, number of cards to open and what words you mean them to find using that clue in json like:\n{\n\"clue\": \"one-word-noun\",\n\"number\": \"number-from-0-to-9-as-string\",\n\"words_I_mean_my_team_to_open\": [\"this\", \"that\", ...]\n}\nplease return json only.`
|
||||||
|
GuesserSimplePromptRU = `мы играем в alias;\n тебе дана подсказка (clue): \"%s\";\nпожалуйста, верни свою догадку (guess), а также слова, что тоже подходят к подсказке, но ты меньше в них уверен, в формате json; пример:\n{\n\"guess\": \"отгадка\",\n\"could_be\": [\"слово1\", \"слово2\", ...]\n}\nвот список слов из которых нужно выбрать:\n%v`
|
||||||
|
MimeSimplePromptRU = `мы играем в alias;\nтебе нужно дать подсказку одним словом и число слов, что ты подразумевал этой подсказкой; слова твоей комманды: %v;\nслова противоположной комманды, что ты хочешь избежать: %v;\nи вот ЧЕРНОЕ СЛОВО, открыв которое твоя комманда проиграет игру: %s;\nпожалуйста, верни подсказку (одним словом) и количество слов, что ты подразумеваешь в формате json; пример:\n{\n\"clue\": \"подсказка\",\n\"number\": \"число-от-0-до-9-as-string\",\n\"words_I_mean_my_team_to_open\": [\"слово1\", \"слово2\", ...]\n}\nпожалуйста верни только json.`
|
||||||
)
|
)
|
||||||
|
|
||||||
func convertToSliceOfStrings(value any) ([]string, error) {
|
func convertToSliceOfStrings(value any) ([]string, error) {
|
||||||
@@ -188,6 +188,11 @@ func (b *Bot) BotMove() {
|
|||||||
b.log.Error("bot loop", "error", err)
|
b.log.Error("bot loop", "error", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if room.BotFailed {
|
||||||
|
if err := repo.RoomUnSetBotFailed(context.Background(), room.ID); err != nil {
|
||||||
|
b.log.Error("failed to unset bot failed bool", "error", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
// eventName := models.NotifyBacklogPrefix + room.ID
|
// eventName := models.NotifyBacklogPrefix + room.ID
|
||||||
eventName := models.NotifyRoomUpdatePrefix + room.ID
|
eventName := models.NotifyRoomUpdatePrefix + room.ID
|
||||||
eventPayload := ""
|
eventPayload := ""
|
||||||
@@ -222,21 +227,30 @@ func (b *Bot) BotMove() {
|
|||||||
// call llm
|
// call llm
|
||||||
llmResp, err := b.CallLLM(prompt)
|
llmResp, err := b.CallLLM(prompt)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
room.LogJournal = append(room.LogJournal, models.Journal{
|
lj := models.Journal{
|
||||||
Entry: "send call got error: " + err.Error(),
|
Entry: fmt.Sprintf("bot '%s' exceeded attempts to call llm;", b.BotName),
|
||||||
Username: b.BotName,
|
Username: b.BotName,
|
||||||
RoomID: room.ID,
|
RoomID: b.RoomID,
|
||||||
})
|
}
|
||||||
|
if err := repo.JournalCreate(context.Background(), &lj); err != nil {
|
||||||
|
b.log.Warn("failed to write to journal", "entry", lj)
|
||||||
|
}
|
||||||
b.log.Error("bot loop", "error", err)
|
b.log.Error("bot loop", "error", err)
|
||||||
|
if err := repo.RoomSetBotFailed(context.Background(), room.ID); err != nil {
|
||||||
|
b.log.Error("failed to set bot failed bool", "error", err)
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
tempMap, err := b.LLMParser.ParseBytes(llmResp)
|
tempMap, err := b.LLMParser.ParseBytes(llmResp)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
room.LogJournal = append(room.LogJournal, models.Journal{
|
lj := models.Journal{
|
||||||
Entry: "parse resp got error: " + err.Error(),
|
Entry: fmt.Sprintf("bot '%s' parsing resp failed;", b.BotName),
|
||||||
Username: b.BotName,
|
Username: b.BotName,
|
||||||
RoomID: room.ID,
|
RoomID: b.RoomID,
|
||||||
})
|
}
|
||||||
|
if err := repo.JournalCreate(context.Background(), &lj); err != nil {
|
||||||
|
b.log.Warn("failed to write to journal", "entry", lj)
|
||||||
|
}
|
||||||
b.log.Error("bot loop", "error", err, "resp", string(llmResp))
|
b.log.Error("bot loop", "error", err, "resp", string(llmResp))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -245,6 +259,22 @@ func (b *Bot) BotMove() {
|
|||||||
mimeResp := MimeResp{}
|
mimeResp := MimeResp{}
|
||||||
b.log.Info("mime resp log", "mimeResp", tempMap)
|
b.log.Info("mime resp log", "mimeResp", tempMap)
|
||||||
mimeResp.Clue = strings.ToLower(tempMap["clue"].(string))
|
mimeResp.Clue = strings.ToLower(tempMap["clue"].(string))
|
||||||
|
for _, card := range room.Cards {
|
||||||
|
if strings.ToLower(card.Word) == mimeResp.Clue {
|
||||||
|
b.log.Warn("bot-mime clue is one of the words on the board; retrying", "clue", mimeResp.Clue, "bot", b.BotName)
|
||||||
|
entry := fmt.Sprintf("bot-mime '%s' gave a clue '%s' which is one of the words on the board. retrying.", b.BotName, mimeResp.Clue)
|
||||||
|
lj := models.Journal{
|
||||||
|
Entry: entry,
|
||||||
|
Username: b.BotName,
|
||||||
|
RoomID: room.ID,
|
||||||
|
}
|
||||||
|
room.LogJournal = append(room.LogJournal, lj)
|
||||||
|
if err := repo.JournalCreate(context.Background(), &lj); err != nil {
|
||||||
|
b.log.Warn("failed to write to journal", "entry", lj)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
var ok bool
|
var ok bool
|
||||||
mimeResp.Number, ok = tempMap["number"].(string)
|
mimeResp.Number, ok = tempMap["number"].(string)
|
||||||
if !ok {
|
if !ok {
|
||||||
@@ -523,6 +553,9 @@ func (b *Bot) BuildSimpleGuesserPrompt(room *models.Room) string {
|
|||||||
}
|
}
|
||||||
words[i] = card.Word
|
words[i] = card.Word
|
||||||
}
|
}
|
||||||
|
if strings.EqualFold(room.Settings.Language, "ru") {
|
||||||
|
return fmt.Sprintf(MimeSimplePromptRU, clueAction.Word, words)
|
||||||
|
}
|
||||||
return fmt.Sprintf(GuesserSimplePrompt, clueAction.Word, words)
|
return fmt.Sprintf(GuesserSimplePrompt, clueAction.Word, words)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -551,34 +584,20 @@ func (b *Bot) BuildSimpleMimePrompt(room *models.Room) string {
|
|||||||
theirwords = append(theirwords, card.Word)
|
theirwords = append(theirwords, card.Word)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return fmt.Sprintf(MimeSimplePrompt, ourwords, theirwords, blackWord, room.BlueCounter, room.RedCounter)
|
if strings.EqualFold(room.Settings.Language, "ru") {
|
||||||
|
return fmt.Sprintf(MimeSimplePromptRU, ourwords, theirwords, blackWord)
|
||||||
|
}
|
||||||
|
return fmt.Sprintf(MimeSimplePrompt, ourwords, theirwords, blackWord)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Bot) BuildPrompt(room *models.Room) string {
|
func (b *Bot) BuildPrompt(room *models.Room) string {
|
||||||
if b.Role == "" {
|
if b.Role == "" {
|
||||||
return ""
|
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
|
|
||||||
// }
|
|
||||||
// data, err := json.Marshal(toText)
|
|
||||||
// if err != nil {
|
|
||||||
// b.log.Error("failed to marshal", "error", err)
|
|
||||||
// return ""
|
|
||||||
// }
|
|
||||||
// Escape the JSON string for inclusion in another JSON field
|
|
||||||
// escapedData := strings.ReplaceAll(string(data), `"`, `\\"`)
|
|
||||||
if b.Role == models.UserRoleMime {
|
if b.Role == models.UserRoleMime {
|
||||||
// return fmt.Sprintf(MimeSimplePrompt, b.Team, b.Team, room.BlueCounter, room.RedCounter, escapedData)
|
|
||||||
// return fmt.Sprintf(MimePrompt, b.Team, b.Team, room.BlueCounter, room.RedCounter, escapedData)
|
|
||||||
return b.BuildSimpleMimePrompt(room)
|
return b.BuildSimpleMimePrompt(room)
|
||||||
}
|
}
|
||||||
if b.Role == models.UserRoleGuesser {
|
if b.Role == models.UserRoleGuesser {
|
||||||
// return fmt.Sprintf(GuesserPrompt, b.Team, b.Team, room.BlueCounter, room.RedCounter, escapedData)
|
|
||||||
return b.BuildSimpleGuesserPrompt(room)
|
return b.BuildSimpleGuesserPrompt(room)
|
||||||
}
|
}
|
||||||
return ""
|
return ""
|
||||||
@@ -596,7 +615,7 @@ func (b *Bot) CallLLM(prompt string) ([]byte, error) {
|
|||||||
req, err := http.NewRequest(method, b.cfg.LLMConfig.URL, payloadReader)
|
req, err := http.NewRequest(method, b.cfg.LLMConfig.URL, payloadReader)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if attempt == maxRetries-1 {
|
if attempt == maxRetries-1 {
|
||||||
return nil, fmt.Errorf("failed to create request: %w", err)
|
return nil, fmt.Errorf("LLM call failed after %d retries on request creation: %w", maxRetries, err)
|
||||||
}
|
}
|
||||||
b.log.Error("failed to make new request; will retry", "error", err, "url", b.cfg.LLMConfig.URL, "attempt", attempt)
|
b.log.Error("failed to make new request; will retry", "error", err, "url", b.cfg.LLMConfig.URL, "attempt", attempt)
|
||||||
time.Sleep(time.Duration(baseDelay) * time.Second * time.Duration(attempt+1))
|
time.Sleep(time.Duration(baseDelay) * time.Second * time.Duration(attempt+1))
|
||||||
@@ -608,7 +627,7 @@ func (b *Bot) CallLLM(prompt string) ([]byte, error) {
|
|||||||
resp, err := client.Do(req)
|
resp, err := client.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if attempt == maxRetries-1 {
|
if attempt == maxRetries-1 {
|
||||||
return nil, fmt.Errorf("http request failed: %w", err)
|
return nil, fmt.Errorf("LLM call failed after %d retries on client.Do: %w", maxRetries, err)
|
||||||
}
|
}
|
||||||
b.log.Error("http request failed; will retry", "error", err, "url", b.cfg.LLMConfig.URL, "attempt", attempt)
|
b.log.Error("http request failed; will retry", "error", err, "url", b.cfg.LLMConfig.URL, "attempt", attempt)
|
||||||
delay := time.Duration(baseDelay*(attempt+1)) * time.Second
|
delay := time.Duration(baseDelay*(attempt+1)) * time.Second
|
||||||
@@ -619,7 +638,7 @@ func (b *Bot) CallLLM(prompt string) ([]byte, error) {
|
|||||||
resp.Body.Close()
|
resp.Body.Close()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if attempt == maxRetries-1 {
|
if attempt == maxRetries-1 {
|
||||||
return nil, fmt.Errorf("failed to read response body: %w", err)
|
return nil, fmt.Errorf("LLM call failed after %d retries on reading body: %w", maxRetries, err)
|
||||||
}
|
}
|
||||||
b.log.Error("failed to read response body; will retry", "error", err, "url", b.cfg.LLMConfig.URL, "attempt", attempt)
|
b.log.Error("failed to read response body; will retry", "error", err, "url", b.cfg.LLMConfig.URL, "attempt", attempt)
|
||||||
delay := time.Duration(baseDelay*(attempt+1)) * time.Second
|
delay := time.Duration(baseDelay*(attempt+1)) * time.Second
|
||||||
@@ -629,7 +648,7 @@ func (b *Bot) CallLLM(prompt string) ([]byte, error) {
|
|||||||
// Check status code
|
// Check status code
|
||||||
if resp.StatusCode >= 400 && resp.StatusCode < 600 {
|
if resp.StatusCode >= 400 && resp.StatusCode < 600 {
|
||||||
if attempt == maxRetries-1 {
|
if attempt == maxRetries-1 {
|
||||||
return nil, fmt.Errorf("after %d retries, still got status %d", maxRetries, resp.StatusCode)
|
return nil, fmt.Errorf("LLM call failed after %d retries, got status %d", maxRetries, resp.StatusCode)
|
||||||
}
|
}
|
||||||
b.log.Warn("retriable status code; will retry", "code", resp.StatusCode, "attempt", attempt)
|
b.log.Warn("retriable status code; will retry", "code", resp.StatusCode, "attempt", attempt)
|
||||||
delay := time.Duration((baseDelay * (1 << attempt))) * time.Second
|
delay := time.Duration((baseDelay * (1 << attempt))) * time.Second
|
||||||
@@ -644,7 +663,5 @@ func (b *Bot) CallLLM(prompt string) ([]byte, error) {
|
|||||||
b.log.Debug("llm resp", "body", string(body), "url", b.cfg.LLMConfig.URL, "attempt", attempt)
|
b.log.Debug("llm resp", "body", string(body), "url", b.cfg.LLMConfig.URL, "attempt", attempt)
|
||||||
return body, nil
|
return body, nil
|
||||||
}
|
}
|
||||||
// This line should not be reached because each error path returns in the loop.
|
|
||||||
return nil, errors.New("unknown error in retry loop")
|
return nil, errors.New("unknown error in retry loop")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -46,20 +46,20 @@ func updateStatsOnGameOver(ctx context.Context, room *models.Room) {
|
|||||||
if player.Role == models.UserRoleMime {
|
if player.Role == models.UserRoleMime {
|
||||||
stats.PlayedAsMime++
|
stats.PlayedAsMime++
|
||||||
if stats.PlayedAsMime > 0 {
|
if stats.PlayedAsMime > 0 {
|
||||||
gamesWonAsMime := stats.MimeWinrate * float64(stats.PlayedAsMime-1)
|
gamesWonAsMime := stats.MimeWinrate * float32(stats.PlayedAsMime-1)
|
||||||
if player.Team == room.TeamWon {
|
if player.Team == room.TeamWon {
|
||||||
gamesWonAsMime++
|
gamesWonAsMime++
|
||||||
}
|
}
|
||||||
stats.MimeWinrate = gamesWonAsMime / float64(stats.PlayedAsMime)
|
stats.MimeWinrate = gamesWonAsMime / float32(stats.PlayedAsMime)
|
||||||
}
|
}
|
||||||
} else if player.Role == models.UserRoleGuesser {
|
} else if player.Role == models.UserRoleGuesser {
|
||||||
stats.PlayedAsGuesser++
|
stats.PlayedAsGuesser++
|
||||||
if stats.PlayedAsGuesser > 0 {
|
if stats.PlayedAsGuesser > 0 {
|
||||||
gamesWonAsGuesser := stats.GuesserWinrate * float64(stats.PlayedAsGuesser-1)
|
gamesWonAsGuesser := stats.GuesserWinrate * float32(stats.PlayedAsGuesser-1)
|
||||||
if player.Team == room.TeamWon {
|
if player.Team == room.TeamWon {
|
||||||
gamesWonAsGuesser++
|
gamesWonAsGuesser++
|
||||||
}
|
}
|
||||||
stats.GuesserWinrate = gamesWonAsGuesser / float64(stats.PlayedAsGuesser)
|
stats.GuesserWinrate = gamesWonAsGuesser / float32(stats.PlayedAsGuesser)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
31
main.go
31
main.go
@@ -6,8 +6,10 @@ import (
|
|||||||
"gralias/crons"
|
"gralias/crons"
|
||||||
"gralias/handlers"
|
"gralias/handlers"
|
||||||
"gralias/repos"
|
"gralias/repos"
|
||||||
|
"gralias/telemetry"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
_ "net/http/pprof"
|
||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
@@ -56,8 +58,11 @@ func getContentType(path string) string {
|
|||||||
|
|
||||||
func ListenToRequests(port string) *http.Server {
|
func ListenToRequests(port string) *http.Server {
|
||||||
mux := http.NewServeMux()
|
mux := http.NewServeMux()
|
||||||
|
var handler http.Handler = mux
|
||||||
|
handler = handlers.LogRequests(handlers.GetSession(handler))
|
||||||
|
handler = telemetry.OtelMiddleware(handler)
|
||||||
server := &http.Server{
|
server := &http.Server{
|
||||||
Handler: handlers.LogRequests(handlers.GetSession(mux)),
|
Handler: handler,
|
||||||
Addr: ":" + port,
|
Addr: ":" + port,
|
||||||
// ReadTimeout: time.Second * 5, // does this timeout conflict with sse connection?
|
// ReadTimeout: time.Second * 5, // does this timeout conflict with sse connection?
|
||||||
WriteTimeout: 0, // sse streaming
|
WriteTimeout: 0, // sse streaming
|
||||||
@@ -87,6 +92,7 @@ func ListenToRequests(port string) *http.Server {
|
|||||||
mux.HandleFunc("GET /add-bot", handlers.HandleAddBot)
|
mux.HandleFunc("GET /add-bot", handlers.HandleAddBot)
|
||||||
mux.HandleFunc("GET /remove-bot", handlers.HandleRemoveBot)
|
mux.HandleFunc("GET /remove-bot", handlers.HandleRemoveBot)
|
||||||
mux.HandleFunc("GET /mark-card", handlers.HandleMarkCard)
|
mux.HandleFunc("GET /mark-card", handlers.HandleMarkCard)
|
||||||
|
mux.HandleFunc("GET /room", handlers.HandleGetRoom)
|
||||||
// special
|
// special
|
||||||
mux.HandleFunc("GET /renotify-bot", handlers.HandleRenotifyBot)
|
mux.HandleFunc("GET /renotify-bot", handlers.HandleRenotifyBot)
|
||||||
// sse
|
// sse
|
||||||
@@ -96,6 +102,8 @@ func ListenToRequests(port string) *http.Server {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
shutdown := telemetry.InitTracer()
|
||||||
|
defer shutdown()
|
||||||
// Setup graceful shutdown
|
// Setup graceful shutdown
|
||||||
stop := make(chan os.Signal, 1)
|
stop := make(chan os.Signal, 1)
|
||||||
signal.Notify(stop, os.Interrupt, syscall.SIGTERM)
|
signal.Notify(stop, os.Interrupt, syscall.SIGTERM)
|
||||||
@@ -105,16 +113,33 @@ func main() {
|
|||||||
cm := crons.NewCronManager(repo, slog.Default())
|
cm := crons.NewCronManager(repo, slog.Default())
|
||||||
cm.Start()
|
cm.Start()
|
||||||
server := ListenToRequests(cfg.ServerConfig.Port)
|
server := ListenToRequests(cfg.ServerConfig.Port)
|
||||||
|
pprofPort := "6060"
|
||||||
|
pprofServer := &http.Server{
|
||||||
|
Addr: ":" + pprofPort,
|
||||||
|
}
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
slog.Info("Pprof server listening", "addr", pprofPort)
|
||||||
|
if err := pprofServer.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
||||||
|
slog.Error("Pprof server failed", "error", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
<-stop
|
<-stop
|
||||||
slog.Info("Shutting down server...")
|
slog.Info("Shutting down servers...")
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
|
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
if err := server.Shutdown(ctx); err != nil {
|
if err := server.Shutdown(ctx); err != nil {
|
||||||
slog.Error("server shutdown failed", "error", err)
|
slog.Error("Main server shutdown failed", "error", err)
|
||||||
|
}
|
||||||
|
if err := pprofServer.Shutdown(ctx); err != nil {
|
||||||
|
slog.Error("Pprof server shutdown failed", "error", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -13,6 +13,7 @@ CREATE TABLE rooms (
|
|||||||
mime_done BOOLEAN NOT NULL DEFAULT FALSE,
|
mime_done BOOLEAN NOT NULL DEFAULT FALSE,
|
||||||
is_running BOOLEAN NOT NULL DEFAULT FALSE,
|
is_running BOOLEAN NOT NULL DEFAULT FALSE,
|
||||||
is_over BOOLEAN NOT NULL DEFAULT FALSE,
|
is_over BOOLEAN NOT NULL DEFAULT FALSE,
|
||||||
|
bot_failed BOOLEAN NOT NULL DEFAULT FALSE,
|
||||||
team_won TEXT NOT NULL DEFAULT '',
|
team_won TEXT NOT NULL DEFAULT '',
|
||||||
room_link TEXT NOT NULL DEFAULT ''
|
room_link TEXT NOT NULL DEFAULT ''
|
||||||
);
|
);
|
||||||
|
3
migrations/002_add_stats_elo.down.sql
Normal file
3
migrations/002_add_stats_elo.down.sql
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
DROP TRIGGER IF EXISTS update_player_rating;
|
||||||
|
|
||||||
|
ALTER TABLE DROP COLUMN rating;
|
20
migrations/002_add_stats_elo.up.sql
Normal file
20
migrations/002_add_stats_elo.up.sql
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
ALTER TABLE player_stats
|
||||||
|
ADD COLUMN rating REAL NOT NULL DEFAULT 1000.0;
|
||||||
|
|
||||||
|
CREATE TRIGGER update_player_rating
|
||||||
|
AFTER UPDATE OF games_played, games_won ON player_stats
|
||||||
|
WHEN NEW.games_played = OLD.games_played + 1
|
||||||
|
BEGIN
|
||||||
|
UPDATE player_stats
|
||||||
|
SET rating = OLD.rating +
|
||||||
|
32.0 * (
|
||||||
|
CASE
|
||||||
|
WHEN NEW.games_won = OLD.games_won + 1
|
||||||
|
THEN 1.0 - 0.5 -- Win term: 0.5
|
||||||
|
ELSE 0.0 - 0.5 -- Loss term: -0.5
|
||||||
|
END
|
||||||
|
) +
|
||||||
|
0.05 * (1000.0 - OLD.rating)
|
||||||
|
WHERE id = OLD.id;
|
||||||
|
END;
|
||||||
|
|
@@ -148,10 +148,11 @@ type PlayerStats struct {
|
|||||||
OpenedOppositeWords int `db:"opened_opposite_words"`
|
OpenedOppositeWords int `db:"opened_opposite_words"`
|
||||||
OpenedWhiteWords int `db:"opened_white_words"`
|
OpenedWhiteWords int `db:"opened_white_words"`
|
||||||
OpenedBlackWords int `db:"opened_black_words"`
|
OpenedBlackWords int `db:"opened_black_words"`
|
||||||
MimeWinrate float64 `db:"mime_winrate"`
|
MimeWinrate float32 `db:"mime_winrate"`
|
||||||
GuesserWinrate float64 `db:"guesser_winrate"`
|
GuesserWinrate float32 `db:"guesser_winrate"`
|
||||||
PlayedAsMime int `db:"played_as_mime"`
|
PlayedAsMime int `db:"played_as_mime"`
|
||||||
PlayedAsGuesser int `db:"played_as_guesser"`
|
PlayedAsGuesser int `db:"played_as_guesser"`
|
||||||
|
Rating float32 `db:"rating"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Room struct {
|
type Room struct {
|
||||||
@@ -177,6 +178,8 @@ type Room struct {
|
|||||||
BotMap map[string]BotPlayer `db:"-"`
|
BotMap map[string]BotPlayer `db:"-"`
|
||||||
LogJournal []Journal `db:"-"`
|
LogJournal []Journal `db:"-"`
|
||||||
Settings GameSettings `db:"-"`
|
Settings GameSettings `db:"-"`
|
||||||
|
//
|
||||||
|
BotFailed bool `db:"bot_failed"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Room) FindColor(word string) (WordColor, bool) {
|
func (r *Room) FindColor(word string) (WordColor, bool) {
|
||||||
|
@@ -39,6 +39,6 @@ func (r *RepoProvider) CardMarksByRoomID(ctx context.Context, roomID string) ([]
|
|||||||
}
|
}
|
||||||
func (r *RepoProvider) CardMarksRemoveByRoomID(ctx context.Context, roomID string) error {
|
func (r *RepoProvider) CardMarksRemoveByRoomID(ctx context.Context, roomID string) error {
|
||||||
db := getDB(ctx, r.DB)
|
db := getDB(ctx, r.DB)
|
||||||
_, err := db.ExecContext(ctx, "DELETE FROM card_marks WHERE room_id = ?;", roomID)
|
_, err := db.ExecContext(ctx, "DELETE FROM card_marks WHERE card_id IN (select id from word_cards where room_id = ?);", roomID)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@@ -37,7 +37,8 @@ func (p *RepoProvider) UpdatePlayerStats(ctx context.Context, stats *models.Play
|
|||||||
mime_winrate = :mime_winrate,
|
mime_winrate = :mime_winrate,
|
||||||
guesser_winrate = :guesser_winrate,
|
guesser_winrate = :guesser_winrate,
|
||||||
played_as_mime = :played_as_mime,
|
played_as_mime = :played_as_mime,
|
||||||
played_as_guesser = :played_as_guesser
|
played_as_guesser = :played_as_guesser,
|
||||||
|
rating = :rating
|
||||||
WHERE username = :username`, stats)
|
WHERE username = :username`, stats)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@@ -14,18 +14,139 @@ func setupPlayersTestDB(t *testing.T) (*sqlx.DB, func()) {
|
|||||||
db, err := sqlx.Connect("sqlite3", ":memory:")
|
db, err := sqlx.Connect("sqlite3", ":memory:")
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
schema := `
|
// Load schema from migration files
|
||||||
CREATE TABLE IF NOT EXISTS players (
|
schema001 := `
|
||||||
|
-- migrations/001_initial_schema.up.sql
|
||||||
|
|
||||||
|
CREATE TABLE rooms (
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
creator_name TEXT NOT NULL,
|
||||||
|
team_turn TEXT NOT NULL DEFAULT '',
|
||||||
|
this_turn_limit INTEGER NOT NULL DEFAULT 0,
|
||||||
|
opened_this_turn INTEGER NOT NULL DEFAULT 0,
|
||||||
|
blue_counter INTEGER NOT NULL DEFAULT 0,
|
||||||
|
red_counter INTEGER NOT NULL DEFAULT 0,
|
||||||
|
red_turn BOOLEAN NOT NULL DEFAULT FALSE,
|
||||||
|
mime_done BOOLEAN NOT NULL DEFAULT FALSE,
|
||||||
|
is_running BOOLEAN NOT NULL DEFAULT FALSE,
|
||||||
|
is_over BOOLEAN NOT NULL DEFAULT FALSE,
|
||||||
|
team_won TEXT NOT NULL DEFAULT '',
|
||||||
|
room_link TEXT NOT NULL DEFAULT ''
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE players (
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
room_id TEXT,
|
room_id TEXT, -- nullable
|
||||||
username TEXT,
|
username TEXT NOT NULL UNIQUE,
|
||||||
password TEXT NOT NULL DEFAULT '',
|
password TEXT NOT NULL DEFAULT '',
|
||||||
team TEXT,
|
team TEXT NOT NULL DEFAULT '', -- 'red' or 'blue'
|
||||||
role TEXT,
|
role TEXT NOT NULL DEFAULT '', -- 'guesser' or 'mime'
|
||||||
is_bot BOOLEAN
|
is_bot BOOLEAN NOT NULL DEFAULT FALSE,
|
||||||
);
|
FOREIGN KEY (room_id) REFERENCES rooms(id) ON DELETE CASCADE
|
||||||
`
|
);
|
||||||
_, err = db.Exec(schema)
|
|
||||||
|
CREATE TABLE word_cards (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
room_id TEXT NOT NULL,
|
||||||
|
word TEXT NOT NULL,
|
||||||
|
color TEXT NOT NULL DEFAULT '',
|
||||||
|
revealed BOOLEAN NOT NULL DEFAULT FALSE,
|
||||||
|
mime_view BOOLEAN NOT NULL DEFAULT FALSE,
|
||||||
|
FOREIGN KEY (room_id) REFERENCES rooms(id) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE card_marks (
|
||||||
|
card_id INTEGER NOT NULL,
|
||||||
|
username TEXT NOT NULL,
|
||||||
|
FOREIGN KEY (card_id) REFERENCES word_cards(id) ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY (username) REFERENCES players(username) ON DELETE CASCADE,
|
||||||
|
PRIMARY KEY (card_id, username)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE actions (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
room_id TEXT NOT NULL,
|
||||||
|
actor TEXT NOT NULL,
|
||||||
|
actor_color TEXT NOT NULL DEFAULT '',
|
||||||
|
action_type TEXT NOT NULL,
|
||||||
|
word TEXT NOT NULL DEFAULT '',
|
||||||
|
word_color TEXT NOT NULL DEFAULT '',
|
||||||
|
number_associated TEXT NOT NULL DEFAULT '', -- for clues
|
||||||
|
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
FOREIGN KEY (room_id) REFERENCES rooms(id) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE settings (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
room_id TEXT NOT NULL,
|
||||||
|
language TEXT NOT NULL DEFAULT 'en',
|
||||||
|
room_pass TEXT NOT NULL DEFAULT '',
|
||||||
|
turn_time INTEGER NOT NULL DEFAULT 60, -- seconds
|
||||||
|
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
FOREIGN KEY (room_id) REFERENCES rooms(id) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE sessions(
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
lifetime INTEGER NOT NULL DEFAULT 3600,
|
||||||
|
token_key TEXT NOT NULL DEFAULT '' UNIQUE, -- encoded value
|
||||||
|
username TEXT NOT NULL,
|
||||||
|
FOREIGN KEY (username) REFERENCES players(username) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE journal(
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
entry TEXT NOT NULL DEFAULT '',
|
||||||
|
username TEXT NOT NULL,
|
||||||
|
room_id TEXT NOT NULL,
|
||||||
|
FOREIGN KEY (username) REFERENCES players(username) ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY (room_id) REFERENCES rooms(id) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE player_stats (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
username TEXT NOT NULL UNIQUE,
|
||||||
|
games_played INTEGER NOT NULL DEFAULT 0,
|
||||||
|
games_won INTEGER NOT NULL DEFAULT 0,
|
||||||
|
games_lost INTEGER NOT NULL DEFAULT 0,
|
||||||
|
opened_opposite_words INTEGER NOT NULL DEFAULT 0,
|
||||||
|
opened_white_words INTEGER NOT NULL DEFAULT 0,
|
||||||
|
opened_black_words INTEGER NOT NULL DEFAULT 0,
|
||||||
|
mime_winrate REAL NOT NULL DEFAULT 0.0,
|
||||||
|
guesser_winrate REAL NOT NULL DEFAULT 0.0,
|
||||||
|
played_as_mime INTEGER NOT NULL DEFAULT 0,
|
||||||
|
played_as_guesser INTEGER NOT NULL DEFAULT 0,
|
||||||
|
FOREIGN KEY (username) REFERENCES players(username) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
`
|
||||||
|
_, err = db.Exec(schema001)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
schema002 := `
|
||||||
|
ALTER TABLE player_stats
|
||||||
|
ADD COLUMN rating REAL NOT NULL DEFAULT 1000.0;
|
||||||
|
|
||||||
|
CREATE TRIGGER update_player_rating
|
||||||
|
AFTER UPDATE OF games_played, games_won ON player_stats
|
||||||
|
WHEN NEW.games_played = OLD.games_played + 1
|
||||||
|
BEGIN
|
||||||
|
UPDATE player_stats
|
||||||
|
SET rating = OLD.rating +
|
||||||
|
32.0 * (
|
||||||
|
CASE
|
||||||
|
WHEN NEW.games_won = OLD.games_won + 1
|
||||||
|
THEN 1.0 - 0.5 -- Win term: 0.5
|
||||||
|
ELSE 0.0 - 0.5 -- Loss term: -0.5
|
||||||
|
END
|
||||||
|
) +
|
||||||
|
0.05 * (1000.0 - OLD.rating)
|
||||||
|
WHERE id = OLD.id;
|
||||||
|
END;
|
||||||
|
`
|
||||||
|
_, err = db.Exec(schema002)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
return db, func() {
|
return db, func() {
|
||||||
@@ -33,6 +154,39 @@ func setupPlayersTestDB(t *testing.T) (*sqlx.DB, func()) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestPlayerStatsRatingUpdate(t *testing.T) {
|
||||||
|
db, teardown := setupPlayersTestDB(t)
|
||||||
|
defer teardown()
|
||||||
|
|
||||||
|
username := "test_player_rating"
|
||||||
|
_, err := db.Exec(`INSERT INTO players (username) VALUES (?)`, username)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
_, err = db.Exec(`INSERT INTO player_stats (username, games_played, games_won, rating) VALUES (?, 0, 0, 1000.0)`, username)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
// Simulate a win
|
||||||
|
_, err = db.Exec(`UPDATE player_stats SET games_played = 1, games_won = 1 WHERE username = ?`, username)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
var ratingAfterWin float64
|
||||||
|
err = db.Get(&ratingAfterWin, `SELECT rating FROM player_stats WHERE username = ?`, username)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
// Expected: 1000 + 32 * (1 - 0.5) + 0.05 * (1000 - 1000) = 1000 + 16 = 1016
|
||||||
|
assert.InDelta(t, 1016.0, ratingAfterWin, 0.001)
|
||||||
|
|
||||||
|
// Simulate a loss
|
||||||
|
_, err = db.Exec(`UPDATE player_stats SET games_played = 2, games_won = 1 WHERE username = ?`, username)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
var ratingAfterLoss float64
|
||||||
|
err = db.Get(&ratingAfterLoss, `SELECT rating FROM player_stats WHERE username = ?`, username)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
// Expected: 1016 + 32 * (0 - 0.5) + 0.05 * (1000 - 1016) = 1016 - 16 + 0.05 * (-16) = 1000 - 0.8 = 999.2
|
||||||
|
assert.InDelta(t, 999.2, ratingAfterLoss, 0.001)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
func TestPlayersRepo_AddPlayer(t *testing.T) {
|
func TestPlayersRepo_AddPlayer(t *testing.T) {
|
||||||
db, teardown := setupPlayersTestDB(t)
|
db, teardown := setupPlayersTestDB(t)
|
||||||
defer teardown()
|
defer teardown()
|
||||||
@@ -107,4 +261,3 @@ func TestPlayersRepo_DeletePlayer(t *testing.T) {
|
|||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Equal(t, 0, count)
|
assert.Equal(t, 0, count)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -15,6 +15,20 @@ type RoomsRepo interface {
|
|||||||
RoomCreate(ctx context.Context, room *models.Room) error
|
RoomCreate(ctx context.Context, room *models.Room) error
|
||||||
RoomDeleteByID(ctx context.Context, id string) error
|
RoomDeleteByID(ctx context.Context, id string) error
|
||||||
RoomUpdate(ctx context.Context, room *models.Room) error
|
RoomUpdate(ctx context.Context, room *models.Room) error
|
||||||
|
RoomSetBotFailed(ctx context.Context, roomID string) error
|
||||||
|
RoomUnSetBotFailed(ctx context.Context, roomID string) error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *RepoProvider) RoomSetBotFailed(ctx context.Context, roomID string) error {
|
||||||
|
db := getDB(ctx, p.DB)
|
||||||
|
_, err := db.ExecContext(ctx, "UPDATE rooms SET bot_failed = true WHERE id = ?", roomID)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *RepoProvider) RoomUnSetBotFailed(ctx context.Context, roomID string) error {
|
||||||
|
db := getDB(ctx, p.DB)
|
||||||
|
_, err := db.ExecContext(ctx, "UPDATE rooms SET bot_failed = false WHERE id = ?", roomID)
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *RepoProvider) RoomList(ctx context.Context) ([]*models.Room, error) {
|
func (p *RepoProvider) RoomList(ctx context.Context) ([]*models.Room, error) {
|
||||||
|
63
telemetry/telemetry.go
Normal file
63
telemetry/telemetry.go
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
package telemetry
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
|
||||||
|
"go.opentelemetry.io/otel"
|
||||||
|
"go.opentelemetry.io/otel/exporters/jaeger"
|
||||||
|
"go.opentelemetry.io/otel/sdk/resource"
|
||||||
|
sdktrace "go.opentelemetry.io/otel/sdk/trace"
|
||||||
|
semconv "go.opentelemetry.io/otel/semconv/v1.21.0"
|
||||||
|
)
|
||||||
|
|
||||||
|
// newJaegerExporter creates a new Jaeger exporter.
|
||||||
|
func newJaegerExporter() (sdktrace.SpanExporter, error) {
|
||||||
|
return jaeger.New(jaeger.WithCollectorEndpoint(jaeger.WithEndpoint("http://localhost:14268/api/traces")))
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewTracerProvider creates a new tracer provider.
|
||||||
|
func NewTracerProvider(exp sdktrace.SpanExporter) *sdktrace.TracerProvider {
|
||||||
|
res := resource.NewWithAttributes(
|
||||||
|
semconv.SchemaURL,
|
||||||
|
semconv.ServiceName("gralias"),
|
||||||
|
semconv.ServiceVersion("v0.1.0"),
|
||||||
|
)
|
||||||
|
|
||||||
|
tracerProvider := sdktrace.NewTracerProvider(
|
||||||
|
sdktrace.WithSampler(sdktrace.AlwaysSample()),
|
||||||
|
sdktrace.WithResource(res),
|
||||||
|
sdktrace.WithBatcher(exp),
|
||||||
|
)
|
||||||
|
return tracerProvider
|
||||||
|
}
|
||||||
|
|
||||||
|
// OtelMiddleware wraps the provided http.Handler with OpenTelemetry tracing.
|
||||||
|
func OtelMiddleware(handler http.Handler) http.Handler {
|
||||||
|
return otelhttp.NewHandler(handler, "http.server",
|
||||||
|
otelhttp.WithSpanNameFormatter(func(operation string, r *http.Request) string {
|
||||||
|
return r.URL.Path
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func InitTracer() func() {
|
||||||
|
exp, err := newJaegerExporter()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("failed to create exporter: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
tp := NewTracerProvider(exp)
|
||||||
|
otel.SetTracerProvider(tp)
|
||||||
|
|
||||||
|
return func() {
|
||||||
|
if err := tp.Shutdown(context.Background()); err != nil {
|
||||||
|
log.Printf("Error shutting down tracer provider: %v", err)
|
||||||
|
}
|
||||||
|
if err := exp.Shutdown(context.Background()); err != nil {
|
||||||
|
log.Printf("Error shutting down exporter: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
12
todos.md
12
todos.md
@@ -21,8 +21,8 @@
|
|||||||
- redo card .revealed use: it should mean that card is revealed for everybody, while mime should be able to see cards as is; +
|
- redo card .revealed use: it should mean that card is revealed for everybody, while mime should be able to see cards as is; +
|
||||||
- better styles and fluff;
|
- better styles and fluff;
|
||||||
- common auth system between sites;
|
- common auth system between sites;
|
||||||
- signup vs login;
|
- signup vs login; +
|
||||||
- passwords (to room and to login);
|
- passwords (to room and to login); +
|
||||||
===
|
===
|
||||||
- show in backlog (and with that in prompt to llm) how many cards are left to open, also additional comment: if guess was right;
|
- show in backlog (and with that in prompt to llm) how many cards are left to open, also additional comment: if guess was right;
|
||||||
- gameover to backlog;
|
- gameover to backlog;
|
||||||
@@ -33,7 +33,9 @@
|
|||||||
- possibly turn markings into parts of names of users (first three letters?); +
|
- possibly turn markings into parts of names of users (first three letters?); +
|
||||||
- at game creation list languages and support them at backend; +
|
- at game creation list languages and support them at backend; +
|
||||||
- sql ping goroutine with reconnect on fail; +
|
- sql ping goroutine with reconnect on fail; +
|
||||||
- player stats: played games, lost, won, rating elo, opened opposite words, opened white words, opened black words.
|
- player stats: played games, lost, won, rating elo, opened opposite words, opened white words, opened black words. +
|
||||||
|
- at the end of the game, all colors should be revealed;
|
||||||
|
- tracing;
|
||||||
|
|
||||||
#### sse points
|
#### sse points
|
||||||
- clue sse update;
|
- clue sse update;
|
||||||
@@ -89,5 +91,5 @@
|
|||||||
- mime sees the clue input out of turn; (eh)
|
- mime sees the clue input out of turn; (eh)
|
||||||
- there is a problem of two timers, they both could switch turn, but it is not easy to stop them from llmapi or handlers. +
|
- there is a problem of two timers, they both could switch turn, but it is not easy to stop them from llmapi or handlers. +
|
||||||
- journal still does not work; +
|
- journal still does not work; +
|
||||||
- lose/win game; then exit room (while being the creator), then press to stats -> cannot find session in db, although cookie in place and session in db;
|
- lose/win game; then exit room (while being the creator), then press to stats -> cannot find session in db, although cookie in place and session in db; +
|
||||||
- exit endpoints delets player from db;
|
- exit endpoints delets player from db; +
|
||||||
|
Reference in New Issue
Block a user