Stage 3: game domain (lifecycle, journal+cache, hint, word-check, GCG, stats)
internal/game drives the engine over a single match and owns everything the
engine does not: event-sourced persistence (a games row + an append-only decoded
move journal, with the live engine.Game kept warm in a cache and rebuilt by
replay on a miss), the play/pass/exchange/resign transitions with
validate-at-submit scoring, an unlimited score/legality preview, the hint
(per-game allowance + profile wallet), the word-check tool with complaint
capture, per-player game state, history and GCG export (Poslfit dialect), and a
background turn-timeout sweeper that auto-resigns overdue turns honouring each
player's daily away window. Like Stages 1-2 it is a service/store layer with no
HTTP; the gateway surface lands in Stage 6.
Engine: additive decoded domain API (Direction, SubmitPlay/SubmitExchange/
EvaluatePlay/HintView/Hand, MoveRecord.{Dir,MainRow,MainCol}, Registry.Lookup,
ParseVariant) so internal/game never imports scrabble-solver; and a Resign fix
so the resigner keeps their score yet never wins (the other player wins a
two-player game). Timeout reuses Resign.
Persistence: migration 00002 adds games, game_players, game_moves, complaints,
account_stats and extends accounts with away_start/away_end/hint_balance; go-jet
regenerated. account gained SpendHint. Config adds BACKEND_DICT_DIR (required),
BACKEND_DICT_VERSION, BACKEND_GAME_TIMEOUT_SWEEP_INTERVAL, BACKEND_GAME_CACHE_TTL;
main loads the registry at boot (hard dependency) and starts the sweeper.
Tests: engine resign + decoded-API tests; game unit tests (GCG, away-window
boundaries, hint budget, cache, keyed mutex, payload); inttest integration
(lifecycle, replay equivalence, timeout sweep with away grace, resign stats,
hint policy, word-check/complaint, per-game-lock). Docs (PLAN, ARCHITECTURE,
FUNCTIONAL +_ru, TESTING, README) updated.
This commit is contained in:
@@ -0,0 +1,23 @@
|
||||
//
|
||||
// Code generated by go-jet DO NOT EDIT.
|
||||
//
|
||||
// WARNING: Changes to this file may cause incorrect behavior
|
||||
// and will be lost if the code is regenerated
|
||||
//
|
||||
|
||||
package model
|
||||
|
||||
import (
|
||||
"github.com/google/uuid"
|
||||
"time"
|
||||
)
|
||||
|
||||
type AccountStats struct {
|
||||
AccountID uuid.UUID `sql:"primary_key"`
|
||||
Wins int32
|
||||
Losses int32
|
||||
Draws int32
|
||||
MaxGamePoints int32
|
||||
MaxWordPoints int32
|
||||
UpdatedAt time.Time
|
||||
}
|
||||
@@ -21,4 +21,7 @@ type Accounts struct {
|
||||
BlockFriendRequests bool
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
AwayStart time.Time
|
||||
AwayEnd time.Time
|
||||
HintBalance int32
|
||||
}
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
//
|
||||
// Code generated by go-jet DO NOT EDIT.
|
||||
//
|
||||
// WARNING: Changes to this file may cause incorrect behavior
|
||||
// and will be lost if the code is regenerated
|
||||
//
|
||||
|
||||
package model
|
||||
|
||||
import (
|
||||
"github.com/google/uuid"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Complaints struct {
|
||||
ComplaintID uuid.UUID `sql:"primary_key"`
|
||||
ComplainantID uuid.UUID
|
||||
GameID uuid.UUID
|
||||
Variant string
|
||||
DictVersion string
|
||||
Word string
|
||||
WasValid bool
|
||||
Note string
|
||||
Status string
|
||||
CreatedAt time.Time
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
//
|
||||
// Code generated by go-jet DO NOT EDIT.
|
||||
//
|
||||
// WARNING: Changes to this file may cause incorrect behavior
|
||||
// and will be lost if the code is regenerated
|
||||
//
|
||||
|
||||
package model
|
||||
|
||||
import (
|
||||
"github.com/google/uuid"
|
||||
"time"
|
||||
)
|
||||
|
||||
type GameMoves struct {
|
||||
GameID uuid.UUID `sql:"primary_key"`
|
||||
Seq int32 `sql:"primary_key"`
|
||||
Seat int16
|
||||
Action string
|
||||
Score int32
|
||||
RunningTotal int32
|
||||
ExchangedCount int16
|
||||
Payload string
|
||||
CreatedAt time.Time
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
//
|
||||
// Code generated by go-jet DO NOT EDIT.
|
||||
//
|
||||
// WARNING: Changes to this file may cause incorrect behavior
|
||||
// and will be lost if the code is regenerated
|
||||
//
|
||||
|
||||
package model
|
||||
|
||||
import (
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type GamePlayers struct {
|
||||
GameID uuid.UUID `sql:"primary_key"`
|
||||
Seat int16 `sql:"primary_key"`
|
||||
AccountID uuid.UUID
|
||||
Score int32
|
||||
HintsUsed int16
|
||||
IsWinner bool
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
//
|
||||
// Code generated by go-jet DO NOT EDIT.
|
||||
//
|
||||
// WARNING: Changes to this file may cause incorrect behavior
|
||||
// and will be lost if the code is regenerated
|
||||
//
|
||||
|
||||
package model
|
||||
|
||||
import (
|
||||
"github.com/google/uuid"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Games struct {
|
||||
GameID uuid.UUID `sql:"primary_key"`
|
||||
Variant string
|
||||
DictVersion string
|
||||
Seed int64
|
||||
Status string
|
||||
Players int16
|
||||
ToMove int16
|
||||
TurnStartedAt time.Time
|
||||
TurnTimeoutSecs int32
|
||||
HintsAllowed bool
|
||||
HintsPerPlayer int16
|
||||
MoveCount int32
|
||||
EndReason *string
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
FinishedAt *time.Time
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
//
|
||||
// Code generated by go-jet DO NOT EDIT.
|
||||
//
|
||||
// WARNING: Changes to this file may cause incorrect behavior
|
||||
// and will be lost if the code is regenerated
|
||||
//
|
||||
|
||||
package table
|
||||
|
||||
import (
|
||||
"github.com/go-jet/jet/v2/postgres"
|
||||
)
|
||||
|
||||
var AccountStats = newAccountStatsTable("backend", "account_stats", "")
|
||||
|
||||
type accountStatsTable struct {
|
||||
postgres.Table
|
||||
|
||||
// Columns
|
||||
AccountID postgres.ColumnString
|
||||
Wins postgres.ColumnInteger
|
||||
Losses postgres.ColumnInteger
|
||||
Draws postgres.ColumnInteger
|
||||
MaxGamePoints postgres.ColumnInteger
|
||||
MaxWordPoints postgres.ColumnInteger
|
||||
UpdatedAt postgres.ColumnTimestampz
|
||||
|
||||
AllColumns postgres.ColumnList
|
||||
MutableColumns postgres.ColumnList
|
||||
DefaultColumns postgres.ColumnList
|
||||
}
|
||||
|
||||
type AccountStatsTable struct {
|
||||
accountStatsTable
|
||||
|
||||
EXCLUDED accountStatsTable
|
||||
}
|
||||
|
||||
// AS creates new AccountStatsTable with assigned alias
|
||||
func (a AccountStatsTable) AS(alias string) *AccountStatsTable {
|
||||
return newAccountStatsTable(a.SchemaName(), a.TableName(), alias)
|
||||
}
|
||||
|
||||
// Schema creates new AccountStatsTable with assigned schema name
|
||||
func (a AccountStatsTable) FromSchema(schemaName string) *AccountStatsTable {
|
||||
return newAccountStatsTable(schemaName, a.TableName(), a.Alias())
|
||||
}
|
||||
|
||||
// WithPrefix creates new AccountStatsTable with assigned table prefix
|
||||
func (a AccountStatsTable) WithPrefix(prefix string) *AccountStatsTable {
|
||||
return newAccountStatsTable(a.SchemaName(), prefix+a.TableName(), a.TableName())
|
||||
}
|
||||
|
||||
// WithSuffix creates new AccountStatsTable with assigned table suffix
|
||||
func (a AccountStatsTable) WithSuffix(suffix string) *AccountStatsTable {
|
||||
return newAccountStatsTable(a.SchemaName(), a.TableName()+suffix, a.TableName())
|
||||
}
|
||||
|
||||
func newAccountStatsTable(schemaName, tableName, alias string) *AccountStatsTable {
|
||||
return &AccountStatsTable{
|
||||
accountStatsTable: newAccountStatsTableImpl(schemaName, tableName, alias),
|
||||
EXCLUDED: newAccountStatsTableImpl("", "excluded", ""),
|
||||
}
|
||||
}
|
||||
|
||||
func newAccountStatsTableImpl(schemaName, tableName, alias string) accountStatsTable {
|
||||
var (
|
||||
AccountIDColumn = postgres.StringColumn("account_id")
|
||||
WinsColumn = postgres.IntegerColumn("wins")
|
||||
LossesColumn = postgres.IntegerColumn("losses")
|
||||
DrawsColumn = postgres.IntegerColumn("draws")
|
||||
MaxGamePointsColumn = postgres.IntegerColumn("max_game_points")
|
||||
MaxWordPointsColumn = postgres.IntegerColumn("max_word_points")
|
||||
UpdatedAtColumn = postgres.TimestampzColumn("updated_at")
|
||||
allColumns = postgres.ColumnList{AccountIDColumn, WinsColumn, LossesColumn, DrawsColumn, MaxGamePointsColumn, MaxWordPointsColumn, UpdatedAtColumn}
|
||||
mutableColumns = postgres.ColumnList{WinsColumn, LossesColumn, DrawsColumn, MaxGamePointsColumn, MaxWordPointsColumn, UpdatedAtColumn}
|
||||
defaultColumns = postgres.ColumnList{WinsColumn, LossesColumn, DrawsColumn, MaxGamePointsColumn, MaxWordPointsColumn, UpdatedAtColumn}
|
||||
)
|
||||
|
||||
return accountStatsTable{
|
||||
Table: postgres.NewTable(schemaName, tableName, alias, allColumns...),
|
||||
|
||||
//Columns
|
||||
AccountID: AccountIDColumn,
|
||||
Wins: WinsColumn,
|
||||
Losses: LossesColumn,
|
||||
Draws: DrawsColumn,
|
||||
MaxGamePoints: MaxGamePointsColumn,
|
||||
MaxWordPoints: MaxWordPointsColumn,
|
||||
UpdatedAt: UpdatedAtColumn,
|
||||
|
||||
AllColumns: allColumns,
|
||||
MutableColumns: mutableColumns,
|
||||
DefaultColumns: defaultColumns,
|
||||
}
|
||||
}
|
||||
@@ -25,6 +25,9 @@ type accountsTable struct {
|
||||
BlockFriendRequests postgres.ColumnBool
|
||||
CreatedAt postgres.ColumnTimestampz
|
||||
UpdatedAt postgres.ColumnTimestampz
|
||||
AwayStart postgres.ColumnTime
|
||||
AwayEnd postgres.ColumnTime
|
||||
HintBalance postgres.ColumnInteger
|
||||
|
||||
AllColumns postgres.ColumnList
|
||||
MutableColumns postgres.ColumnList
|
||||
@@ -74,9 +77,12 @@ func newAccountsTableImpl(schemaName, tableName, alias string) accountsTable {
|
||||
BlockFriendRequestsColumn = postgres.BoolColumn("block_friend_requests")
|
||||
CreatedAtColumn = postgres.TimestampzColumn("created_at")
|
||||
UpdatedAtColumn = postgres.TimestampzColumn("updated_at")
|
||||
allColumns = postgres.ColumnList{AccountIDColumn, DisplayNameColumn, PreferredLanguageColumn, TimeZoneColumn, BlockChatColumn, BlockFriendRequestsColumn, CreatedAtColumn, UpdatedAtColumn}
|
||||
mutableColumns = postgres.ColumnList{DisplayNameColumn, PreferredLanguageColumn, TimeZoneColumn, BlockChatColumn, BlockFriendRequestsColumn, CreatedAtColumn, UpdatedAtColumn}
|
||||
defaultColumns = postgres.ColumnList{DisplayNameColumn, PreferredLanguageColumn, TimeZoneColumn, BlockChatColumn, BlockFriendRequestsColumn, CreatedAtColumn, UpdatedAtColumn}
|
||||
AwayStartColumn = postgres.TimeColumn("away_start")
|
||||
AwayEndColumn = postgres.TimeColumn("away_end")
|
||||
HintBalanceColumn = postgres.IntegerColumn("hint_balance")
|
||||
allColumns = postgres.ColumnList{AccountIDColumn, DisplayNameColumn, PreferredLanguageColumn, TimeZoneColumn, BlockChatColumn, BlockFriendRequestsColumn, CreatedAtColumn, UpdatedAtColumn, AwayStartColumn, AwayEndColumn, HintBalanceColumn}
|
||||
mutableColumns = postgres.ColumnList{DisplayNameColumn, PreferredLanguageColumn, TimeZoneColumn, BlockChatColumn, BlockFriendRequestsColumn, CreatedAtColumn, UpdatedAtColumn, AwayStartColumn, AwayEndColumn, HintBalanceColumn}
|
||||
defaultColumns = postgres.ColumnList{DisplayNameColumn, PreferredLanguageColumn, TimeZoneColumn, BlockChatColumn, BlockFriendRequestsColumn, CreatedAtColumn, UpdatedAtColumn, AwayStartColumn, AwayEndColumn, HintBalanceColumn}
|
||||
)
|
||||
|
||||
return accountsTable{
|
||||
@@ -91,6 +97,9 @@ func newAccountsTableImpl(schemaName, tableName, alias string) accountsTable {
|
||||
BlockFriendRequests: BlockFriendRequestsColumn,
|
||||
CreatedAt: CreatedAtColumn,
|
||||
UpdatedAt: UpdatedAtColumn,
|
||||
AwayStart: AwayStartColumn,
|
||||
AwayEnd: AwayEndColumn,
|
||||
HintBalance: HintBalanceColumn,
|
||||
|
||||
AllColumns: allColumns,
|
||||
MutableColumns: mutableColumns,
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
//
|
||||
// Code generated by go-jet DO NOT EDIT.
|
||||
//
|
||||
// WARNING: Changes to this file may cause incorrect behavior
|
||||
// and will be lost if the code is regenerated
|
||||
//
|
||||
|
||||
package table
|
||||
|
||||
import (
|
||||
"github.com/go-jet/jet/v2/postgres"
|
||||
)
|
||||
|
||||
var Complaints = newComplaintsTable("backend", "complaints", "")
|
||||
|
||||
type complaintsTable struct {
|
||||
postgres.Table
|
||||
|
||||
// Columns
|
||||
ComplaintID postgres.ColumnString
|
||||
ComplainantID postgres.ColumnString
|
||||
GameID postgres.ColumnString
|
||||
Variant postgres.ColumnString
|
||||
DictVersion postgres.ColumnString
|
||||
Word postgres.ColumnString
|
||||
WasValid postgres.ColumnBool
|
||||
Note postgres.ColumnString
|
||||
Status postgres.ColumnString
|
||||
CreatedAt postgres.ColumnTimestampz
|
||||
|
||||
AllColumns postgres.ColumnList
|
||||
MutableColumns postgres.ColumnList
|
||||
DefaultColumns postgres.ColumnList
|
||||
}
|
||||
|
||||
type ComplaintsTable struct {
|
||||
complaintsTable
|
||||
|
||||
EXCLUDED complaintsTable
|
||||
}
|
||||
|
||||
// AS creates new ComplaintsTable with assigned alias
|
||||
func (a ComplaintsTable) AS(alias string) *ComplaintsTable {
|
||||
return newComplaintsTable(a.SchemaName(), a.TableName(), alias)
|
||||
}
|
||||
|
||||
// Schema creates new ComplaintsTable with assigned schema name
|
||||
func (a ComplaintsTable) FromSchema(schemaName string) *ComplaintsTable {
|
||||
return newComplaintsTable(schemaName, a.TableName(), a.Alias())
|
||||
}
|
||||
|
||||
// WithPrefix creates new ComplaintsTable with assigned table prefix
|
||||
func (a ComplaintsTable) WithPrefix(prefix string) *ComplaintsTable {
|
||||
return newComplaintsTable(a.SchemaName(), prefix+a.TableName(), a.TableName())
|
||||
}
|
||||
|
||||
// WithSuffix creates new ComplaintsTable with assigned table suffix
|
||||
func (a ComplaintsTable) WithSuffix(suffix string) *ComplaintsTable {
|
||||
return newComplaintsTable(a.SchemaName(), a.TableName()+suffix, a.TableName())
|
||||
}
|
||||
|
||||
func newComplaintsTable(schemaName, tableName, alias string) *ComplaintsTable {
|
||||
return &ComplaintsTable{
|
||||
complaintsTable: newComplaintsTableImpl(schemaName, tableName, alias),
|
||||
EXCLUDED: newComplaintsTableImpl("", "excluded", ""),
|
||||
}
|
||||
}
|
||||
|
||||
func newComplaintsTableImpl(schemaName, tableName, alias string) complaintsTable {
|
||||
var (
|
||||
ComplaintIDColumn = postgres.StringColumn("complaint_id")
|
||||
ComplainantIDColumn = postgres.StringColumn("complainant_id")
|
||||
GameIDColumn = postgres.StringColumn("game_id")
|
||||
VariantColumn = postgres.StringColumn("variant")
|
||||
DictVersionColumn = postgres.StringColumn("dict_version")
|
||||
WordColumn = postgres.StringColumn("word")
|
||||
WasValidColumn = postgres.BoolColumn("was_valid")
|
||||
NoteColumn = postgres.StringColumn("note")
|
||||
StatusColumn = postgres.StringColumn("status")
|
||||
CreatedAtColumn = postgres.TimestampzColumn("created_at")
|
||||
allColumns = postgres.ColumnList{ComplaintIDColumn, ComplainantIDColumn, GameIDColumn, VariantColumn, DictVersionColumn, WordColumn, WasValidColumn, NoteColumn, StatusColumn, CreatedAtColumn}
|
||||
mutableColumns = postgres.ColumnList{ComplainantIDColumn, GameIDColumn, VariantColumn, DictVersionColumn, WordColumn, WasValidColumn, NoteColumn, StatusColumn, CreatedAtColumn}
|
||||
defaultColumns = postgres.ColumnList{NoteColumn, StatusColumn, CreatedAtColumn}
|
||||
)
|
||||
|
||||
return complaintsTable{
|
||||
Table: postgres.NewTable(schemaName, tableName, alias, allColumns...),
|
||||
|
||||
//Columns
|
||||
ComplaintID: ComplaintIDColumn,
|
||||
ComplainantID: ComplainantIDColumn,
|
||||
GameID: GameIDColumn,
|
||||
Variant: VariantColumn,
|
||||
DictVersion: DictVersionColumn,
|
||||
Word: WordColumn,
|
||||
WasValid: WasValidColumn,
|
||||
Note: NoteColumn,
|
||||
Status: StatusColumn,
|
||||
CreatedAt: CreatedAtColumn,
|
||||
|
||||
AllColumns: allColumns,
|
||||
MutableColumns: mutableColumns,
|
||||
DefaultColumns: defaultColumns,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
//
|
||||
// Code generated by go-jet DO NOT EDIT.
|
||||
//
|
||||
// WARNING: Changes to this file may cause incorrect behavior
|
||||
// and will be lost if the code is regenerated
|
||||
//
|
||||
|
||||
package table
|
||||
|
||||
import (
|
||||
"github.com/go-jet/jet/v2/postgres"
|
||||
)
|
||||
|
||||
var GameMoves = newGameMovesTable("backend", "game_moves", "")
|
||||
|
||||
type gameMovesTable struct {
|
||||
postgres.Table
|
||||
|
||||
// Columns
|
||||
GameID postgres.ColumnString
|
||||
Seq postgres.ColumnInteger
|
||||
Seat postgres.ColumnInteger
|
||||
Action postgres.ColumnString
|
||||
Score postgres.ColumnInteger
|
||||
RunningTotal postgres.ColumnInteger
|
||||
ExchangedCount postgres.ColumnInteger
|
||||
Payload postgres.ColumnString
|
||||
CreatedAt postgres.ColumnTimestampz
|
||||
|
||||
AllColumns postgres.ColumnList
|
||||
MutableColumns postgres.ColumnList
|
||||
DefaultColumns postgres.ColumnList
|
||||
}
|
||||
|
||||
type GameMovesTable struct {
|
||||
gameMovesTable
|
||||
|
||||
EXCLUDED gameMovesTable
|
||||
}
|
||||
|
||||
// AS creates new GameMovesTable with assigned alias
|
||||
func (a GameMovesTable) AS(alias string) *GameMovesTable {
|
||||
return newGameMovesTable(a.SchemaName(), a.TableName(), alias)
|
||||
}
|
||||
|
||||
// Schema creates new GameMovesTable with assigned schema name
|
||||
func (a GameMovesTable) FromSchema(schemaName string) *GameMovesTable {
|
||||
return newGameMovesTable(schemaName, a.TableName(), a.Alias())
|
||||
}
|
||||
|
||||
// WithPrefix creates new GameMovesTable with assigned table prefix
|
||||
func (a GameMovesTable) WithPrefix(prefix string) *GameMovesTable {
|
||||
return newGameMovesTable(a.SchemaName(), prefix+a.TableName(), a.TableName())
|
||||
}
|
||||
|
||||
// WithSuffix creates new GameMovesTable with assigned table suffix
|
||||
func (a GameMovesTable) WithSuffix(suffix string) *GameMovesTable {
|
||||
return newGameMovesTable(a.SchemaName(), a.TableName()+suffix, a.TableName())
|
||||
}
|
||||
|
||||
func newGameMovesTable(schemaName, tableName, alias string) *GameMovesTable {
|
||||
return &GameMovesTable{
|
||||
gameMovesTable: newGameMovesTableImpl(schemaName, tableName, alias),
|
||||
EXCLUDED: newGameMovesTableImpl("", "excluded", ""),
|
||||
}
|
||||
}
|
||||
|
||||
func newGameMovesTableImpl(schemaName, tableName, alias string) gameMovesTable {
|
||||
var (
|
||||
GameIDColumn = postgres.StringColumn("game_id")
|
||||
SeqColumn = postgres.IntegerColumn("seq")
|
||||
SeatColumn = postgres.IntegerColumn("seat")
|
||||
ActionColumn = postgres.StringColumn("action")
|
||||
ScoreColumn = postgres.IntegerColumn("score")
|
||||
RunningTotalColumn = postgres.IntegerColumn("running_total")
|
||||
ExchangedCountColumn = postgres.IntegerColumn("exchanged_count")
|
||||
PayloadColumn = postgres.StringColumn("payload")
|
||||
CreatedAtColumn = postgres.TimestampzColumn("created_at")
|
||||
allColumns = postgres.ColumnList{GameIDColumn, SeqColumn, SeatColumn, ActionColumn, ScoreColumn, RunningTotalColumn, ExchangedCountColumn, PayloadColumn, CreatedAtColumn}
|
||||
mutableColumns = postgres.ColumnList{SeatColumn, ActionColumn, ScoreColumn, RunningTotalColumn, ExchangedCountColumn, PayloadColumn, CreatedAtColumn}
|
||||
defaultColumns = postgres.ColumnList{ScoreColumn, RunningTotalColumn, ExchangedCountColumn, PayloadColumn, CreatedAtColumn}
|
||||
)
|
||||
|
||||
return gameMovesTable{
|
||||
Table: postgres.NewTable(schemaName, tableName, alias, allColumns...),
|
||||
|
||||
//Columns
|
||||
GameID: GameIDColumn,
|
||||
Seq: SeqColumn,
|
||||
Seat: SeatColumn,
|
||||
Action: ActionColumn,
|
||||
Score: ScoreColumn,
|
||||
RunningTotal: RunningTotalColumn,
|
||||
ExchangedCount: ExchangedCountColumn,
|
||||
Payload: PayloadColumn,
|
||||
CreatedAt: CreatedAtColumn,
|
||||
|
||||
AllColumns: allColumns,
|
||||
MutableColumns: mutableColumns,
|
||||
DefaultColumns: defaultColumns,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
//
|
||||
// Code generated by go-jet DO NOT EDIT.
|
||||
//
|
||||
// WARNING: Changes to this file may cause incorrect behavior
|
||||
// and will be lost if the code is regenerated
|
||||
//
|
||||
|
||||
package table
|
||||
|
||||
import (
|
||||
"github.com/go-jet/jet/v2/postgres"
|
||||
)
|
||||
|
||||
var GamePlayers = newGamePlayersTable("backend", "game_players", "")
|
||||
|
||||
type gamePlayersTable struct {
|
||||
postgres.Table
|
||||
|
||||
// Columns
|
||||
GameID postgres.ColumnString
|
||||
Seat postgres.ColumnInteger
|
||||
AccountID postgres.ColumnString
|
||||
Score postgres.ColumnInteger
|
||||
HintsUsed postgres.ColumnInteger
|
||||
IsWinner postgres.ColumnBool
|
||||
|
||||
AllColumns postgres.ColumnList
|
||||
MutableColumns postgres.ColumnList
|
||||
DefaultColumns postgres.ColumnList
|
||||
}
|
||||
|
||||
type GamePlayersTable struct {
|
||||
gamePlayersTable
|
||||
|
||||
EXCLUDED gamePlayersTable
|
||||
}
|
||||
|
||||
// AS creates new GamePlayersTable with assigned alias
|
||||
func (a GamePlayersTable) AS(alias string) *GamePlayersTable {
|
||||
return newGamePlayersTable(a.SchemaName(), a.TableName(), alias)
|
||||
}
|
||||
|
||||
// Schema creates new GamePlayersTable with assigned schema name
|
||||
func (a GamePlayersTable) FromSchema(schemaName string) *GamePlayersTable {
|
||||
return newGamePlayersTable(schemaName, a.TableName(), a.Alias())
|
||||
}
|
||||
|
||||
// WithPrefix creates new GamePlayersTable with assigned table prefix
|
||||
func (a GamePlayersTable) WithPrefix(prefix string) *GamePlayersTable {
|
||||
return newGamePlayersTable(a.SchemaName(), prefix+a.TableName(), a.TableName())
|
||||
}
|
||||
|
||||
// WithSuffix creates new GamePlayersTable with assigned table suffix
|
||||
func (a GamePlayersTable) WithSuffix(suffix string) *GamePlayersTable {
|
||||
return newGamePlayersTable(a.SchemaName(), a.TableName()+suffix, a.TableName())
|
||||
}
|
||||
|
||||
func newGamePlayersTable(schemaName, tableName, alias string) *GamePlayersTable {
|
||||
return &GamePlayersTable{
|
||||
gamePlayersTable: newGamePlayersTableImpl(schemaName, tableName, alias),
|
||||
EXCLUDED: newGamePlayersTableImpl("", "excluded", ""),
|
||||
}
|
||||
}
|
||||
|
||||
func newGamePlayersTableImpl(schemaName, tableName, alias string) gamePlayersTable {
|
||||
var (
|
||||
GameIDColumn = postgres.StringColumn("game_id")
|
||||
SeatColumn = postgres.IntegerColumn("seat")
|
||||
AccountIDColumn = postgres.StringColumn("account_id")
|
||||
ScoreColumn = postgres.IntegerColumn("score")
|
||||
HintsUsedColumn = postgres.IntegerColumn("hints_used")
|
||||
IsWinnerColumn = postgres.BoolColumn("is_winner")
|
||||
allColumns = postgres.ColumnList{GameIDColumn, SeatColumn, AccountIDColumn, ScoreColumn, HintsUsedColumn, IsWinnerColumn}
|
||||
mutableColumns = postgres.ColumnList{AccountIDColumn, ScoreColumn, HintsUsedColumn, IsWinnerColumn}
|
||||
defaultColumns = postgres.ColumnList{ScoreColumn, HintsUsedColumn, IsWinnerColumn}
|
||||
)
|
||||
|
||||
return gamePlayersTable{
|
||||
Table: postgres.NewTable(schemaName, tableName, alias, allColumns...),
|
||||
|
||||
//Columns
|
||||
GameID: GameIDColumn,
|
||||
Seat: SeatColumn,
|
||||
AccountID: AccountIDColumn,
|
||||
Score: ScoreColumn,
|
||||
HintsUsed: HintsUsedColumn,
|
||||
IsWinner: IsWinnerColumn,
|
||||
|
||||
AllColumns: allColumns,
|
||||
MutableColumns: mutableColumns,
|
||||
DefaultColumns: defaultColumns,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
//
|
||||
// Code generated by go-jet DO NOT EDIT.
|
||||
//
|
||||
// WARNING: Changes to this file may cause incorrect behavior
|
||||
// and will be lost if the code is regenerated
|
||||
//
|
||||
|
||||
package table
|
||||
|
||||
import (
|
||||
"github.com/go-jet/jet/v2/postgres"
|
||||
)
|
||||
|
||||
var Games = newGamesTable("backend", "games", "")
|
||||
|
||||
type gamesTable struct {
|
||||
postgres.Table
|
||||
|
||||
// Columns
|
||||
GameID postgres.ColumnString
|
||||
Variant postgres.ColumnString
|
||||
DictVersion postgres.ColumnString
|
||||
Seed postgres.ColumnInteger
|
||||
Status postgres.ColumnString
|
||||
Players postgres.ColumnInteger
|
||||
ToMove postgres.ColumnInteger
|
||||
TurnStartedAt postgres.ColumnTimestampz
|
||||
TurnTimeoutSecs postgres.ColumnInteger
|
||||
HintsAllowed postgres.ColumnBool
|
||||
HintsPerPlayer postgres.ColumnInteger
|
||||
MoveCount postgres.ColumnInteger
|
||||
EndReason postgres.ColumnString
|
||||
CreatedAt postgres.ColumnTimestampz
|
||||
UpdatedAt postgres.ColumnTimestampz
|
||||
FinishedAt postgres.ColumnTimestampz
|
||||
|
||||
AllColumns postgres.ColumnList
|
||||
MutableColumns postgres.ColumnList
|
||||
DefaultColumns postgres.ColumnList
|
||||
}
|
||||
|
||||
type GamesTable struct {
|
||||
gamesTable
|
||||
|
||||
EXCLUDED gamesTable
|
||||
}
|
||||
|
||||
// AS creates new GamesTable with assigned alias
|
||||
func (a GamesTable) AS(alias string) *GamesTable {
|
||||
return newGamesTable(a.SchemaName(), a.TableName(), alias)
|
||||
}
|
||||
|
||||
// Schema creates new GamesTable with assigned schema name
|
||||
func (a GamesTable) FromSchema(schemaName string) *GamesTable {
|
||||
return newGamesTable(schemaName, a.TableName(), a.Alias())
|
||||
}
|
||||
|
||||
// WithPrefix creates new GamesTable with assigned table prefix
|
||||
func (a GamesTable) WithPrefix(prefix string) *GamesTable {
|
||||
return newGamesTable(a.SchemaName(), prefix+a.TableName(), a.TableName())
|
||||
}
|
||||
|
||||
// WithSuffix creates new GamesTable with assigned table suffix
|
||||
func (a GamesTable) WithSuffix(suffix string) *GamesTable {
|
||||
return newGamesTable(a.SchemaName(), a.TableName()+suffix, a.TableName())
|
||||
}
|
||||
|
||||
func newGamesTable(schemaName, tableName, alias string) *GamesTable {
|
||||
return &GamesTable{
|
||||
gamesTable: newGamesTableImpl(schemaName, tableName, alias),
|
||||
EXCLUDED: newGamesTableImpl("", "excluded", ""),
|
||||
}
|
||||
}
|
||||
|
||||
func newGamesTableImpl(schemaName, tableName, alias string) gamesTable {
|
||||
var (
|
||||
GameIDColumn = postgres.StringColumn("game_id")
|
||||
VariantColumn = postgres.StringColumn("variant")
|
||||
DictVersionColumn = postgres.StringColumn("dict_version")
|
||||
SeedColumn = postgres.IntegerColumn("seed")
|
||||
StatusColumn = postgres.StringColumn("status")
|
||||
PlayersColumn = postgres.IntegerColumn("players")
|
||||
ToMoveColumn = postgres.IntegerColumn("to_move")
|
||||
TurnStartedAtColumn = postgres.TimestampzColumn("turn_started_at")
|
||||
TurnTimeoutSecsColumn = postgres.IntegerColumn("turn_timeout_secs")
|
||||
HintsAllowedColumn = postgres.BoolColumn("hints_allowed")
|
||||
HintsPerPlayerColumn = postgres.IntegerColumn("hints_per_player")
|
||||
MoveCountColumn = postgres.IntegerColumn("move_count")
|
||||
EndReasonColumn = postgres.StringColumn("end_reason")
|
||||
CreatedAtColumn = postgres.TimestampzColumn("created_at")
|
||||
UpdatedAtColumn = postgres.TimestampzColumn("updated_at")
|
||||
FinishedAtColumn = postgres.TimestampzColumn("finished_at")
|
||||
allColumns = postgres.ColumnList{GameIDColumn, VariantColumn, DictVersionColumn, SeedColumn, StatusColumn, PlayersColumn, ToMoveColumn, TurnStartedAtColumn, TurnTimeoutSecsColumn, HintsAllowedColumn, HintsPerPlayerColumn, MoveCountColumn, EndReasonColumn, CreatedAtColumn, UpdatedAtColumn, FinishedAtColumn}
|
||||
mutableColumns = postgres.ColumnList{VariantColumn, DictVersionColumn, SeedColumn, StatusColumn, PlayersColumn, ToMoveColumn, TurnStartedAtColumn, TurnTimeoutSecsColumn, HintsAllowedColumn, HintsPerPlayerColumn, MoveCountColumn, EndReasonColumn, CreatedAtColumn, UpdatedAtColumn, FinishedAtColumn}
|
||||
defaultColumns = postgres.ColumnList{StatusColumn, ToMoveColumn, TurnStartedAtColumn, HintsAllowedColumn, HintsPerPlayerColumn, MoveCountColumn, CreatedAtColumn, UpdatedAtColumn}
|
||||
)
|
||||
|
||||
return gamesTable{
|
||||
Table: postgres.NewTable(schemaName, tableName, alias, allColumns...),
|
||||
|
||||
//Columns
|
||||
GameID: GameIDColumn,
|
||||
Variant: VariantColumn,
|
||||
DictVersion: DictVersionColumn,
|
||||
Seed: SeedColumn,
|
||||
Status: StatusColumn,
|
||||
Players: PlayersColumn,
|
||||
ToMove: ToMoveColumn,
|
||||
TurnStartedAt: TurnStartedAtColumn,
|
||||
TurnTimeoutSecs: TurnTimeoutSecsColumn,
|
||||
HintsAllowed: HintsAllowedColumn,
|
||||
HintsPerPlayer: HintsPerPlayerColumn,
|
||||
MoveCount: MoveCountColumn,
|
||||
EndReason: EndReasonColumn,
|
||||
CreatedAt: CreatedAtColumn,
|
||||
UpdatedAt: UpdatedAtColumn,
|
||||
FinishedAt: FinishedAtColumn,
|
||||
|
||||
AllColumns: allColumns,
|
||||
MutableColumns: mutableColumns,
|
||||
DefaultColumns: defaultColumns,
|
||||
}
|
||||
}
|
||||
@@ -10,7 +10,12 @@ package table
|
||||
// UseSchema sets a new schema name for all generated table SQL builder types. It is recommended to invoke
|
||||
// this method only once at the beginning of the program.
|
||||
func UseSchema(schema string) {
|
||||
AccountStats = AccountStats.FromSchema(schema)
|
||||
Accounts = Accounts.FromSchema(schema)
|
||||
Complaints = Complaints.FromSchema(schema)
|
||||
GameMoves = GameMoves.FromSchema(schema)
|
||||
GamePlayers = GamePlayers.FromSchema(schema)
|
||||
Games = Games.FromSchema(schema)
|
||||
Identities = Identities.FromSchema(schema)
|
||||
Sessions = Sessions.FromSchema(schema)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,133 @@
|
||||
-- +goose Up
|
||||
-- Stage 3 game domain: the per-game lifecycle, the dictionary-independent move
|
||||
-- journal, word-check complaints and per-account statistics, plus two account
|
||||
-- columns the game domain needs.
|
||||
SET search_path = backend, pg_catalog;
|
||||
|
||||
-- Extend accounts with the per-user away window (one interval per day, in the
|
||||
-- account's local time_zone) honoured by the turn-timeout sweeper -- and, in a
|
||||
-- later stage, by the robot's sleep -- and a hint wallet (purchasable hints; the
|
||||
-- purchase flow lands later, so the balance defaults to empty). Profile editing
|
||||
-- of the away window arrives with the profile surface (Stage 4).
|
||||
ALTER TABLE accounts
|
||||
ADD COLUMN away_start time NOT NULL DEFAULT '00:00',
|
||||
ADD COLUMN away_end time NOT NULL DEFAULT '07:00',
|
||||
ADD COLUMN hint_balance integer NOT NULL DEFAULT 0,
|
||||
ADD CONSTRAINT accounts_hint_balance_chk CHECK (hint_balance >= 0);
|
||||
|
||||
-- One match. The live position is event-sourced: this row carries the pinned
|
||||
-- dictionary, the bag seed and the denormalised turn cursor the sweeper needs,
|
||||
-- while game_moves is the append-only journal the in-memory engine.Game is
|
||||
-- replayed from (docs/ARCHITECTURE.md §9). turn_timeout_secs is the per-game move
|
||||
-- clock; its allowed values are enforced in Go. variant uses engine.Variant's
|
||||
-- stable labels.
|
||||
CREATE TABLE games (
|
||||
game_id uuid PRIMARY KEY,
|
||||
variant text NOT NULL,
|
||||
dict_version text NOT NULL,
|
||||
seed bigint NOT NULL,
|
||||
status text NOT NULL DEFAULT 'active',
|
||||
players smallint NOT NULL,
|
||||
to_move smallint NOT NULL DEFAULT 0,
|
||||
turn_started_at timestamptz NOT NULL DEFAULT now(),
|
||||
turn_timeout_secs integer NOT NULL,
|
||||
hints_allowed boolean NOT NULL DEFAULT true,
|
||||
hints_per_player smallint NOT NULL DEFAULT 1,
|
||||
move_count integer NOT NULL DEFAULT 0,
|
||||
end_reason text,
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
updated_at timestamptz NOT NULL DEFAULT now(),
|
||||
finished_at timestamptz,
|
||||
CONSTRAINT games_variant_chk CHECK (variant IN ('english', 'russian_scrabble', 'erudit')),
|
||||
CONSTRAINT games_status_chk CHECK (status IN ('active', 'finished')),
|
||||
CONSTRAINT games_players_chk CHECK (players BETWEEN 2 AND 4),
|
||||
CONSTRAINT games_to_move_chk CHECK (to_move >= 0 AND to_move < players),
|
||||
CONSTRAINT games_turn_timeout_chk CHECK (turn_timeout_secs > 0),
|
||||
CONSTRAINT games_hints_per_player_chk CHECK (hints_per_player >= 0),
|
||||
CONSTRAINT games_end_reason_chk CHECK (
|
||||
end_reason IS NULL OR end_reason IN ('out_of_tiles', 'scoreless', 'resign', 'timeout')
|
||||
)
|
||||
);
|
||||
-- The sweeper scans active games oldest-turn-first; a partial index keeps it
|
||||
-- off the finished archive.
|
||||
CREATE INDEX games_active_idx ON games (turn_started_at) WHERE status = 'active';
|
||||
|
||||
-- Seats in turn order (seat 0 moves first), one row per player. account_id is a
|
||||
-- durable account (guests and robots are revisited when they arrive). score is
|
||||
-- the running/final score, is_winner is stamped on finish (false for every seat
|
||||
-- on a draw), hints_used counts the per-game allowance consumed before the
|
||||
-- profile wallet.
|
||||
CREATE TABLE game_players (
|
||||
game_id uuid NOT NULL REFERENCES games (game_id) ON DELETE CASCADE,
|
||||
seat smallint NOT NULL,
|
||||
account_id uuid NOT NULL REFERENCES accounts (account_id),
|
||||
score integer NOT NULL DEFAULT 0,
|
||||
hints_used smallint NOT NULL DEFAULT 0,
|
||||
is_winner boolean NOT NULL DEFAULT false,
|
||||
PRIMARY KEY (game_id, seat),
|
||||
CONSTRAINT game_players_account_key UNIQUE (game_id, account_id)
|
||||
);
|
||||
CREATE INDEX game_players_account_idx ON game_players (account_id);
|
||||
|
||||
-- The append-only, dictionary-independent move journal (docs/ARCHITECTURE.md
|
||||
-- §9.1). seq orders the moves from 0. payload holds the decoded values needed to
|
||||
-- both replay the game through the engine and emit GCG without a dictionary: the
|
||||
-- acting rack, and for a play its direction, placed tiles and formed words; for
|
||||
-- an exchange the swapped tiles. score / running_total / exchanged_count are
|
||||
-- lifted out for cheap history rendering.
|
||||
CREATE TABLE game_moves (
|
||||
game_id uuid NOT NULL REFERENCES games (game_id) ON DELETE CASCADE,
|
||||
seq integer NOT NULL,
|
||||
seat smallint NOT NULL,
|
||||
action text NOT NULL,
|
||||
score integer NOT NULL DEFAULT 0,
|
||||
running_total integer NOT NULL DEFAULT 0,
|
||||
exchanged_count smallint NOT NULL DEFAULT 0,
|
||||
payload text NOT NULL DEFAULT '{}',
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
PRIMARY KEY (game_id, seq),
|
||||
CONSTRAINT game_moves_action_chk CHECK (action IN ('play', 'pass', 'exchange', 'resign', 'timeout'))
|
||||
);
|
||||
|
||||
-- Word-check complaints captured in the context of a game's pinned dictionary.
|
||||
-- The admin review queue and the resolution lifecycle land in Stage 9, which
|
||||
-- owns the status state machine; Stage 3 only ever writes 'open'.
|
||||
CREATE TABLE complaints (
|
||||
complaint_id uuid PRIMARY KEY,
|
||||
complainant_id uuid NOT NULL REFERENCES accounts (account_id),
|
||||
game_id uuid NOT NULL REFERENCES games (game_id) ON DELETE CASCADE,
|
||||
variant text NOT NULL,
|
||||
dict_version text NOT NULL,
|
||||
word text NOT NULL,
|
||||
was_valid boolean NOT NULL,
|
||||
note text NOT NULL DEFAULT '',
|
||||
status text NOT NULL DEFAULT 'open',
|
||||
created_at timestamptz NOT NULL DEFAULT now()
|
||||
);
|
||||
CREATE INDEX complaints_status_idx ON complaints (status);
|
||||
|
||||
-- Per-account lifetime statistics, recomputed incrementally on each game finish.
|
||||
-- Guests have no durable account and never appear here. A draw increments draws
|
||||
-- only (neither wins nor losses). max_word_points is the best single move score
|
||||
-- (which already folds in every word the move formed and the all-tiles bonus).
|
||||
CREATE TABLE account_stats (
|
||||
account_id uuid PRIMARY KEY REFERENCES accounts (account_id) ON DELETE CASCADE,
|
||||
wins integer NOT NULL DEFAULT 0,
|
||||
losses integer NOT NULL DEFAULT 0,
|
||||
draws integer NOT NULL DEFAULT 0,
|
||||
max_game_points integer NOT NULL DEFAULT 0,
|
||||
max_word_points integer NOT NULL DEFAULT 0,
|
||||
updated_at timestamptz NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
-- +goose Down
|
||||
DROP TABLE account_stats;
|
||||
DROP TABLE complaints;
|
||||
DROP TABLE game_moves;
|
||||
DROP TABLE game_players;
|
||||
DROP TABLE games;
|
||||
ALTER TABLE accounts
|
||||
DROP CONSTRAINT accounts_hint_balance_chk,
|
||||
DROP COLUMN hint_balance,
|
||||
DROP COLUMN away_end,
|
||||
DROP COLUMN away_start;
|
||||
Reference in New Issue
Block a user