845 lines
19 KiB
Go
845 lines
19 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
_ "embed"
|
|
"encoding/json"
|
|
"fmt"
|
|
"maps"
|
|
"math/rand"
|
|
"slices"
|
|
"strings"
|
|
"time"
|
|
"unicode"
|
|
|
|
"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) UpdateUserName(ctx context.Context, id, name string) error {
|
|
var n int
|
|
if err := games.db.Query(ctx, func(rows *sql.Rows) error {
|
|
return rows.Scan(&n)
|
|
}, `SELECT COUNT(uuid) FROM users WHERE uuid=?`, id); err != nil {
|
|
return err
|
|
}
|
|
if n > 0 {
|
|
return games.db.Exec(ctx, `UPDATE users SET name=? WHERE uuid=?`, name, id)
|
|
}
|
|
return games.db.Exec(ctx, `INSERT INTO users (uuid, name) VALUES (?, ?)`, id, name)
|
|
}
|
|
|
|
//go:embed adjectives.txt
|
|
var namesAdjectives string
|
|
|
|
//go:embed animals.txt
|
|
var namesAnimals string
|
|
|
|
func (games Games) UserName(ctx context.Context, id string) (string, error) {
|
|
result := ""
|
|
err := games.db.Query(ctx, func(rows *sql.Rows) error {
|
|
return rows.Scan(&result)
|
|
}, `
|
|
SELECT users.name
|
|
FROM users
|
|
WHERE users.uuid=?
|
|
`, id)
|
|
if result == "" {
|
|
adjectives := strings.Fields(namesAdjectives)
|
|
animals := strings.Split(namesAnimals, "\n")
|
|
animals = slices.DeleteFunc(animals, func(s string) bool { return s == "" })
|
|
name := strings.Title(fmt.Sprintf("%s %s", adjectives[rand.Intn(len(adjectives))], animals[rand.Intn(len(animals))]))
|
|
if err := games.UpdateUserName(ctx, id, name); err != nil {
|
|
return "", err
|
|
}
|
|
return games.UserName(ctx, id)
|
|
}
|
|
return result, err
|
|
}
|
|
|
|
func (games Games) UserByName(ctx context.Context, gid, name string) (string, error) {
|
|
result := ""
|
|
err := games.db.Query(ctx, func(rows *sql.Rows) error {
|
|
return rows.Scan(&result)
|
|
}, `
|
|
SELECT users.uuid
|
|
FROM players
|
|
JOIN users ON players.user_uuid=users.uuid
|
|
WHERE players.game_uuid=? AND users.name=?
|
|
`, gid, name)
|
|
return result, err
|
|
}
|
|
|
|
func (a KillWord) Empty() bool {
|
|
return a == (KillWord{})
|
|
}
|
|
|
|
func (a Assignment) Empty() bool {
|
|
return len(a.Public) == 0 && len(a.Private) == 0
|
|
}
|
|
|
|
func (s PlayerState) Empty() bool {
|
|
return len(s.Kills) == 0 && s.KillWords.Empty()
|
|
}
|
|
|
|
func (s PlayerState) Points() int {
|
|
points := 0
|
|
for _, kill := range s.Kills {
|
|
points += kill.KillWord.Points
|
|
}
|
|
return points
|
|
}
|
|
|
|
func (games Games) GameName(ctx context.Context, id string) (string, error) {
|
|
var result string
|
|
err := games.db.Query(ctx, func(rows *sql.Rows) error {
|
|
return rows.Scan(&result)
|
|
}, `
|
|
SELECT
|
|
games.name
|
|
FROM
|
|
games
|
|
WHERE games.uuid=?
|
|
ORDER BY games.updated DESC
|
|
LIMIT 1
|
|
`, id)
|
|
return result, err
|
|
}
|
|
|
|
func (games Games) GameByName(ctx context.Context, name string) (string, error) {
|
|
var result string
|
|
err := games.db.Query(ctx, func(rows *sql.Rows) error {
|
|
return rows.Scan(&result)
|
|
}, `
|
|
SELECT
|
|
games.uuid
|
|
FROM
|
|
games
|
|
WHERE games.name=?
|
|
ORDER BY games.updated DESC
|
|
LIMIT 1
|
|
`, name)
|
|
return result, err
|
|
}
|
|
|
|
type (
|
|
GameState struct {
|
|
ID string
|
|
Started bool
|
|
Completed time.Time
|
|
|
|
Players map[string]PlayerState
|
|
|
|
Trial Trial
|
|
}
|
|
|
|
PlayerState struct {
|
|
Kills []Kill
|
|
KillWords KillWords
|
|
}
|
|
|
|
Kill struct {
|
|
Timestamp time.Time
|
|
Victim string
|
|
KillWord KillWord
|
|
}
|
|
|
|
KillWords struct {
|
|
Codename Codename
|
|
|
|
Assigned time.Time
|
|
Assignee string
|
|
|
|
Assignment Assignment
|
|
}
|
|
|
|
Codename struct {
|
|
KillWord KillWord
|
|
Consumed bool
|
|
}
|
|
|
|
Assignment struct {
|
|
Public []KillWord
|
|
Private []KillWord
|
|
}
|
|
|
|
KillWord struct {
|
|
Word string
|
|
Points int
|
|
}
|
|
|
|
Trial struct {
|
|
Prosecutor string
|
|
Defendant string
|
|
Word string
|
|
}
|
|
|
|
EventType int
|
|
|
|
EventPlayerJoin struct {
|
|
Type EventType
|
|
Timestamp time.Time
|
|
ID string
|
|
}
|
|
EventPlayerLeave struct {
|
|
Type EventType
|
|
Timestamp time.Time
|
|
ID string
|
|
}
|
|
EventGameComplete struct {
|
|
Type EventType
|
|
Timestamp time.Time
|
|
}
|
|
EventAssignmentRotation struct {
|
|
Type EventType
|
|
Timestamp time.Time
|
|
Killer string
|
|
Victim string
|
|
KillWord KillWord
|
|
AllKillWords AllKillWords
|
|
}
|
|
EventGameReset struct {
|
|
Type EventType
|
|
Timestamp time.Time
|
|
ID string
|
|
}
|
|
EventCodenameAccusal struct {
|
|
Type EventType
|
|
Timestamp time.Time
|
|
Prosecutor string
|
|
Defendant string
|
|
Word string
|
|
}
|
|
EventCodenameTrial struct {
|
|
Type EventType
|
|
Timestamp time.Time
|
|
Guilty bool
|
|
}
|
|
EventNotification struct {
|
|
Type EventType
|
|
Timestamp time.Time
|
|
Recipient string
|
|
Message string
|
|
}
|
|
AllKillWords map[string]KillWords
|
|
)
|
|
|
|
const (
|
|
PlayerJoin EventType = iota + 1
|
|
PlayerLeave
|
|
GameComplete
|
|
AssignmentRotation
|
|
GameReset
|
|
CodenameAccusal
|
|
CodenameTrial
|
|
Notification
|
|
)
|
|
|
|
type Event interface{ event() }
|
|
|
|
func (EventPlayerJoin) event() {}
|
|
func (EventPlayerLeave) event() {}
|
|
func (EventGameComplete) event() {}
|
|
func (EventAssignmentRotation) event() {}
|
|
func (EventGameReset) event() {}
|
|
func (EventCodenameAccusal) event() {}
|
|
func (EventCodenameTrial) event() {}
|
|
func (EventNotification) event() {}
|
|
|
|
func EventWithTime(event Event, t time.Time) Event {
|
|
switch e := event.(type) {
|
|
case EventPlayerJoin:
|
|
e.Timestamp = t
|
|
event = e
|
|
case EventPlayerLeave:
|
|
e.Timestamp = t
|
|
event = e
|
|
case EventGameComplete:
|
|
e.Timestamp = t
|
|
event = e
|
|
case EventAssignmentRotation:
|
|
e.Timestamp = t
|
|
event = e
|
|
case EventGameReset:
|
|
e.Timestamp = t
|
|
event = e
|
|
case EventCodenameAccusal:
|
|
e.Timestamp = t
|
|
event = e
|
|
case EventCodenameTrial:
|
|
e.Timestamp = t
|
|
event = e
|
|
case EventNotification:
|
|
e.Timestamp = t
|
|
event = e
|
|
}
|
|
return event
|
|
}
|
|
|
|
func (games Games) GameEvents(ctx context.Context, id string, since time.Time) ([]Event, error) {
|
|
var results []Event
|
|
err := games.db.Query(ctx, func(rows *sql.Rows) error {
|
|
var timestamp time.Time
|
|
var b []byte
|
|
if err := rows.Scan(×tamp, &b); err != nil {
|
|
return err
|
|
}
|
|
event, err := parseEvent(b, timestamp)
|
|
results = append(results, event)
|
|
return err
|
|
}, `
|
|
SELECT
|
|
timestamp,
|
|
payload
|
|
FROM events
|
|
WHERE game_uuid=? and timestamp>?
|
|
ORDER BY timestamp ASC
|
|
`, id, since)
|
|
return results, err
|
|
}
|
|
|
|
func parseEvent(b []byte, timestamp time.Time) (Event, error) {
|
|
var peek struct {
|
|
Type EventType
|
|
}
|
|
if err := json.Unmarshal(b, &peek); err != nil {
|
|
return nil, err
|
|
}
|
|
switch peek.Type {
|
|
case PlayerJoin:
|
|
var v EventPlayerJoin
|
|
err := json.Unmarshal(b, &v)
|
|
return EventWithTime(v, timestamp), err
|
|
case PlayerLeave:
|
|
var v EventPlayerLeave
|
|
err := json.Unmarshal(b, &v)
|
|
return EventWithTime(v, timestamp), err
|
|
case GameComplete:
|
|
var v EventGameComplete
|
|
err := json.Unmarshal(b, &v)
|
|
return EventWithTime(v, timestamp), err
|
|
case AssignmentRotation:
|
|
var v EventAssignmentRotation
|
|
err := json.Unmarshal(b, &v)
|
|
return EventWithTime(v, timestamp), err
|
|
case GameReset:
|
|
var v EventGameReset
|
|
err := json.Unmarshal(b, &v)
|
|
return EventWithTime(v, timestamp), err
|
|
case CodenameAccusal:
|
|
var v EventCodenameAccusal
|
|
err := json.Unmarshal(b, &v)
|
|
return EventWithTime(v, timestamp), err
|
|
case CodenameTrial:
|
|
var v EventCodenameTrial
|
|
err := json.Unmarshal(b, &v)
|
|
return EventWithTime(v, timestamp), err
|
|
case Notification:
|
|
var v EventNotification
|
|
err := json.Unmarshal(b, &v)
|
|
return EventWithTime(v, timestamp), err
|
|
}
|
|
return nil, fmt.Errorf("unknown event type %d: %s", peek.Type, b)
|
|
}
|
|
|
|
func (games Games) GameState(ctx context.Context, id string) (GameState, error) {
|
|
result := GameState{ID: id, Players: map[string]PlayerState{}}
|
|
|
|
events, err := games.GameEvents(ctx, id, time.Time{})
|
|
if err != nil {
|
|
return result, err
|
|
}
|
|
|
|
for _, event := range events {
|
|
switch e := event.(type) {
|
|
case EventPlayerJoin:
|
|
result.Players[e.ID] = PlayerState{}
|
|
case EventPlayerLeave:
|
|
delete(result.Players, e.ID)
|
|
case EventGameComplete:
|
|
result.Completed = e.Timestamp
|
|
case EventAssignmentRotation:
|
|
result.Started = true
|
|
assignmentRotation := e
|
|
|
|
if killer, ok := result.Players[assignmentRotation.Killer]; !ok {
|
|
} else if _, ok := result.Players[assignmentRotation.Victim]; !ok {
|
|
} else {
|
|
killer.Kills = append(killer.Kills, Kill{
|
|
Timestamp: assignmentRotation.Timestamp,
|
|
Victim: assignmentRotation.Victim,
|
|
KillWord: 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.AllKillWords {
|
|
player := result.Players[k]
|
|
player.KillWords = v
|
|
result.Players[k] = player
|
|
}
|
|
case EventCodenameAccusal:
|
|
if actual := result.Players[e.Defendant].KillWords.Codename; !actual.Consumed {
|
|
result.Trial.Prosecutor = e.Prosecutor
|
|
result.Trial.Defendant = e.Defendant
|
|
result.Trial.Word = e.Word
|
|
|
|
if !basicallyTheSame(actual.KillWord.Word, e.Word) {
|
|
} else if err := games.CreateEventCodenameTrial(ctx, id, true); err != nil { // TODO cannot be in State loop
|
|
return GameState{}, err
|
|
}
|
|
}
|
|
case EventCodenameTrial:
|
|
if result.Trial == (Trial{}) {
|
|
} else if e.Guilty {
|
|
if err := games.CreateEventNotification(ctx, id, fmt.Sprintf(`%s revealed %s is %s and collected %s's bounty.`, result.Trial.Prosecutor, result.Trial.Defendant, result.Trial.Word, result.Trial.Defendant)); err != nil { // TODO not in this loop
|
|
return GameState{}, err
|
|
}
|
|
return GameState{}, fmt.Errorf("not impl: trial: guilty: %+v", e)
|
|
} else {
|
|
v := result.Players[result.Trial.Prosecutor]
|
|
v.KillWords.Codename.Consumed = true
|
|
v.Kills = append(v.Kills, Kill{
|
|
Timestamp: e.Timestamp,
|
|
Victim: result.Trial.Defendant,
|
|
KillWord: KillWord{
|
|
Word: result.Trial.Word,
|
|
Points: -200,
|
|
},
|
|
})
|
|
result.Players[result.Trial.Prosecutor] = v
|
|
|
|
v = result.Players[result.Trial.Defendant]
|
|
v.KillWords.Codename.KillWord.Word = "" // TODO
|
|
|
|
return GameState{}, fmt.Errorf("creating state CANNOT create events because it will eval every loop")
|
|
if err := games.CreateEventNotification(ctx, id, fmt.Sprintf(`%s accused the innocent %s of being %s. %s will get a new codename.`, result.Trial.Prosecutor, result.Trial.Defendant, result.Trial.Word, result.Trial.Defendant)); err != nil {
|
|
return GameState{}, err
|
|
}
|
|
}
|
|
result.Trial = Trial{}
|
|
case EventGameReset:
|
|
return games.GameState(ctx, e.ID)
|
|
default:
|
|
return GameState{}, fmt.Errorf("unknown event type %T", event)
|
|
}
|
|
}
|
|
|
|
return result, err
|
|
}
|
|
|
|
func basicallyTheSame(a, b string) bool {
|
|
simplify := func(s string) string {
|
|
s = strings.TrimSpace(strings.ToLower(s))
|
|
s2 := ""
|
|
for _, c := range s {
|
|
if unicode.IsLetter(c) {
|
|
s2 = fmt.Sprintf("%s%c", s2, c)
|
|
}
|
|
}
|
|
return s2
|
|
}
|
|
return simplify(a) == simplify(b)
|
|
}
|
|
|
|
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 {
|
|
if _, err := games.UserName(ctx, player); err != nil {
|
|
return err
|
|
}
|
|
if err := games.db.Exec(ctx, `
|
|
INSERT INTO players (
|
|
game_uuid,
|
|
user_uuid
|
|
) VALUES (?, ?);
|
|
`, id, player); err != nil {
|
|
return err
|
|
}
|
|
return games.createEvent(ctx, id, EventPlayerJoin{Type: PlayerJoin, ID: player})
|
|
}
|
|
|
|
func (games Games) CreateEventPlayerLeave(ctx context.Context, id string, player string) error {
|
|
return games.createEvent(ctx, id, EventPlayerLeave{Type: PlayerLeave, ID: player})
|
|
}
|
|
|
|
func (games Games) CreateEventAssignmentRotation(ctx context.Context, id string, killer, victim, word string, points int) error {
|
|
state, err := games.GameState(ctx, id)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
event := EventAssignmentRotation{
|
|
Type: AssignmentRotation,
|
|
Killer: killer,
|
|
Victim: victim,
|
|
KillWord: KillWord{
|
|
Word: word,
|
|
Points: points,
|
|
},
|
|
}
|
|
|
|
prevAllKillWords := state.AllKillWords()
|
|
|
|
event.AllKillWords = prevAllKillWords.ShuffleAssignees(killer, victim, word)
|
|
event.AllKillWords = event.AllKillWords.FillKillWords()
|
|
|
|
return games.createEvent(ctx, id, event)
|
|
}
|
|
|
|
func (state GameState) AllKillWords() AllKillWords {
|
|
m := make(AllKillWords)
|
|
for k, v := range state.Players {
|
|
m[k] = v.KillWords
|
|
}
|
|
return m
|
|
}
|
|
|
|
func (games Games) CreateEventGameReset(ctx context.Context, gid string) error {
|
|
state, err := games.GameState(ctx, gid)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
name, err := games.GameName(ctx, gid)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
gid2, err := games.CreateGame(ctx, name)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for p := range state.Players {
|
|
if err := games.CreateEventPlayerJoin(ctx, gid2, p); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
event := EventGameReset{
|
|
Type: GameReset,
|
|
Timestamp: time.Now(),
|
|
ID: gid2,
|
|
}
|
|
return games.createEvent(ctx, gid, event)
|
|
}
|
|
|
|
func (words KillWords) Empty() bool {
|
|
return words.Codename == (Codename{}) && words.Assigned.IsZero() && words.Assignee == "" && words.Assignment.Empty()
|
|
}
|
|
|
|
func (words KillWords) Privates() []KillWord {
|
|
a := slices.Clone(words.Assignment.Private)
|
|
slices.SortFunc(a, func(a, b KillWord) int {
|
|
return strings.Compare(a.Word, b.Word)
|
|
})
|
|
return a
|
|
}
|
|
|
|
func (words KillWords) Publics() []KillWord {
|
|
a := slices.Clone(words.Assignment.Public)
|
|
slices.SortFunc(a, func(a, b KillWord) int {
|
|
return strings.Compare(a.Word, b.Word)
|
|
})
|
|
return a
|
|
}
|
|
|
|
func (prev AllKillWords) ShuffleAssignees(killer, victim, word string) AllKillWords {
|
|
m := prev.withoutAssignees()
|
|
|
|
if _, ok := prev[killer]; !ok {
|
|
} else if victimState, ok := prev[victim]; !ok {
|
|
} else if killer != victimState.Assignee { // if victim was targeting killer, just randomize
|
|
m.assign(killer, victimState.Assignee)
|
|
}
|
|
|
|
for !func() bool {
|
|
allKillWords := maps.Clone(m)
|
|
unassigned := allKillWords.unassigned()
|
|
pop := func() string {
|
|
result := unassigned[0]
|
|
unassigned = unassigned[1:]
|
|
return result
|
|
}
|
|
|
|
for k, v := range allKillWords {
|
|
if v.Assignee != "" {
|
|
continue
|
|
}
|
|
v.Assignee = pop()
|
|
if k == v.Assignee || prev[k].Assignee == v.Assignee {
|
|
return false
|
|
}
|
|
allKillWords[k] = v
|
|
}
|
|
|
|
if len(unassigned) > 0 {
|
|
panic(unassigned)
|
|
}
|
|
|
|
m = allKillWords
|
|
return true
|
|
}() {
|
|
}
|
|
|
|
return m
|
|
}
|
|
|
|
func (m AllKillWords) assign(killer, victim string) {
|
|
v := m[killer]
|
|
v.Assignee = victim
|
|
m[killer] = v
|
|
}
|
|
|
|
func (m AllKillWords) withoutAssignees() AllKillWords {
|
|
now := time.Now()
|
|
result := make(AllKillWords)
|
|
for k := range m {
|
|
result[k] = KillWords{
|
|
Codename: m[k].Codename,
|
|
Assigned: now,
|
|
Assignee: "",
|
|
Assignment: m[k].Assignment,
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
|
|
func (m AllKillWords) unassigned() []string {
|
|
var result []string
|
|
for k := range m {
|
|
if !slices.Contains(m.assigned(), k) {
|
|
result = append(result, k)
|
|
}
|
|
}
|
|
for i := range result {
|
|
j := rand.Intn(i + 1)
|
|
result[i], result[j] = result[j], result[i]
|
|
}
|
|
return result
|
|
}
|
|
|
|
func (m AllKillWords) assigned() []string {
|
|
var result []string
|
|
for k := range m {
|
|
v := m[k].Assignee
|
|
if v == "" {
|
|
continue
|
|
}
|
|
result = append(result, v)
|
|
}
|
|
return result
|
|
}
|
|
|
|
//go:embed holiday.txt
|
|
var wordsHoliday string
|
|
|
|
func (m AllKillWords) FillKillWords() AllKillWords {
|
|
return m.fillKillWords(
|
|
strings.Fields(wordsHoliday),
|
|
1,
|
|
strings.Fields(wordsHoliday), // TODO medium difficulty
|
|
2,
|
|
strings.Fields(wordsHoliday), // TODO hard difficulty
|
|
)
|
|
}
|
|
|
|
func (m AllKillWords) fillKillWords(
|
|
poolCodename []string,
|
|
nPublic int,
|
|
poolPublic []string,
|
|
nPrivate int,
|
|
poolPrivate []string,
|
|
) AllKillWords {
|
|
result := maps.Clone(m)
|
|
m = result
|
|
for k, v := range m {
|
|
if v.Codename.KillWord.Word == "" {
|
|
v.Codename = Codename{KillWord: KillWord{Word: m.unusedCodename(poolCodename), Points: 200}}
|
|
m[k] = v
|
|
}
|
|
if len(v.Assignment.Public) == 0 {
|
|
v.Assignment.Public = []KillWord{}
|
|
for i := 0; i < nPublic; i++ {
|
|
v.Assignment.Public = append(v.Assignment.Public, KillWord{Word: m.unusedPublic(poolPublic), Points: 50})
|
|
m[k] = v
|
|
}
|
|
}
|
|
if len(v.Assignment.Private) == 0 {
|
|
v.Assignment.Private = []KillWord{}
|
|
for i := 0; i < nPrivate; i++ {
|
|
v.Assignment.Private = append(v.Assignment.Private, KillWord{Word: m.unusedPrivate(poolPrivate), Points: 100})
|
|
m[k] = v
|
|
}
|
|
}
|
|
}
|
|
return m
|
|
}
|
|
|
|
func (m AllKillWords) unusedCodename(pool []string) string {
|
|
inUse := func() []string {
|
|
result := []string{}
|
|
for _, killWords := range m {
|
|
result = append(result, killWords.Codename.KillWord.Word)
|
|
}
|
|
return result
|
|
}
|
|
for {
|
|
picked := pool[rand.Intn(len(pool))]
|
|
if !slices.Contains(inUse(), picked) {
|
|
return picked
|
|
}
|
|
}
|
|
}
|
|
|
|
func (m AllKillWords) unusedPrivate(pool []string) string {
|
|
inUse := func() []string {
|
|
result := []string{}
|
|
for _, killWords := range m {
|
|
for _, killWord := range killWords.Assignment.Private {
|
|
result = append(result, killWord.Word)
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
for {
|
|
picked := pool[rand.Intn(len(pool))]
|
|
if !slices.Contains(inUse(), picked) {
|
|
return picked
|
|
}
|
|
}
|
|
}
|
|
|
|
func (m AllKillWords) unusedPublic(pool []string) string {
|
|
inUse := func() []string {
|
|
result := []string{}
|
|
for _, killWords := range m {
|
|
for _, killWord := range killWords.Assignment.Public {
|
|
result = append(result, killWord.Word)
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
for {
|
|
picked := pool[rand.Intn(len(pool))]
|
|
if !slices.Contains(inUse(), picked) {
|
|
return picked
|
|
}
|
|
}
|
|
}
|
|
|
|
func (games Games) CreateEventGameComplete(ctx context.Context, id string) error {
|
|
if err := games.db.Exec(ctx, `
|
|
UPDATE games
|
|
SET completed=?, updated=?
|
|
WHERE uuid=?
|
|
`, time.Now(), time.Now(), id); err != nil {
|
|
return err
|
|
}
|
|
return games.createEvent(ctx, id, EventGameComplete{Type: GameComplete})
|
|
}
|
|
|
|
func (games Games) CreateEventCodenameAccusal(ctx context.Context, gid, prosecutor, defendant, codename string) error {
|
|
return fmt.Errorf("not impl: x accused y")
|
|
return fmt.Errorf("not impl: x caught by y")
|
|
}
|
|
|
|
func (games Games) CreateEventCodenameTrial(ctx context.Context, gid string, guilty bool) error {
|
|
return fmt.Errorf("not impl: x found guilty/notguilty")
|
|
}
|
|
|
|
func (games Games) CreateEventNotification(ctx context.Context, gid, msg string) error {
|
|
return games.CreateEventNotificationTo(ctx, gid, "", msg)
|
|
}
|
|
|
|
func (games Games) CreateEventNotificationTo(ctx context.Context, gid, uid, msg string) error {
|
|
return fmt.Errorf("not impl: simple")
|
|
}
|
|
|
|
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)
|
|
}
|