Compare commits
8 Commits
17c0e2c259
...
e565f1f8d8
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e565f1f8d8 | ||
|
|
e35ddef4b7 | ||
|
|
1f6b79aa3b | ||
|
|
56f994c837 | ||
|
|
0ec74308a7 | ||
|
|
a7910adc26 | ||
|
|
fa46313386 | ||
|
|
53ca14a1bb |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -1,2 +1,4 @@
|
||||
**/*.sw*
|
||||
/src/state/fsm/template
|
||||
/pkg/state/fsm/template
|
||||
/cmd/cli/cli
|
||||
|
||||
108
cmd/cli/main.go
Normal file
108
cmd/cli/main.go
Normal file
@@ -0,0 +1,108 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"gitea/price-is-wrong/pkg/lib/db"
|
||||
"gitea/price-is-wrong/pkg/state/lobby"
|
||||
"gitea/price-is-wrong/pkg/state/priceiswrong"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"os/signal"
|
||||
"strconv"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
func main() {
|
||||
ctx, can := signal.NotifyContext(context.Background(), syscall.SIGINT)
|
||||
defer can()
|
||||
|
||||
if err := run(
|
||||
ctx,
|
||||
envOr("GAME_DB_DRIVER", "sqlite"),
|
||||
envOr("GAME_DB_CONN", "/tmp/game.db"),
|
||||
envOr("GAME_SESSION", "session"),
|
||||
os.Args[1:],
|
||||
); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
func envOr(k, v string) string {
|
||||
if v := os.Getenv(k); v != "" {
|
||||
return v
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
func run(ctx context.Context, dbDriver, dbConn, session string, args []string) error {
|
||||
b, err := db.New(ctx, dbDriver, dbConn)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer b.Close()
|
||||
ctx = db.Inject(ctx, b)
|
||||
|
||||
l, err := lobby.Open(ctx, session)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !l.Closed {
|
||||
if len(args) == 0 {
|
||||
log.Printf("lobby | %+v", *l)
|
||||
return nil
|
||||
}
|
||||
switch args[0] {
|
||||
case "join":
|
||||
return l.Join(ctx, mustParseInt(args[1]))
|
||||
case "leave":
|
||||
return l.Leave(ctx, mustParseInt(args[1]))
|
||||
case "close":
|
||||
return l.Close(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
p, err := priceiswrong.Open(ctx, session)
|
||||
if err != nil {
|
||||
return err
|
||||
} else if len(p.Contestants) > 0 {
|
||||
} else if err := p.SetPlayers(ctx, l.Players); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(args) == 0 {
|
||||
log.Printf("priceiswrong | %+v", *p)
|
||||
return nil
|
||||
}
|
||||
|
||||
defer func() {
|
||||
log.Printf("priceiswrong | %+v", *p)
|
||||
}()
|
||||
switch args[0] {
|
||||
case "guess":
|
||||
return p.Guess(ctx, mustParseInt(args[1]), args[2])
|
||||
case "score":
|
||||
return p.Score(ctx, mustParseInt(args[1]), mustParseInt(args[2]))
|
||||
case "sethost":
|
||||
return p.SetHost(ctx, mustParseInt(args[1]))
|
||||
case "setitem":
|
||||
return p.SetItem(ctx, args[1], args[2], args[3], args[4])
|
||||
case "setplayers":
|
||||
ids := []int{}
|
||||
for _, id := range args[1:] {
|
||||
ids = append(ids, mustParseInt(id))
|
||||
}
|
||||
return p.SetPlayers(ctx, ids)
|
||||
}
|
||||
|
||||
return io.EOF
|
||||
}
|
||||
|
||||
func mustParseInt(a string) int {
|
||||
i, err := strconv.Atoi(a)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return i
|
||||
}
|
||||
@@ -3,7 +3,7 @@ package db_test
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"gitea/price-is-wrong/src/lib/db"
|
||||
"gitea/price-is-wrong/pkg/lib/db"
|
||||
"testing"
|
||||
)
|
||||
|
||||
21
pkg/lib/db/new.go
Normal file
21
pkg/lib/db/new.go
Normal file
@@ -0,0 +1,21 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
|
||||
_ "github.com/glebarez/sqlite"
|
||||
)
|
||||
|
||||
func New(ctx context.Context, driver, conn string) (*sql.DB, error) {
|
||||
db, err := sql.Open(driver, conn)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := db.PingContext(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return db, nil
|
||||
}
|
||||
@@ -2,11 +2,10 @@ package lib
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"path"
|
||||
"testing"
|
||||
|
||||
"gitea/price-is-wrong/src/lib/db"
|
||||
"gitea/price-is-wrong/pkg/lib/db"
|
||||
|
||||
_ "github.com/glebarez/sqlite"
|
||||
)
|
||||
@@ -15,7 +14,7 @@ func NewTestCtx(t *testing.T) context.Context {
|
||||
d := t.TempDir()
|
||||
p := path.Join(d, "db.db")
|
||||
t.Logf("test db at %s", p)
|
||||
b, err := sql.Open("sqlite", p)
|
||||
b, err := db.New(context.Background(), "sqlite", p)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -2,8 +2,8 @@ package lib_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"gitea/price-is-wrong/src/lib"
|
||||
"gitea/price-is-wrong/src/lib/db"
|
||||
"gitea/price-is-wrong/pkg/lib"
|
||||
"gitea/price-is-wrong/pkg/lib/db"
|
||||
"sync"
|
||||
"testing"
|
||||
)
|
||||
@@ -1,3 +1,3 @@
|
||||
package fsm
|
||||
package state
|
||||
|
||||
//go:generate cp -r ./.template ./template
|
||||
@@ -2,8 +2,8 @@ package lobby
|
||||
|
||||
import (
|
||||
"context"
|
||||
"gitea/price-is-wrong/src/lib/db"
|
||||
lobby "gitea/price-is-wrong/src/state/fsm/lobby/internal"
|
||||
"gitea/price-is-wrong/pkg/lib/db"
|
||||
lobby "gitea/price-is-wrong/pkg/state/lobby/internal"
|
||||
"slices"
|
||||
)
|
||||
|
||||
@@ -15,6 +15,10 @@ func (l *Lobby) Leave(ctx context.Context, id int) error {
|
||||
return l.upsertEvent(ctx, &lobby.PlayerLeave{ID: id})
|
||||
}
|
||||
|
||||
func (l *Lobby) Close(ctx context.Context) error {
|
||||
return l.upsertEvent(ctx, &lobby.Close{})
|
||||
}
|
||||
|
||||
func (l *Lobby) upsertEvent(ctx context.Context, e lobby.Event) error {
|
||||
if err := upsertEvent(ctx, l.id, e); err != nil {
|
||||
return err
|
||||
60
pkg/state/lobby/events.go
Normal file
60
pkg/state/lobby/events.go
Normal file
@@ -0,0 +1,60 @@
|
||||
package lobby
|
||||
|
||||
import (
|
||||
"context"
|
||||
"gitea/price-is-wrong/pkg/lib/db"
|
||||
lobby "gitea/price-is-wrong/pkg/state/lobby/internal"
|
||||
"slices"
|
||||
)
|
||||
|
||||
func (l *Lobby) Join(ctx context.Context, id int) error {
|
||||
return l.upsertEvent(ctx, &lobby.PlayerJoin{ID: id})
|
||||
}
|
||||
|
||||
func (l *Lobby) Leave(ctx context.Context, id int) error {
|
||||
return l.upsertEvent(ctx, &lobby.PlayerLeave{ID: id})
|
||||
}
|
||||
|
||||
func (l *Lobby) Close(ctx context.Context) error {
|
||||
return l.upsertEvent(ctx, &lobby.Close{})
|
||||
}
|
||||
|
||||
func (l *Lobby) upsertEvent(ctx context.Context, e lobby.Event) error {
|
||||
if err := upsertEvent(ctx, l.id, e); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
l2, err := openID(ctx, l.id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
*l = *l2
|
||||
return nil
|
||||
}
|
||||
|
||||
func upsertEvent(ctx context.Context, lobbyID int, e lobby.Event) error {
|
||||
b, err := lobby.MarshalEvent(e)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = db.From(ctx).ExecContext(ctx, `
|
||||
INSERT INTO lobby_events (lobby_id, payload) VALUES (?, ?)
|
||||
`, lobbyID, b)
|
||||
return err
|
||||
}
|
||||
|
||||
func (l *Lobby) apply(e lobby.Event) error {
|
||||
switch e := e.(type) {
|
||||
case *lobby.PlayerJoin:
|
||||
if !slices.Contains(l.Players, e.ID) {
|
||||
l.Players = append(l.Players, e.ID)
|
||||
}
|
||||
case *lobby.PlayerLeave:
|
||||
l.Players = slices.DeleteFunc(l.Players, func(id int) bool { return id == e.ID })
|
||||
case *lobby.Close:
|
||||
l.Closed = true
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -2,7 +2,7 @@ package lobby
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"gitea/price-is-wrong/src/lib/event"
|
||||
"gitea/price-is-wrong/pkg/lib/event"
|
||||
)
|
||||
|
||||
type Event interface{}
|
||||
@@ -15,10 +15,14 @@ type PlayerLeave struct {
|
||||
ID int
|
||||
}
|
||||
|
||||
type Close struct {
|
||||
}
|
||||
|
||||
func ParseEvent(b []byte) (Event, error) {
|
||||
typesToPointers := map[string]any{
|
||||
"*lobby.PlayerJoin": &PlayerJoin{},
|
||||
"*lobby.PlayerLeave": &PlayerLeave{},
|
||||
"*lobby.Close": &Close{},
|
||||
}
|
||||
t, err := event.Parse(b, typesToPointers)
|
||||
return typesToPointers[t], err
|
||||
@@ -2,7 +2,7 @@ package lobby_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
lobby "gitea/price-is-wrong/src/state/fsm/lobby/internal"
|
||||
lobby "gitea/price-is-wrong/pkg/state/lobby/internal"
|
||||
"testing"
|
||||
)
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
package lobby_test
|
||||
|
||||
import (
|
||||
"gitea/price-is-wrong/src/lib"
|
||||
"gitea/price-is-wrong/src/state/fsm/lobby"
|
||||
"gitea/price-is-wrong/pkg/lib"
|
||||
"gitea/price-is-wrong/pkg/state/lobby"
|
||||
"testing"
|
||||
)
|
||||
|
||||
@@ -44,4 +44,10 @@ func TestOpen(t *testing.T) {
|
||||
} else if len(l.Players) != 2 {
|
||||
t.Errorf("second join yielded wrong players: %+v", l.Players)
|
||||
}
|
||||
|
||||
if err := l.Close(ctx); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if !l.Closed {
|
||||
t.Error(l.Closed)
|
||||
}
|
||||
}
|
||||
@@ -4,13 +4,14 @@ import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"gitea/price-is-wrong/src/lib/db"
|
||||
lobby "gitea/price-is-wrong/src/state/fsm/lobby/internal"
|
||||
"gitea/price-is-wrong/pkg/lib/db"
|
||||
lobby "gitea/price-is-wrong/pkg/state/lobby/internal"
|
||||
)
|
||||
|
||||
type Lobby struct {
|
||||
id int
|
||||
Players []int
|
||||
Closed bool
|
||||
}
|
||||
|
||||
func Open(ctx context.Context, name string) (*Lobby, error) {
|
||||
100
pkg/state/priceiswrong/events.go
Normal file
100
pkg/state/priceiswrong/events.go
Normal file
@@ -0,0 +1,100 @@
|
||||
package priceiswrong
|
||||
|
||||
import (
|
||||
"context"
|
||||
"gitea/price-is-wrong/pkg/lib/db"
|
||||
priceiswrong "gitea/price-is-wrong/pkg/state/priceiswrong/internal"
|
||||
"html"
|
||||
"math/rand"
|
||||
)
|
||||
|
||||
func (p *PriceIsWrong) apply(e priceiswrong.Event) error {
|
||||
switch e := e.(type) {
|
||||
case *priceiswrong.Players:
|
||||
p.Contestants = p.Contestants[:0]
|
||||
for _, id := range e.IDs {
|
||||
p.Contestants = append(p.Contestants, Player{ID: id})
|
||||
}
|
||||
if n := len(p.Contestants); n > 0 {
|
||||
p.Host = p.Contestants[rand.Intn(n)].ID
|
||||
}
|
||||
case *priceiswrong.Host:
|
||||
p.Host = e.ID
|
||||
case *priceiswrong.Score:
|
||||
for i := range p.Contestants {
|
||||
if p.Contestants[i].ID == e.ID {
|
||||
p.Contestants[i].Score += e.Score
|
||||
}
|
||||
}
|
||||
case *priceiswrong.Item:
|
||||
for i := range p.Contestants {
|
||||
p.Contestants[i].Guess = ""
|
||||
}
|
||||
p.Item.ImageURL = e.ImageURL
|
||||
p.Item.Title = html.EscapeString(e.Title)
|
||||
p.Item.Description = html.EscapeString(e.Description)
|
||||
p.Item.Value = html.EscapeString(e.Value)
|
||||
case *priceiswrong.Guess:
|
||||
for i := range p.Contestants {
|
||||
if p.Contestants[i].ID == e.ID {
|
||||
p.Contestants[i].Guess = html.EscapeString(e.Guess)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *PriceIsWrong) SetPlayers(ctx context.Context, ids []int) error {
|
||||
return p.upsertEvent(ctx, &priceiswrong.Players{IDs: ids})
|
||||
}
|
||||
|
||||
func (p *PriceIsWrong) SetHost(ctx context.Context, id int) error {
|
||||
return p.upsertEvent(ctx, &priceiswrong.Host{ID: id})
|
||||
}
|
||||
|
||||
func (p *PriceIsWrong) Score(ctx context.Context, id, score int) error {
|
||||
return p.upsertEvent(ctx, &priceiswrong.Score{ID: id, Score: score})
|
||||
}
|
||||
|
||||
func (p *PriceIsWrong) SetItem(ctx context.Context, imageURL, title, description, value string) error {
|
||||
return p.upsertEvent(ctx, &priceiswrong.Item{
|
||||
ImageURL: imageURL,
|
||||
Title: title,
|
||||
Description: description,
|
||||
Value: value,
|
||||
})
|
||||
}
|
||||
|
||||
func (p *PriceIsWrong) Guess(ctx context.Context, id int, guess string) error {
|
||||
return p.upsertEvent(ctx, &priceiswrong.Guess{
|
||||
ID: id,
|
||||
Guess: guess,
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
func (p *PriceIsWrong) upsertEvent(ctx context.Context, e priceiswrong.Event) error {
|
||||
if err := upsertEvent(ctx, p.id, e); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
p2, err := openID(ctx, p.id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
*p = *p2
|
||||
return nil
|
||||
}
|
||||
|
||||
func upsertEvent(ctx context.Context, priceIsWrongID int, e priceiswrong.Event) error {
|
||||
b, err := priceiswrong.MarshalEvent(e)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = db.From(ctx).ExecContext(ctx, `
|
||||
INSERT INTO priceiswrong_events (priceiswrong_id, payload) VALUES (?, ?)
|
||||
`, priceIsWrongID, b)
|
||||
return err
|
||||
}
|
||||
49
pkg/state/priceiswrong/internal/event.go
Normal file
49
pkg/state/priceiswrong/internal/event.go
Normal file
@@ -0,0 +1,49 @@
|
||||
package priceiswrong
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"gitea/price-is-wrong/pkg/lib/event"
|
||||
)
|
||||
|
||||
type Event interface{}
|
||||
|
||||
type Players struct {
|
||||
IDs []int
|
||||
}
|
||||
|
||||
type Host struct {
|
||||
ID int
|
||||
}
|
||||
|
||||
type Score struct {
|
||||
ID int
|
||||
Score int
|
||||
}
|
||||
|
||||
type Item struct {
|
||||
ImageURL string
|
||||
Title string
|
||||
Description string
|
||||
Value string
|
||||
}
|
||||
|
||||
type Guess struct {
|
||||
ID int
|
||||
Guess string
|
||||
}
|
||||
|
||||
func ParseEvent(b []byte) (Event, error) {
|
||||
typesToPointers := map[string]any{
|
||||
"*priceiswrong.Players": &Players{},
|
||||
"*priceiswrong.Host": &Host{},
|
||||
"*priceiswrong.Score": &Score{},
|
||||
"*priceiswrong.Item": &Item{},
|
||||
"*priceiswrong.Guess": &Guess{},
|
||||
}
|
||||
t, err := event.Parse(b, typesToPointers)
|
||||
return typesToPointers[t], err
|
||||
}
|
||||
|
||||
func MarshalEvent(e Event) ([]byte, error) {
|
||||
return event.Serialize(fmt.Sprintf("%T", e), e)
|
||||
}
|
||||
@@ -4,18 +4,21 @@ import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"gitea/price-is-wrong/src/lib/db"
|
||||
priceiswrong "gitea/price-is-wrong/src/state/fsm/priceiswrong/internal"
|
||||
"gitea/price-is-wrong/pkg/lib/db"
|
||||
priceiswrong "gitea/price-is-wrong/pkg/state/priceiswrong/internal"
|
||||
)
|
||||
|
||||
type PriceIsWrong struct {
|
||||
id int
|
||||
Host int
|
||||
Contestants []Player
|
||||
Item priceiswrong.Item
|
||||
}
|
||||
|
||||
type Player struct {
|
||||
ID int
|
||||
ID int
|
||||
Score int
|
||||
Guess string
|
||||
}
|
||||
|
||||
func Open(ctx context.Context, name string) (*PriceIsWrong, error) {
|
||||
62
pkg/state/priceiswrong/priceiswrong_test.go
Normal file
62
pkg/state/priceiswrong/priceiswrong_test.go
Normal file
@@ -0,0 +1,62 @@
|
||||
package priceiswrong_test
|
||||
|
||||
import (
|
||||
"gitea/price-is-wrong/pkg/lib"
|
||||
"gitea/price-is-wrong/pkg/state/priceiswrong"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestPriceIsWrong(t *testing.T) {
|
||||
ctx := lib.NewTestCtx(t)
|
||||
|
||||
p, err := priceiswrong.Open(ctx, "name")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if err := p.SetPlayers(ctx, []int{4, 5, 6}); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if len(p.Contestants) != 3 {
|
||||
t.Errorf("bad contestants: %+v", p.Contestants)
|
||||
} else if p.Host < 4 || p.Host > 6 {
|
||||
t.Errorf("bad host: %+v", p.Host)
|
||||
}
|
||||
|
||||
if err := p.SetHost(ctx, 5); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if p.Host != 5 {
|
||||
t.Errorf("bad host: %+v", p.Host)
|
||||
}
|
||||
|
||||
if err := p.Score(ctx, 4, 9); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if p.Contestants[0].Score != 9 {
|
||||
t.Errorf("players changed order or didnt give 4 its points: %+v", p.Contestants)
|
||||
} else if err := p.Score(ctx, 4, 9); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if p.Contestants[0].Score != 18 {
|
||||
t.Errorf("players changed order or didnt give 4 more points: %+v", p.Contestants)
|
||||
} else if p.Contestants[1].Score != 0 {
|
||||
t.Errorf("gave someone else points: %+v", p.Contestants)
|
||||
}
|
||||
|
||||
if err := p.Guess(ctx, 4, "guess"); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if p.Contestants[0].Guess != "guess" {
|
||||
t.Errorf("guess didnt persist: %+v", p.Contestants)
|
||||
}
|
||||
|
||||
if err := p.SetItem(ctx, "a", "b", "c", "d"); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if p.Contestants[0].Guess != "" {
|
||||
t.Errorf("set item didnt clear guesses")
|
||||
} else if err := p.SetItem(ctx, "imageURL", "title", "description", "d"); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if p.Item.ImageURL != "imageURL" {
|
||||
t.Errorf("wrong image url: %s", p.Item.ImageURL)
|
||||
} else if p.Item.Title != "title" {
|
||||
t.Errorf("wrong title: %s", p.Item.Title)
|
||||
} else if p.Item.Description != "description" {
|
||||
t.Errorf("wrong description: %s", p.Item.Description)
|
||||
}
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
package template
|
||||
|
||||
import (
|
||||
"context"
|
||||
"gitea/price-is-wrong/src/lib/db"
|
||||
template "gitea/price-is-wrong/src/state/fsm/template/internal"
|
||||
"io"
|
||||
)
|
||||
|
||||
func (t *Template) TODO(ctx context.Context, id int) error {
|
||||
return io.EOF
|
||||
}
|
||||
|
||||
func (t *Template) upsertEvent(ctx context.Context, e template.Event) error {
|
||||
if err := upsertEvent(ctx, p.id, e); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
p2, err := openID(ctx, p.id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
*p = *p2
|
||||
return nil
|
||||
}
|
||||
|
||||
func upsertEvent(ctx context.Context, templateID int, e template.Event) error {
|
||||
b, err := template.MarshalEvent(e)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = db.From(ctx).ExecContext(ctx, `
|
||||
INSERT INTO template_events (template_id, payload) VALUES (?, ?)
|
||||
`, templateID, b)
|
||||
return err
|
||||
}
|
||||
|
||||
func (t *Template) apply(e template.Event) error {
|
||||
switch e := e.(type) {
|
||||
case *int:
|
||||
_ = e
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
package template
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"gitea/price-is-wrong/src/lib/event"
|
||||
)
|
||||
|
||||
type Event interface{}
|
||||
|
||||
func ParseEvent(b []byte) (Event, error) {
|
||||
typesToPointers := map[string]any{
|
||||
"*any": &b,
|
||||
}
|
||||
t, err := event.Parse(b, typesToPointers)
|
||||
return typesToPointers[t], err
|
||||
}
|
||||
|
||||
func MarshalEvent(e Event) ([]byte, error) {
|
||||
return event.Serialize(fmt.Sprintf("%T", e), e)
|
||||
}
|
||||
@@ -1,115 +0,0 @@
|
||||
package template
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"gitea/price-is-wrong/src/lib/db"
|
||||
template "gitea/price-is-wrong/src/state/fsm/template/internal"
|
||||
)
|
||||
|
||||
type Template struct {
|
||||
id int
|
||||
Host int
|
||||
Contestants Player
|
||||
}
|
||||
|
||||
type Player struct {
|
||||
ID int
|
||||
}
|
||||
|
||||
func Open(ctx context.Context, name string) (*Template, error) {
|
||||
if err := initialize(ctx); err != nil {
|
||||
return nil, fmt.Errorf("failed to initialize template: %w", err)
|
||||
}
|
||||
|
||||
if result, err := open(ctx, name); err != nil {
|
||||
return nil, err
|
||||
} else if result == nil {
|
||||
if err := create(ctx, name); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return mustOpen(ctx, name)
|
||||
}
|
||||
|
||||
func mustOpen(ctx context.Context, name string) (*Template, error) {
|
||||
result, err := open(ctx, name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if result == nil {
|
||||
return nil, fmt.Errorf("no template found with name %s", name)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func open(ctx context.Context, name string) (*Template, error) {
|
||||
row := db.From(ctx).QueryRow(`SELECT id FROM templates WHERE name=?`, name)
|
||||
if err := row.Err(); err != nil {
|
||||
return nil, fmt.Errorf("no template found with name %s", name)
|
||||
}
|
||||
|
||||
var id sql.NullInt32
|
||||
if err := row.Scan(&id); err == sql.ErrNoRows || !id.Valid {
|
||||
return nil, nil
|
||||
} else if err != nil {
|
||||
return nil, fmt.Errorf("failed to scan id from templates: %w", err)
|
||||
}
|
||||
|
||||
return openID(ctx, int(id.Int32))
|
||||
}
|
||||
|
||||
func openID(ctx context.Context, id int) (*Template, error) {
|
||||
rows, err := db.From(ctx).QueryContext(ctx, `
|
||||
SELECT payload
|
||||
FROM template_events
|
||||
WHERE template_events.template_id=?
|
||||
ORDER BY id
|
||||
`, id)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to query event payloads for id %d: %w", id, err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
result := Template{id: id}
|
||||
for rows.Next() {
|
||||
var b []byte
|
||||
if err := rows.Scan(&b); err != nil {
|
||||
return nil, fmt.Errorf("failed to scan event: %w", err)
|
||||
}
|
||||
event, err := template.ParseEvent(b)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse event: %w", err)
|
||||
}
|
||||
if err := result.apply(event); err != nil {
|
||||
return nil, fmt.Errorf("failed to apply event %s: %w", b, err)
|
||||
}
|
||||
}
|
||||
|
||||
return &result, rows.Err()
|
||||
}
|
||||
|
||||
func create(ctx context.Context, name string) error {
|
||||
_, err := db.From(ctx).ExecContext(ctx, `
|
||||
INSERT INTO templates (name) VALUES (?)
|
||||
`, name)
|
||||
return err
|
||||
}
|
||||
|
||||
func initialize(ctx context.Context) error {
|
||||
_, err := db.From(ctx).ExecContext(ctx, `
|
||||
CREATE TABLE IF NOT EXISTS templates (
|
||||
id INTEGER PRIMARY KEY,
|
||||
name TEXT
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS template_events (
|
||||
id INTEGER PRIMARY KEY,
|
||||
template_id NUMBER,
|
||||
payload TEXT,
|
||||
FOREIGN KEY (template_id) REFERENCES templates (id)
|
||||
);
|
||||
`)
|
||||
return err
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
package priceiswrong
|
||||
|
||||
import (
|
||||
"context"
|
||||
"gitea/price-is-wrong/src/lib/db"
|
||||
priceiswrong "gitea/price-is-wrong/src/state/fsm/priceiswrong/internal"
|
||||
"math/rand"
|
||||
)
|
||||
|
||||
func (p *PriceIsWrong) Players(ctx context.Context, ids []int) error {
|
||||
return p.upsertEvent(ctx, &priceiswrong.Players{IDs: ids})
|
||||
}
|
||||
|
||||
func (p *PriceIsWrong) upsertEvent(ctx context.Context, e priceiswrong.Event) error {
|
||||
if err := upsertEvent(ctx, p.id, e); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
p2, err := openID(ctx, p.id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
*p = *p2
|
||||
return nil
|
||||
}
|
||||
|
||||
func upsertEvent(ctx context.Context, priceIsWrongID int, e priceiswrong.Event) error {
|
||||
b, err := priceiswrong.MarshalEvent(e)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = db.From(ctx).ExecContext(ctx, `
|
||||
INSERT INTO priceiswrong_events (priceiswrong_id, payload) VALUES (?, ?)
|
||||
`, priceIsWrongID, b)
|
||||
return err
|
||||
}
|
||||
|
||||
func (p *PriceIsWrong) apply(e priceiswrong.Event) error {
|
||||
switch e := e.(type) {
|
||||
case *priceiswrong.Players:
|
||||
p.Contestants = p.Contestants[:0]
|
||||
for _, id := range e.IDs {
|
||||
p.Contestants = append(p.Contestants, Player{ID: id})
|
||||
}
|
||||
if n := len(p.Contestants); n > 0 {
|
||||
p.Host = p.Contestants[rand.Intn(n)].ID
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
package priceiswrong
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"gitea/price-is-wrong/src/lib/event"
|
||||
)
|
||||
|
||||
type Event interface{}
|
||||
|
||||
type Players struct {
|
||||
IDs []int
|
||||
}
|
||||
|
||||
func ParseEvent(b []byte) (Event, error) {
|
||||
typesToPointers := map[string]any{
|
||||
"*priceiswrong.Players": &Players{},
|
||||
}
|
||||
t, err := event.Parse(b, typesToPointers)
|
||||
return typesToPointers[t], err
|
||||
}
|
||||
|
||||
func MarshalEvent(e Event) ([]byte, error) {
|
||||
return event.Serialize(fmt.Sprintf("%T", e), e)
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
package priceiswrong_test
|
||||
|
||||
import (
|
||||
"gitea/price-is-wrong/src/lib"
|
||||
"gitea/price-is-wrong/src/state/fsm/priceiswrong"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestPriceIsWrong(t *testing.T) {
|
||||
ctx := lib.NewTestCtx(t)
|
||||
|
||||
p, err := priceiswrong.Open(ctx, "name")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if err := p.Players(ctx, []int{4, 5, 6}); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if len(p.Contestants) != 3 {
|
||||
t.Errorf("bad contestants: %+v", p.Contestants)
|
||||
} else if p.Host < 4 || p.Host > 6 {
|
||||
t.Errorf("bad host: %+v", p.Host)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user