Compare commits
16 Commits
8f9865db3f
...
master
Author | SHA1 | Date | |
---|---|---|---|
![]() |
f61de5645d | ||
![]() |
d076bd1348 | ||
![]() |
33bda503fc | ||
![]() |
1664c26c0a | ||
![]() |
6daab9cb1a | ||
![]() |
a1f38c2b26 | ||
![]() |
4fed716f8c | ||
![]() |
ab60476688 | ||
![]() |
57a2abc1f9 | ||
![]() |
995f9f6249 | ||
![]() |
ccf0b8538f | ||
![]() |
123d6c240f | ||
![]() |
6951ec0535 | ||
![]() |
ad44dc0642 | ||
![]() |
155aa1b2cb | ||
![]() |
757586ea22 |
@@ -1,14 +1,14 @@
|
|||||||
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 2em !important;
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
margin-right: auto;
|
margin-right: auto;
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
font-family: Open Sans,Arial;
|
font-family: Open Sans,Arial;
|
||||||
|
font-weight: bold;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
@@ -23,18 +23,6 @@ tr{
|
|||||||
#usertable{
|
#usertable{
|
||||||
display: block ruby;
|
display: block ruby;
|
||||||
}
|
}
|
||||||
.actiontable{
|
|
||||||
display: inline flow-root;
|
|
||||||
margin-inline: 10px;
|
|
||||||
}
|
|
||||||
.action_name{
|
|
||||||
border: none;
|
|
||||||
display: inline;
|
|
||||||
font-family: inherit;
|
|
||||||
font-size: inherit;
|
|
||||||
padding: none;
|
|
||||||
width: auto;
|
|
||||||
}
|
|
||||||
#errorbox{
|
#errorbox{
|
||||||
border: 1px solid black;
|
border: 1px solid black;
|
||||||
background-color: darkorange;
|
background-color: darkorange;
|
||||||
|
Binary file not shown.
@@ -146,7 +146,6 @@ rage
|
|||||||
southwest
|
southwest
|
||||||
cycle
|
cycle
|
||||||
roman
|
roman
|
||||||
jay
|
|
||||||
stage
|
stage
|
||||||
ability
|
ability
|
||||||
duration
|
duration
|
||||||
@@ -545,7 +544,6 @@ norm
|
|||||||
imaginary
|
imaginary
|
||||||
pace
|
pace
|
||||||
weather
|
weather
|
||||||
marina
|
|
||||||
week
|
week
|
||||||
scale
|
scale
|
||||||
step
|
step
|
||||||
@@ -885,7 +883,6 @@ ready
|
|||||||
theorem
|
theorem
|
||||||
disagreement
|
disagreement
|
||||||
led
|
led
|
||||||
benjamin
|
|
||||||
era
|
era
|
||||||
snow
|
snow
|
||||||
bowl
|
bowl
|
||||||
@@ -1355,7 +1352,6 @@ male
|
|||||||
red
|
red
|
||||||
objective
|
objective
|
||||||
bond
|
bond
|
||||||
jimmy
|
|
||||||
opera
|
opera
|
||||||
foliage
|
foliage
|
||||||
motive
|
motive
|
||||||
@@ -1420,7 +1416,6 @@ landscape
|
|||||||
chin
|
chin
|
||||||
sermon
|
sermon
|
||||||
celebration
|
celebration
|
||||||
sba
|
|
||||||
curriculum
|
curriculum
|
||||||
market
|
market
|
||||||
bullet
|
bullet
|
||||||
@@ -1453,7 +1448,6 @@ profit
|
|||||||
conductor
|
conductor
|
||||||
elevator
|
elevator
|
||||||
brutality
|
brutality
|
||||||
tom
|
|
||||||
community
|
community
|
||||||
hydrogen
|
hydrogen
|
||||||
noon
|
noon
|
||||||
@@ -1527,7 +1521,6 @@ touch
|
|||||||
association
|
association
|
||||||
abandon
|
abandon
|
||||||
buddy
|
buddy
|
||||||
christ
|
|
||||||
passion
|
passion
|
||||||
coverage
|
coverage
|
||||||
region
|
region
|
||||||
@@ -1541,13 +1534,11 @@ prince
|
|||||||
availability
|
availability
|
||||||
rebel
|
rebel
|
||||||
development
|
development
|
||||||
pro
|
|
||||||
raise
|
raise
|
||||||
sink
|
sink
|
||||||
value
|
value
|
||||||
discovery
|
discovery
|
||||||
fly
|
fly
|
||||||
warren
|
|
||||||
overhead
|
overhead
|
||||||
spell
|
spell
|
||||||
cross
|
cross
|
||||||
@@ -1622,7 +1613,6 @@ star
|
|||||||
improvement
|
improvement
|
||||||
object
|
object
|
||||||
permanent
|
permanent
|
||||||
pat
|
|
||||||
carpet
|
carpet
|
||||||
separation
|
separation
|
||||||
sport
|
sport
|
||||||
@@ -1924,7 +1914,6 @@ investment
|
|||||||
ancient
|
ancient
|
||||||
listener
|
listener
|
||||||
impulse
|
impulse
|
||||||
magnum
|
|
||||||
affair
|
affair
|
||||||
realm
|
realm
|
||||||
tea
|
tea
|
||||||
@@ -1956,7 +1945,6 @@ darkness
|
|||||||
longer
|
longer
|
||||||
emotion
|
emotion
|
||||||
funeral
|
funeral
|
||||||
pip
|
|
||||||
there
|
there
|
||||||
secondary
|
secondary
|
||||||
obligation
|
obligation
|
||||||
@@ -2008,7 +1996,6 @@ rail
|
|||||||
aesthetic
|
aesthetic
|
||||||
player
|
player
|
||||||
porter
|
porter
|
||||||
sam
|
|
||||||
singular
|
singular
|
||||||
text
|
text
|
||||||
tongue
|
tongue
|
||||||
@@ -2119,7 +2106,6 @@ juvenile
|
|||||||
extent
|
extent
|
||||||
prayer
|
prayer
|
||||||
gin
|
gin
|
||||||
hogan
|
|
||||||
food
|
food
|
||||||
explosion
|
explosion
|
||||||
past
|
past
|
||||||
|
File diff suppressed because it is too large
Load Diff
@@ -9,7 +9,7 @@ import (
|
|||||||
|
|
||||||
// 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.Millisecond * 500
|
||||||
|
|
||||||
type (
|
type (
|
||||||
NotificationEvent struct {
|
NotificationEvent struct {
|
||||||
@@ -91,7 +91,9 @@ func (broker *Broker) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
// Listen for new notifications and redistribute them to clients
|
// Listen for new notifications and redistribute them to clients
|
||||||
func (broker *Broker) Listen() {
|
func (broker *Broker) Listen() {
|
||||||
|
slog.Info("Broker listener started")
|
||||||
for {
|
for {
|
||||||
|
slog.Info("Broker waiting for event")
|
||||||
select {
|
select {
|
||||||
case s := <-broker.newClients:
|
case s := <-broker.newClients:
|
||||||
// A new client has connected.
|
// A new client has connected.
|
||||||
@@ -104,16 +106,21 @@ func (broker *Broker) Listen() {
|
|||||||
delete(broker.clients, s)
|
delete(broker.clients, s)
|
||||||
slog.Info("Client removed", "clients listening", len(broker.clients))
|
slog.Info("Client removed", "clients listening", len(broker.clients))
|
||||||
case event := <-broker.Notifier:
|
case event := <-broker.Notifier:
|
||||||
|
slog.Info("Received new event", "event", event.EventName, "payload", event.Payload)
|
||||||
// We got a new event from the outside!
|
// We got a new event from the outside!
|
||||||
// Send event to all connected clients
|
// Send event to all connected clients
|
||||||
|
slog.Info("Broadcasting event to clients", "client_count", len(broker.clients))
|
||||||
for clientMessageChan := range broker.clients {
|
for clientMessageChan := range broker.clients {
|
||||||
|
slog.Info("Sending event to client", "client", clientMessageChan)
|
||||||
select {
|
select {
|
||||||
case clientMessageChan <- event:
|
case clientMessageChan <- event:
|
||||||
|
slog.Info("Successfully sent event to client", "client", clientMessageChan)
|
||||||
case <-time.After(patience):
|
case <-time.After(patience):
|
||||||
delete(broker.clients, clientMessageChan)
|
delete(broker.clients, clientMessageChan)
|
||||||
slog.Info("Client was removed", "clients listening", len(broker.clients))
|
slog.Warn("Client timed out, removed", "client", clientMessageChan, "clients listening", len(broker.clients))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
slog.Info("Finished broadcasting event")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,27 +1,17 @@
|
|||||||
{{define "actionhistory"}}
|
{{define "actionhistory"}}
|
||||||
<div id="actionHistoryContainer" class="overflow-y-auto max-h-96 border-2 border-gray-300 p-4 rounded-lg space-y-2">
|
<div id="actionHistoryContainer" class="overflow-y-auto max-h-96 border-2 border-gray-300 p-4 rounded-lg space-y-2 h-full flex-col-reverse justify-end">
|
||||||
Backlog:
|
Backlog:
|
||||||
{{range .}}
|
{{range .}}
|
||||||
<div class="flex items-center justify-between p-2 rounded">
|
<div class="flex items-center justify-between p-2 rounded">
|
||||||
<span class="font-mono text-sm">
|
<span class="font-mono text-sm">
|
||||||
<span class="text-{{.ActorColor}}-600">{{.Actor}}:</span>
|
<span class="text-{{.ActorColor}}-600">{{.Actor}}:</span>
|
||||||
<span class="text-gray-600">{{.Action}}:</span>
|
<span class="text-gray-600">{{.Action}}:</span>
|
||||||
<span class="text-{{.WordColor}}-500 font-medium">{{.Word}}</span>
|
<span class="text-{{.WordColor}}-500 font-medium">{{.Word}}</span>
|
||||||
{{if .Number}}
|
{{if .Number}}
|
||||||
<span class="text-gray-400">- {{.Number}}</span>
|
<span class="text-gray-400">- {{.Number}}</span>
|
||||||
{{end}}
|
{{end}}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
<script>
|
|
||||||
if (!window.actionHistoryScrollSet) {
|
|
||||||
htmx.onLoad(function(target) {
|
|
||||||
if (target.id === 'actionHistoryContainer') {
|
|
||||||
target.scrollTop = target.scrollHeight;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
window.actionHistoryScrollSet = true;
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
{{end}}
|
{{end}}
|
||||||
|
@@ -3,7 +3,69 @@
|
|||||||
<div class="flex justify-center">
|
<div class="flex justify-center">
|
||||||
<div class="grid grid-cols-2 sm:grid-cols-5 gap-2">
|
<div class="grid grid-cols-2 sm:grid-cols-5 gap-2">
|
||||||
{{range .Cards}}
|
{{range .Cards}}
|
||||||
{{template "cardword" .}}
|
{{if .Revealed}}
|
||||||
|
{{if eq .Color "amber"}}
|
||||||
|
<div id="card-{{.Word}}" class="bg-{{.Color}}-100 border-8 border-stine-400 p-4 min-w-[100px] text-center text-white cursor-pointer line-through"
|
||||||
|
style="text-shadow: 0 2px 4px rgba(0,0,0,0.9);"> {{.Word}}
|
||||||
|
</div>
|
||||||
|
{{else}}
|
||||||
|
<div id="card-{{.Word}}" class="bg-{{.Color}}-400 border-8 border-stone-400 p-4 min-w-[100px] text-center text-white cursor-pointer line-through"
|
||||||
|
style="text-shadow: 0 2px 4px rgba(0,0,0,0.9);"> {{.Word}}
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
{{else if $.IsOver}}
|
||||||
|
{{if eq .Color "amber"}}
|
||||||
|
<div id="card-{{.Word}}" class="bg-{{.Color}}-100 border border-stone-400 p-4 rounded-lg min-w-[100px] text-center text-white cursor-pointer"
|
||||||
|
style="text-shadow: 0 2px 4px rgba(0,0,0,0.9);"> {{.Word}}
|
||||||
|
</div>
|
||||||
|
{{else}}
|
||||||
|
<div id="card-{{.Word}}" class="bg-{{.Color}}-400 border border-stone-400 p-4 rounded-lg min-w-[100px] text-center text-white cursor-pointer"
|
||||||
|
style="text-shadow: 0 2px 4px rgba(0,0,0,0.9);"> {{.Word}}
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
{{else if .Mime}}
|
||||||
|
{{if eq .Color "amber"}}
|
||||||
|
<div id="card-{{.Word}}" class="bg-{{.Color}}-100 border border-gray-500 rounded-lg min-w-[100px] cursor-pointer flex flex-col h-full">
|
||||||
|
{{else}}
|
||||||
|
<div id="card-{{.Word}}" class="bg-{{.Color}}-400 border border-gray-500 rounded-lg min-w-[100px] cursor-pointer flex flex-col h-full">
|
||||||
|
{{end}}
|
||||||
|
<div class="flex-grow text-center p-4 flex items-center justify-center text-white"
|
||||||
|
style="text-shadow: 0 2px 4px rgba(0,0,0,0.8);"
|
||||||
|
hx-get="/word/show-color?word={{.Word}}" hx-trigger="click" hx-swap="outerHTML transition:true swap:.05s">
|
||||||
|
{{.Word}}
|
||||||
|
</div>
|
||||||
|
<div class="h-6 bg-stone-600 rounded-b flex items-center justify-center text-white text-sm cursor-pointer"
|
||||||
|
hx-get="/mark-card?word={{.Word}}" hx-trigger="click" hx-swap="outerHTML transition:true swap:.05s">
|
||||||
|
{{range .Marks}}
|
||||||
|
{{ $length := len .Username }}
|
||||||
|
{{ if lt $length 3 }}
|
||||||
|
<span class="mx-0.5">{{.Username}}</span>
|
||||||
|
{{else}}
|
||||||
|
<span class="mx-0.5">{{slice .Username 0 3}}</span>
|
||||||
|
{{end}}
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{else}}
|
||||||
|
<div id="card-{{.Word}}" class="bg-stone-400 border border-gray-500 rounded-lg min-w-[100px] cursor-pointer flex flex-col h-full">
|
||||||
|
<div class="flex-grow text-center p-4 flex items-center justify-center text-white"
|
||||||
|
style="text-shadow: 0 2px 4px rgba(0,0,0,0.8);"
|
||||||
|
hx-get="/word/show-color?word={{.Word}}" hx-trigger="click" hx-swap="outerHTML transition:true swap:.05s">
|
||||||
|
{{.Word}}
|
||||||
|
</div>
|
||||||
|
<div class="h-6 bg-stone-600 rounded-b flex items-center justify-center text-white text-sm cursor-pointer"
|
||||||
|
hx-get="/mark-card?word={{.Word}}" hx-trigger="click" hx-swap="outerHTML transition:true swap:.05s">
|
||||||
|
{{range .Marks}}
|
||||||
|
{{ $length := len .Username }}
|
||||||
|
{{ if lt $length 3 }}
|
||||||
|
<span class="mx-0.5">{{.Username}}</span>
|
||||||
|
{{else}}
|
||||||
|
<span class="mx-0.5">{{slice .Username 0 3}}</span>
|
||||||
|
{{end}}
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
{{end}}
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -9,7 +9,7 @@
|
|||||||
style="text-shadow: 0 2px 4px rgba(0,0,0,0.9);"> {{.Word}}
|
style="text-shadow: 0 2px 4px rgba(0,0,0,0.9);"> {{.Word}}
|
||||||
</div>
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
{{else if .Mime}}
|
{{else if or (.Mime) }}
|
||||||
{{if eq .Color "amber"}}
|
{{if eq .Color "amber"}}
|
||||||
<div id="card-{{.Word}}" class="bg-{{.Color}}-100 border border-stone-400 p-4 rounded-lg min-w-[100px] text-center text-white cursor-pointer"
|
<div id="card-{{.Word}}" class="bg-{{.Color}}-100 border border-stone-400 p-4 rounded-lg min-w-[100px] text-center text-white cursor-pointer"
|
||||||
style="text-shadow: 0 2px 4px rgba(0,0,0,0.9);"> {{.Word}}
|
style="text-shadow: 0 2px 4px rgba(0,0,0,0.9);"> {{.Word}}
|
||||||
|
@@ -4,9 +4,9 @@
|
|||||||
Create a room <br/>
|
Create a room <br/>
|
||||||
or<br/>
|
or<br/>
|
||||||
<button button class="justify-center rounded-md bg-indigo-600 px-3 py-1.5 text-sm font-semibold leading-6 text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600" hx-get="/room/hideform" hx-target=".create-room-div" >Hide Form</button>
|
<button button class="justify-center rounded-md bg-indigo-600 px-3 py-1.5 text-sm font-semibold leading-6 text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600" hx-get="/room/hideform" hx-target=".create-room-div" >Hide Form</button>
|
||||||
<form hx-post="/room-create" hx-target="#main-content">
|
<form hx-post="/room-create" hx-target="#main-content" class="space-y-4">
|
||||||
<label For="game_time">Turn Seconds:</label><br/>
|
<label For="game_time">Turn Seconds:</label><br/>
|
||||||
<input type="number" id="game_time" name="game_time" class="text-center text-white" value="300"/><br/>
|
<input type="number" id="game_time" name="game_time" class="text-center text-white" value="120"/><br/>
|
||||||
<label For="language">Language:</label><br/>
|
<label For="language">Language:</label><br/>
|
||||||
<div>
|
<div>
|
||||||
<select class="form-select text-white text-center bg-gray-900" id="languages" name="language">
|
<select class="form-select text-white text-center bg-gray-900" id="languages" name="language">
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
{{define "error"}}
|
{{define "error"}}
|
||||||
<a href="/">
|
<a href="/">
|
||||||
<div id=errorbox class="bg-orange-100 border-l-4 border-orange-500 text-orange-700 p-4" role="alert">
|
<div id=errorbox class="bg-orange-100 border-l-4 border-black text-black p-4" role="alert">
|
||||||
<p class="font-bold">An error from server</p>
|
<p class="font-bold">An error from server</p>
|
||||||
<p>{{.}}</p>
|
<p>{{.}}</p>
|
||||||
<p>Click this banner to return to main page.</p>
|
<p>Click this banner to return to main page.</p>
|
||||||
|
11
components/journal.html
Normal file
11
components/journal.html
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
{{define "journal"}}
|
||||||
|
<div id="systembox" class="overflow-y-auto max-h-96 border-2 border-gray-300 p-4 rounded-lg space-y-2">
|
||||||
|
bot journal: <br>
|
||||||
|
<ul>
|
||||||
|
{{range .LogJournal}}
|
||||||
|
<li>{{.Username}}: {{.Entry}}</li>
|
||||||
|
{{end}}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
|
@@ -1,19 +1,21 @@
|
|||||||
{{define "login"}}
|
{{define "login"}}
|
||||||
<div id="logindiv">
|
<div id="logindiv">
|
||||||
<form class="space-y-6" hx-post="/login" hx-target="#main-content">
|
<form hx-post="/login" hx-target="#main-content" class="space-y-4">
|
||||||
<div>
|
<div>
|
||||||
<label For="username" class="block text-sm font-medium leading-6 text-white-900">tell us your username (signup|login)</label>
|
<label For="username" class="text-sm text-center font-medium leading-6 text-white-900">tell us your username (signup|login)</label>
|
||||||
<div class="mt-2">
|
<div class="mt-2">
|
||||||
<input id="username" name="username" hx-target="#login_notice" hx-swap="outerHTML" hx-post="/check/name" hx-trigger="input changed delay:400ms" autocomplete="username" required class="block w-full rounded-md border-0 bg-white py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-300 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6 text-center"/>
|
<input id="username" name="username" hx-target="#login_notice" hx-swap="outerHTML" hx-post="/check/name" hx-trigger="input changed delay:400ms" autocomplete="username" required class="text-center rounded-md border-0 bg-white py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-300 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6 text-center"/>
|
||||||
</div>
|
</div>
|
||||||
<div id="login_notice">this name looks available</div>
|
<div id="login_notice">this name looks available</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label For="password" class="block text-sm font-medium leading-6 text-white-900">password</label>
|
<label For="password" class="text-sm font-medium text-center leading-6 text-white-900">password</label>
|
||||||
<input id="password" name="password" type="password" class="block w-full rounded-md border-0 bg-white py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-300 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6 text-center"/>
|
<div class="mt-2">
|
||||||
|
<input id="password" name="password" type="password" class="rounded-md border-0 bg-white py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-300 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6 text-center"/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<button type="submit" class="flex w-full justify-center rounded-md bg-indigo-600 px-3 py-1.5 text-sm font-semibold leading-6 text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600">Sign in</button>
|
<button type="submit" class="justify-center rounded-md bg-indigo-600 px-3 py-1.5 text-sm font-semibold leading-6 text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600">Sign in</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</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="interier" hx-get="/" hx-trigger="sse:roomupdate_{{.State.RoomID}}" 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,33 +49,31 @@
|
|||||||
<!-- Right Panel -->
|
<!-- Right Panel -->
|
||||||
{{template "teamlist" .Room.RedTeam}}
|
{{template "teamlist" .Room.RedTeam}}
|
||||||
</div>
|
</div>
|
||||||
<hr/>
|
|
||||||
<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>
|
|
||||||
<ul>
|
|
||||||
{{range .Room.LogJournal}}
|
|
||||||
<li>{{.Username}}: {{.Entry}}</li>
|
|
||||||
{{end}}
|
|
||||||
</ul>
|
|
||||||
</div>
|
</div>
|
||||||
<div hx-get="/actionhistory" hx-trigger="sse:backlog_{{.Room.ID}}">
|
|
||||||
{{template "actionhistory" .Room.ActionHistory}}
|
|
||||||
</div>
|
</div>
|
||||||
<hr/>
|
<hr/>
|
||||||
<div id="cardtable">
|
<div class="grid grid-cols-1 md:grid-cols-5 md:gap-4">
|
||||||
{{template "cardtable" .Room}}
|
<div 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">
|
||||||
|
{{template "journal" .Room}}
|
||||||
|
</div> <!-- Spacer -->
|
||||||
</div>
|
</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) (.Room.MimeDone)}}
|
||||||
<button hx-get="/end-turn" hx-target="#room" class="bg-amber-100 text-black px-4 py-2 rounded">End Turn</button>
|
<button hx-get="/end-turn" hx-target="#room" class="bg-amber-100 text-black px-4 py-2 rounded">End Turn</button>
|
||||||
{{else if and (eq .State.Role "mime") (not .Room.MimeDone)}}
|
{{else if and (eq .State.Role "mime") (not .Room.MimeDone) (eq .State.Team .Room.TeamTurn)}}
|
||||||
{{template "mimeclue"}}
|
{{template "mimeclue"}}
|
||||||
{{end}}
|
{{end}}
|
||||||
{{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>
|
||||||
|
@@ -42,8 +42,9 @@ 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 {
|
||||||
@@ -206,12 +207,18 @@ 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 {
|
||||||
abortWithError(w, err.Error())
|
// abortWithError(w, err.Error())
|
||||||
|
log.Debug("pressed mark-card out of move", "error", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if fi.State.Role == models.UserRoleMime {
|
||||||
|
log.Debug("mime pressed mark-card")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
color, exists := fi.Room.FindColor(word)
|
color, exists := fi.Room.FindColor(word)
|
||||||
@@ -274,8 +281,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")
|
||||||
@@ -293,8 +301,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
|
||||||
@@ -319,8 +328,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 {
|
||||||
|
@@ -72,8 +72,9 @@ 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
|
return
|
||||||
}
|
}
|
||||||
if fi.Room == nil {
|
if fi.Room == nil {
|
||||||
@@ -111,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
|
||||||
@@ -143,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
|
||||||
@@ -250,13 +253,16 @@ func HandleStartGame(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
func HandleJoinRoom(w http.ResponseWriter, r *http.Request) {
|
func HandleJoinRoom(w http.ResponseWriter, r *http.Request) {
|
||||||
roomID := r.URL.Query().Get("id")
|
roomID := r.URL.Query().Get("id")
|
||||||
|
log.Debug("got join-room request", "id", roomID)
|
||||||
room, err := repo.RoomGetExtended(r.Context(), roomID)
|
room, err := repo.RoomGetExtended(r.Context(), roomID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
log.Error("failed to fetch room", "error", err, "room_id", roomID)
|
||||||
abortWithError(w, err.Error())
|
abortWithError(w, err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
tmpl, err := template.ParseGlob("components/*.html")
|
tmpl, err := template.ParseGlob("components/*.html")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
log.Error("failed to parse templates", "error", err, "room_id", roomID)
|
||||||
abortWithError(w, err.Error())
|
abortWithError(w, err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -268,6 +274,7 @@ func HandleJoinRoom(w http.ResponseWriter, r *http.Request) {
|
|||||||
if err := tmpl.ExecuteTemplate(w, "base", fi); err != nil {
|
if err := tmpl.ExecuteTemplate(w, "base", fi); err != nil {
|
||||||
log.Error("failed to execute base template", "error", err)
|
log.Error("failed to execute base template", "error", err)
|
||||||
}
|
}
|
||||||
|
log.Error("failed to fetch fi", "error", err, "room_id", roomID)
|
||||||
// abortWithError(w, err.Error())
|
// abortWithError(w, err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -280,7 +287,7 @@ func HandleJoinRoom(w http.ResponseWriter, r *http.Request) {
|
|||||||
abortWithError(w, err.Error())
|
abortWithError(w, err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if err := tmpl.ExecuteTemplate(w, "room", fi); err != nil {
|
if err := tmpl.ExecuteTemplate(w, "base", fi); err != nil {
|
||||||
log.Error("failed to execute room template", "error", err)
|
log.Error("failed to execute room template", "error", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -293,8 +300,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)
|
||||||
@@ -360,8 +368,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)
|
||||||
|
@@ -9,6 +9,8 @@ import (
|
|||||||
"log/slog"
|
"log/slog"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
|
"slices"
|
||||||
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -55,6 +57,12 @@ func HandleHome(w http.ResponseWriter, r *http.Request) {
|
|||||||
} else {
|
} else {
|
||||||
fi.Room.GuesserView()
|
fi.Room.GuesserView()
|
||||||
}
|
}
|
||||||
|
// reverse actions if first action
|
||||||
|
if len(fi.Room.ActionHistory) > 1 {
|
||||||
|
if strings.EqualFold(fi.Room.ActionHistory[0].Action, models.ActionTypeGameStarted) {
|
||||||
|
slices.Reverse(fi.Room.ActionHistory)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if fi != nil && fi.Room == nil {
|
if fi != nil && fi.Room == nil {
|
||||||
rooms, err := repo.RoomList(r.Context())
|
rooms, err := repo.RoomList(r.Context())
|
||||||
@@ -75,8 +83,14 @@ 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
|
||||||
|
}
|
||||||
|
if fi.Room == nil {
|
||||||
|
log.Error("failed to fetch room")
|
||||||
|
http.Redirect(w, r, "/", 302)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if fi.Room.IsRunning {
|
if fi.Room.IsRunning {
|
||||||
@@ -105,8 +119,9 @@ func HandleExit(w http.ResponseWriter, r *http.Request) {
|
|||||||
if err := repo.RoomDeleteByID(r.Context(), fi.Room.ID); err != nil {
|
if err := repo.RoomDeleteByID(r.Context(), fi.Room.ID); err != nil {
|
||||||
log.Error("failed to remove room", "error", err)
|
log.Error("failed to remove room", "error", err)
|
||||||
}
|
}
|
||||||
notify(models.NotifyRoomListUpdate, "")
|
notify(models.NotifyRoomListUpdate, "") // why is it needed?
|
||||||
} else {
|
} else {
|
||||||
|
notify(models.NotifyRoomUpdatePrefix, "")
|
||||||
// if regular player leaves, just exit room
|
// if regular player leaves, just exit room
|
||||||
if err := repo.PlayerExitRoom(r.Context(), fi.State.Username); err != nil {
|
if err := repo.PlayerExitRoom(r.Context(), fi.State.Username); err != nil {
|
||||||
log.Error("failed to exit room", "error", err)
|
log.Error("failed to exit room", "error", err)
|
||||||
|
@@ -12,7 +12,7 @@ func StartTurnTimer(roomID string, timeLeft uint32) {
|
|||||||
logger := slog.Default().With("room_id", roomID)
|
logger := slog.Default().With("room_id", roomID)
|
||||||
|
|
||||||
onTurnEnd := func(ctx context.Context, roomID string) {
|
onTurnEnd := func(ctx context.Context, roomID string) {
|
||||||
room, err := repo.RoomGetByID(context.Background(), roomID)
|
room, err := repo.RoomGetExtended(context.Background(), roomID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Error("failed to get room by id", "error", err)
|
logger.Error("failed to get room by id", "error", err)
|
||||||
return
|
return
|
||||||
@@ -23,7 +23,7 @@ func StartTurnTimer(roomID string, timeLeft uint32) {
|
|||||||
if err := repo.RoomUpdate(context.Background(), room); err != nil {
|
if err := repo.RoomUpdate(context.Background(), room); err != nil {
|
||||||
logger.Error("failed to save room", "error", err)
|
logger.Error("failed to save room", "error", err)
|
||||||
}
|
}
|
||||||
notify(models.NotifyTurnTimerPrefix+room.ID, strconv.FormatUint(uint64(room.Settings.RoundTime), 10))
|
// notify(models.NotifyTurnTimerPrefix+room.ID, strconv.FormatUint(uint64(room.Settings.RoundTime), 10))
|
||||||
notifyBotIfNeeded(room)
|
notifyBotIfNeeded(room)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -31,9 +31,10 @@ func StartTurnTimer(roomID string, timeLeft uint32) {
|
|||||||
notify(models.NotifyTurnTimerPrefix+roomID, strconv.FormatUint(uint64(currentLeft), 10))
|
notify(models.NotifyTurnTimerPrefix+roomID, strconv.FormatUint(uint64(currentLeft), 10))
|
||||||
}
|
}
|
||||||
|
|
||||||
timer.StartTurnTimer(context.Background(), roomID, timeLeft, onTurnEnd, onTick, logger)
|
timer.StartTurnTimer(context.Background(), roomID, int32(timeLeft), onTurnEnd, onTick, logger)
|
||||||
}
|
}
|
||||||
|
|
||||||
func StopTurnTimer(roomID string) {
|
func StopTurnTimer(roomID string) {
|
||||||
timer.StopTurnTimer(roomID)
|
timer.StopTurnTimer(roomID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -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`
|
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`
|
||||||
// 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`
|
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.`
|
||||||
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`
|
GuesserSimplePromptRU = `мы играем в alias;\n тебе дана подсказка (clue): \"%s\";\nпожалуйста, верни свою догадку (guess), а также слова, что тоже подходят к подсказке, но ты меньше в них уверен, в формате json; пример:\n{\n\"guess\": \"отгадка\",\n\"could_be\": [\"слово1\", \"слово2\", ...]\n}\nвот список слов из которых нужно выбрать:\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;`
|
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 := ""
|
||||||
@@ -231,6 +236,9 @@ func (b *Bot) BotMove() {
|
|||||||
b.log.Warn("failed to write to journal", "entry", lj)
|
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)
|
||||||
@@ -545,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)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -573,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 ""
|
||||||
@@ -666,16 +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
|
||||||
}
|
}
|
||||||
entry := fmt.Sprintf("bot '%s' exceeded attempts to call llm;", b.BotName)
|
|
||||||
lj := models.Journal{
|
|
||||||
Entry: entry,
|
|
||||||
Username: b.BotName,
|
|
||||||
RoomID: b.RoomID,
|
|
||||||
}
|
|
||||||
if err := repo.JournalCreate(context.Background(), &lj); err != nil {
|
|
||||||
b.log.Warn("failed to write to journal", "entry", lj)
|
|
||||||
}
|
|
||||||
// notify room
|
|
||||||
// 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")
|
||||||
}
|
}
|
||||||
|
@@ -95,3 +95,52 @@ func (b *Bot) ToPlayer() *models.Player {
|
|||||||
IsBot: true,
|
IsBot: true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ORModel struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
CanonicalSlug string `json:"canonical_slug"`
|
||||||
|
HuggingFaceID string `json:"hugging_face_id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Created int `json:"created"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
ContextLength int `json:"context_length"`
|
||||||
|
Architecture struct {
|
||||||
|
Modality string `json:"modality"`
|
||||||
|
InputModalities []string `json:"input_modalities"`
|
||||||
|
OutputModalities []string `json:"output_modalities"`
|
||||||
|
Tokenizer string `json:"tokenizer"`
|
||||||
|
InstructType any `json:"instruct_type"`
|
||||||
|
} `json:"architecture"`
|
||||||
|
Pricing struct {
|
||||||
|
Prompt string `json:"prompt"`
|
||||||
|
Completion string `json:"completion"`
|
||||||
|
Request string `json:"request"`
|
||||||
|
Image string `json:"image"`
|
||||||
|
Audio string `json:"audio"`
|
||||||
|
WebSearch string `json:"web_search"`
|
||||||
|
InternalReasoning string `json:"internal_reasoning"`
|
||||||
|
} `json:"pricing,omitempty"`
|
||||||
|
TopProvider struct {
|
||||||
|
ContextLength int `json:"context_length"`
|
||||||
|
MaxCompletionTokens int `json:"max_completion_tokens"`
|
||||||
|
IsModerated bool `json:"is_moderated"`
|
||||||
|
} `json:"top_provider"`
|
||||||
|
PerRequestLimits any `json:"per_request_limits"`
|
||||||
|
SupportedParameters []string `json:"supported_parameters"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// https://openrouter.ai/api/v1/models
|
||||||
|
type ORModels struct {
|
||||||
|
Data []ORModel `json:"data"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (orm *ORModels) ListFree() []string {
|
||||||
|
resp := []string{}
|
||||||
|
for _, model := range orm.Data {
|
||||||
|
if model.Pricing.Prompt == "0" && model.Pricing.Request == "0" &&
|
||||||
|
model.Pricing.Completion == "0" {
|
||||||
|
resp = append(resp, model.ID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return resp
|
||||||
|
}
|
||||||
|
64
llmapi/or.go
Normal file
64
llmapi/or.go
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
package llmapi
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"log/slog"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ormodelsLink = "https://openrouter.ai/api/v1/models"
|
||||||
|
ORFreeModels = []string{
|
||||||
|
"google/gemini-2.0-flash-exp:free",
|
||||||
|
"deepseek/deepseek-chat-v3-0324:free",
|
||||||
|
"mistralai/mistral-small-3.2-24b-instruct:free",
|
||||||
|
"qwen/qwen3-14b:free",
|
||||||
|
"google/gemma-3-27b-it:free",
|
||||||
|
"meta-llama/llama-3.3-70b-instruct:free",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
func ListORModels() ([]string, error) {
|
||||||
|
resp, err := http.Get(ormodelsLink)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
if resp.StatusCode != 200 {
|
||||||
|
err := fmt.Errorf("failed to fetch or models; status: %s", resp.Status)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
data := &ORModels{}
|
||||||
|
if err := json.NewDecoder(resp.Body).Decode(data); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
freeModels := data.ListFree()
|
||||||
|
return freeModels, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ORModelListUpdateTicker(ctx context.Context) {
|
||||||
|
ticker := time.NewTicker(time.Hour * 2)
|
||||||
|
freeModels, err := ListORModels()
|
||||||
|
slog.Info("updated free models list", "list", freeModels)
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("failed to update free models list", "list", freeModels)
|
||||||
|
}
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return
|
||||||
|
case <-ticker.C:
|
||||||
|
freeModels, err := ListORModels()
|
||||||
|
slog.Info("updated free models list", "list", freeModels)
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("failed to update free models list", "list", freeModels)
|
||||||
|
// log
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
ORFreeModels = freeModels
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -158,21 +158,12 @@ func (p *openRouterParser) ParseBytes(body []byte) (map[string]any, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (p *openRouterParser) MakePayload(prompt string) io.Reader {
|
func (p *openRouterParser) MakePayload(prompt string) io.Reader {
|
||||||
// Models to rotate through
|
|
||||||
models := []string{
|
|
||||||
"google/gemini-2.0-flash-exp:free",
|
|
||||||
"deepseek/deepseek-chat-v3-0324:free",
|
|
||||||
"mistralai/mistral-small-3.2-24b-instruct:free",
|
|
||||||
"qwen/qwen3-14b:free",
|
|
||||||
"deepseek/deepseek-r1:free",
|
|
||||||
"google/gemma-3-27b-it:free",
|
|
||||||
"meta-llama/llama-3.3-70b-instruct:free",
|
|
||||||
}
|
|
||||||
// Get next model index using atomic addition for thread safety
|
// Get next model index using atomic addition for thread safety
|
||||||
p.modelIndex++
|
p.modelIndex++
|
||||||
model := models[int(p.modelIndex)%len(models)]
|
model := ORFreeModels[int(p.modelIndex)%len(ORFreeModels)]
|
||||||
strPayload := fmt.Sprintf(`{
|
strPayload := fmt.Sprintf(`{
|
||||||
"model": "%s",
|
"model": "%s",
|
||||||
|
"max_tokens": 300,
|
||||||
"messages": [
|
"messages": [
|
||||||
{
|
{
|
||||||
"role": "user",
|
"role": "user",
|
||||||
|
@@ -13,7 +13,7 @@ func (b *Bot) StartTurnTimer(timeLeft uint32) {
|
|||||||
logger := b.log.With("room_id", b.RoomID)
|
logger := b.log.With("room_id", b.RoomID)
|
||||||
|
|
||||||
onTurnEnd := func(ctx context.Context, roomID string) {
|
onTurnEnd := func(ctx context.Context, roomID string) {
|
||||||
room, err := repos.RP.RoomGetByID(context.Background(), roomID)
|
room, err := repos.RP.RoomGetExtended(context.Background(), roomID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Error("failed to get room by id", "error", err)
|
logger.Error("failed to get room by id", "error", err)
|
||||||
return
|
return
|
||||||
@@ -24,10 +24,10 @@ func (b *Bot) StartTurnTimer(timeLeft uint32) {
|
|||||||
if err := repos.RP.RoomUpdate(context.Background(), room); err != nil {
|
if err := repos.RP.RoomUpdate(context.Background(), room); err != nil {
|
||||||
logger.Error("failed to save room", "error", err)
|
logger.Error("failed to save room", "error", err)
|
||||||
}
|
}
|
||||||
broker.Notifier.Notifier <- broker.NotificationEvent{
|
// broker.Notifier.Notifier <- broker.NotificationEvent{
|
||||||
EventName: models.NotifyTurnTimerPrefix + room.ID,
|
// EventName: models.NotifyTurnTimerPrefix + room.ID,
|
||||||
Payload: strconv.FormatUint(uint64(room.Settings.RoundTime), 10),
|
// Payload: strconv.FormatUint(uint64(room.Settings.RoundTime), 10),
|
||||||
}
|
// }
|
||||||
// notifyBotIfNeeded(room)
|
// notifyBotIfNeeded(room)
|
||||||
if botName := room.WhichBotToMove(); botName != "" {
|
if botName := room.WhichBotToMove(); botName != "" {
|
||||||
SignalChanMap[botName] <- true
|
SignalChanMap[botName] <- true
|
||||||
@@ -41,9 +41,10 @@ func (b *Bot) StartTurnTimer(timeLeft uint32) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
timer.StartTurnTimer(context.Background(), b.RoomID, timeLeft, onTurnEnd, onTick, logger)
|
timer.StartTurnTimer(context.Background(), b.RoomID, int32(timeLeft), onTurnEnd, onTick, logger)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Bot) StopTurnTimer() {
|
func (b *Bot) StopTurnTimer() {
|
||||||
timer.StopTurnTimer(b.RoomID)
|
timer.StopTurnTimer(b.RoomID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
2
main.go
2
main.go
@@ -5,6 +5,7 @@ import (
|
|||||||
"gralias/config"
|
"gralias/config"
|
||||||
"gralias/crons"
|
"gralias/crons"
|
||||||
"gralias/handlers"
|
"gralias/handlers"
|
||||||
|
"gralias/llmapi"
|
||||||
"gralias/repos"
|
"gralias/repos"
|
||||||
"gralias/telemetry"
|
"gralias/telemetry"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
@@ -22,6 +23,7 @@ var cfg *config.Config
|
|||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
cfg = config.LoadConfigOrDefault("")
|
cfg = config.LoadConfigOrDefault("")
|
||||||
|
go llmapi.ORModelListUpdateTicker(context.Background())
|
||||||
}
|
}
|
||||||
|
|
||||||
// GzipFileServer serves pre-compressed .gz files if available
|
// GzipFileServer serves pre-compressed .gz files if available
|
||||||
|
@@ -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 ''
|
||||||
);
|
);
|
||||||
|
@@ -178,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) {
|
||||||
@@ -226,7 +228,16 @@ func (r *Room) FetchLastClue() (*Action, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (r *Room) FetchLastClueWord() string {
|
func (r *Room) FetchLastClueWord() string {
|
||||||
for i := len(r.ActionHistory) - 1; i >= 0; i-- {
|
if len(r.ActionHistory) > 1 {
|
||||||
|
if strings.EqualFold(r.ActionHistory[0].Action, ActionTypeGameStarted) {
|
||||||
|
for i := len(r.ActionHistory) - 1; i >= 0; i-- {
|
||||||
|
if r.ActionHistory[i].Action == string(ActionTypeClue) {
|
||||||
|
return r.ActionHistory[i].Word
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for i := 0; i <= len(r.ActionHistory)-1; i++ {
|
||||||
if r.ActionHistory[i].Action == string(ActionTypeClue) {
|
if r.ActionHistory[i].Action == string(ActionTypeClue) {
|
||||||
return r.ActionHistory[i].Word
|
return r.ActionHistory[i].Word
|
||||||
}
|
}
|
||||||
@@ -408,14 +419,22 @@ func (r *Room) RevealSpecificWord(word string) uint32 {
|
|||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *Room) SetGameOverToCards(isover bool) {
|
||||||
|
for i := range r.Cards {
|
||||||
|
r.Cards[i].IsOver = isover
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
type WordCard struct {
|
type WordCard struct {
|
||||||
ID uint32 `json:"id" db:"id"`
|
ID uint32 `json:"id" db:"id"`
|
||||||
RoomID string `json:"room_id" db:"room_id"`
|
RoomID string `json:"room_id" db:"room_id"`
|
||||||
Word string `json:"word" db:"word"`
|
Word string `json:"word" db:"word"`
|
||||||
Color WordColor `json:"color" db:"color"`
|
Color WordColor `json:"color" db:"color"`
|
||||||
Revealed bool `json:"revealed" db:"revealed"`
|
Revealed bool `json:"revealed" db:"revealed"`
|
||||||
Mime bool `json:"mime" db:"mime_view"` // user who sees that card is mime
|
// pain; but at the end of the game players should see color of unopen cards
|
||||||
Marks []CardMark `json:"marks" db:"-"`
|
IsOver bool
|
||||||
|
Mime bool `json:"mime" db:"mime_view"` // user who sees that card is mime
|
||||||
|
Marks []CardMark `json:"marks" db:"-"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// table: settings
|
// table: settings
|
||||||
|
@@ -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) {
|
||||||
|
@@ -14,12 +14,12 @@ type TurnEndCallback func(ctx context.Context, roomID string)
|
|||||||
type TickCallback func(ctx context.Context, roomID string, timeLeft uint32)
|
type TickCallback func(ctx context.Context, roomID string, timeLeft uint32)
|
||||||
|
|
||||||
type RoomTimer struct {
|
type RoomTimer struct {
|
||||||
ticker *time.Ticker
|
ticker *time.Ticker
|
||||||
done chan bool
|
done chan bool
|
||||||
roomID string
|
roomID string
|
||||||
onTurnEnd TurnEndCallback
|
onTurnEnd TurnEndCallback
|
||||||
onTick TickCallback
|
onTick TickCallback
|
||||||
log *slog.Logger
|
log *slog.Logger
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -28,7 +28,7 @@ var (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// StartTurnTimer initializes and starts a new turn timer for a given room.
|
// StartTurnTimer initializes and starts a new turn timer for a given room.
|
||||||
func StartTurnTimer(ctx context.Context, roomID string, timeLeft uint32, onTurnEnd TurnEndCallback, onTick TickCallback, logger *slog.Logger) {
|
func StartTurnTimer(ctx context.Context, roomID string, timeLeft int32, onTurnEnd TurnEndCallback, onTick TickCallback, logger *slog.Logger) {
|
||||||
mu.Lock()
|
mu.Lock()
|
||||||
defer mu.Unlock()
|
defer mu.Unlock()
|
||||||
|
|
||||||
@@ -41,12 +41,12 @@ func StartTurnTimer(ctx context.Context, roomID string, timeLeft uint32, onTurnE
|
|||||||
done := make(chan bool)
|
done := make(chan bool)
|
||||||
|
|
||||||
rt := &RoomTimer{
|
rt := &RoomTimer{
|
||||||
ticker: ticker,
|
ticker: ticker,
|
||||||
done: done,
|
done: done,
|
||||||
roomID: roomID,
|
roomID: roomID,
|
||||||
onTurnEnd: onTurnEnd,
|
onTurnEnd: onTurnEnd,
|
||||||
onTick: onTick,
|
onTick: onTick,
|
||||||
log: logger,
|
log: logger,
|
||||||
}
|
}
|
||||||
timers[roomID] = rt
|
timers[roomID] = rt
|
||||||
|
|
||||||
@@ -62,7 +62,7 @@ func StartTurnTimer(ctx context.Context, roomID string, timeLeft uint32, onTurnE
|
|||||||
StopTurnTimer(roomID)
|
StopTurnTimer(roomID)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
rt.onTick(ctx, roomID, currentLeft)
|
rt.onTick(ctx, roomID, uint32(currentLeft))
|
||||||
currentLeft--
|
currentLeft--
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
26
todos.md
26
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,9 +33,12 @@
|
|||||||
- 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;
|
- at the end of the game, all colors should be revealed;
|
||||||
- tracing;
|
- tracing;
|
||||||
|
====
|
||||||
|
- action history to bot (not json); basic stuff: what words were previously given as clue and guessed (maybe what team bot is on);
|
||||||
|
- automate getting list of free and non-thinking openrouter models;
|
||||||
|
|
||||||
#### sse points
|
#### sse points
|
||||||
- clue sse update;
|
- clue sse update;
|
||||||
@@ -91,5 +94,18 @@
|
|||||||
- 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; +
|
||||||
|
- timer end did not update the page;
|
||||||
|
- timer did not change turn (guesser did not manage to guess 1 word);
|
||||||
|
|
||||||
|
- timers conflict; stop timers;
|
||||||
|
- clue snatching;
|
||||||
|
- llm resp token amount limit;
|
||||||
|
|
||||||
|
=============
|
||||||
|
- autoscroll backlog to the last action; (reversed order) +
|
||||||
|
- mimes to see marks on the words; +
|
||||||
|
- clearer ways to see opened words; (line through) +
|
||||||
|
- guesser sees end turn button before clue was given by mime; +
|
||||||
|
- sql no rows when joining by link?
|
||||||
|
Reference in New Issue
Block a user