out/cmd/server/games.go

275 lines
5.8 KiB
Go

package main
import (
"context"
"database/sql"
"encoding/json"
"fmt"
"io"
"slices"
"time"
"github.com/google/uuid"
)
type Games struct {
db DB
}
func NewGames(ctx context.Context, db DB) (Games, error) {
err := db.Exec(ctx, `
CREATE TABLE IF NOT EXISTS users (
uuid TEXT,
updated DATETIME,
name TEXT
);
CREATE TABLE IF NOT EXISTS games (
uuid TEXT,
updated DATETIME,
name TEXT,
completed DATETIME
);
CREATE TABLE IF NOT EXISTS players (
user_uuid TEXT,
game_uuid TEXT,
updated DATETIME
);
CREATE TABLE IF NOT EXISTS events (
game_uuid TEXT,
timestamp DATETIME,
payload TEXT
);
`)
return Games{db: db}, err
}
func (games Games) GamesForUser(ctx context.Context, id string) ([]string, error) {
result := []string{}
err := games.db.Query(ctx, func(rows *sql.Rows) error {
var game string
err := rows.Scan(&game)
result = append(result, game)
return err
}, `
SELECT players.game_uuid
FROM players
WHERE players.user_uuid=?
`, id)
return result, err
}
func (games Games) GameByName(ctx context.Context, uid, name string) (string, error) {
var result string
err := games.db.Query(ctx, func(rows *sql.Rows) error {
return rows.Scan(&result)
}, `
SELECT
players.game_uuid
FROM
players
JOIN games ON players.game_uuid=games.uuid
WHERE players.user_uuid=? AND games.name=?
ORDER BY games.updated DESC
LIMIT 1
`, uid, name)
return result, err
}
type (
GameState struct {
Completed time.Time
Players map[string]PlayerState
}
PlayerState struct {
Kills []Kill
KillWords KillWords
}
Kill struct {
Timestamp time.Time
Victim string
Public bool
}
KillWords struct {
Global string
Assigned time.Time
Assignee string
Assignment Assignment
}
Assignment struct {
Public []string
Private []string
}
EventType int
EventPlayerJoin struct {
ID string
}
EventPlayerLeave struct {
ID string
}
EventGameComplete struct{}
EventAssignmentRotation struct {
Killer string
Killed string
KillWord string
KillWords map[string]KillWords
}
)
const (
PlayerJoin EventType = iota + 1
PlayerLeave
GameComplete
AssignmentRotation
)
func (games Games) GameState(ctx context.Context, id string) (GameState, error) {
result := GameState{Players: map[string]PlayerState{}}
err := games.db.Query(ctx, func(rows *sql.Rows) error {
var timestamp time.Time
var payload []byte
if err := rows.Scan(&timestamp, &payload); err != nil {
return err
}
var peek struct {
Type EventType
}
if err := json.Unmarshal(payload, &peek); err != nil {
return err
}
switch peek.Type {
case PlayerJoin:
var playerJoin EventPlayerJoin
if err := json.Unmarshal(payload, &playerJoin); err != nil {
return err
}
result.Players[playerJoin.ID] = PlayerState{}
return nil
case PlayerLeave:
var playerLeave EventPlayerLeave
if err := json.Unmarshal(payload, &playerLeave); err != nil {
return err
}
delete(result.Players, playerLeave.ID)
return nil
case GameComplete:
var gameComplete EventGameComplete
if err := json.Unmarshal(payload, &gameComplete); err != nil {
return err
}
result.Completed = timestamp
return nil
case AssignmentRotation:
var assignmentRotation EventAssignmentRotation
if err := json.Unmarshal(payload, &assignmentRotation); err != nil {
return err
}
if killer, ok := result.Players[assignmentRotation.Killer]; !ok {
} else if victim, ok := result.Players[assignmentRotation.Killed]; !ok {
} else {
killer.Kills = append(killer.Kills, Kill{
Timestamp: timestamp,
Victim: assignmentRotation.Killed,
Public: slices.Contains(victim.KillWords.Assignment.Public, assignmentRotation.KillWord),
})
result.Players[assignmentRotation.Killer] = killer
}
for k, v := range result.Players {
v.KillWords = KillWords{}
result.Players[k] = v
}
for k, v := range assignmentRotation.KillWords {
player := result.Players[k]
player.KillWords = v
result.Players[k] = player
}
return nil
default:
return fmt.Errorf("unknown event type %d: %s", peek.Type, payload)
}
return nil
}, `
SELECT timestamp, payload
FROM events
WHERE game_uuid=?
`, id)
return result, err
}
func (games Games) CreateGame(ctx context.Context, name string) (string, error) {
var exists string
if err := games.db.Query(ctx,
func(rows *sql.Rows) error {
return rows.Scan(&exists)
},
`
SELECT uuid
FROM games
WHERE name=? AND completed IS NULL
`, name); err != nil {
return "", err
} else if exists != "" {
return exists, nil
}
id := uuid.New().String()
return id, games.db.Exec(ctx, `
INSERT INTO games (
uuid,
updated,
name
) VALUES (?, ?, ?)
`, id, time.Now(), name)
}
func (games Games) CreateEventPlayerJoin(ctx context.Context, id string, player string) error {
return games.createEvent(ctx, id, EventPlayerJoin{ID: player})
}
func (games Games) CreateEventPlayerLeave(ctx context.Context, id string, player string) error {
return games.createEvent(ctx, id, EventPlayerLeave{ID: player})
}
func (games Games) CreateEventAssignmentRotation(ctx context.Context, id string, killer, killed, killWord string) error {
// TODO gather current assignees
// TODO get victim's target
// TODO assign victim's target to killer
// TODO randomize everyone else so not the same as before AND not self
return io.EOF
//return games.createEvent(ctx, id, v)
}
func (games Games) CreateEventGameComplete(ctx context.Context, id string) error {
return games.createEvent(ctx, id, EventGameComplete{})
}
func (games Games) createEvent(ctx context.Context, id string, v any) error {
payload, err := json.Marshal(v)
if err != nil {
panic(err)
}
return games.db.Exec(ctx, `
INSERT INTO events (
game_uuid,
timestamp,
payload
) VALUES (?, ?, ?)
`, id, time.Now(), payload)
}