From 83680b3d5c15448bbb90c8f156a72268376a8782 Mon Sep 17 00:00:00 2001 From: frag223 Date: Fri, 17 Jul 2026 20:19:27 +1000 Subject: [PATCH 01/14] fixing naming --- internal/games/{dice_roll.go => decision_engine.go} | 0 internal/games/{dice_roll_rules.go => decision_engine_rules.go} | 0 .../{dice_roll_rules_test.go => decision_engine_rules_test.go} | 0 internal/games/{dice_roll_test.go => decision_engine_test.go} | 0 4 files changed, 0 insertions(+), 0 deletions(-) rename internal/games/{dice_roll.go => decision_engine.go} (100%) rename internal/games/{dice_roll_rules.go => decision_engine_rules.go} (100%) rename internal/games/{dice_roll_rules_test.go => decision_engine_rules_test.go} (100%) rename internal/games/{dice_roll_test.go => decision_engine_test.go} (100%) diff --git a/internal/games/dice_roll.go b/internal/games/decision_engine.go similarity index 100% rename from internal/games/dice_roll.go rename to internal/games/decision_engine.go diff --git a/internal/games/dice_roll_rules.go b/internal/games/decision_engine_rules.go similarity index 100% rename from internal/games/dice_roll_rules.go rename to internal/games/decision_engine_rules.go diff --git a/internal/games/dice_roll_rules_test.go b/internal/games/decision_engine_rules_test.go similarity index 100% rename from internal/games/dice_roll_rules_test.go rename to internal/games/decision_engine_rules_test.go diff --git a/internal/games/dice_roll_test.go b/internal/games/decision_engine_test.go similarity index 100% rename from internal/games/dice_roll_test.go rename to internal/games/decision_engine_test.go From c2b53cfb8f00e0e7839472db4a9b03ce9b465288 Mon Sep 17 00:00:00 2001 From: frag223 Date: Fri, 17 Jul 2026 20:37:24 +1000 Subject: [PATCH 02/14] fixing game model --- internal/database/game.sql.gen.go | 21 +++--------- internal/database/models.gen.go | 10 +++++- internal/database/queries/game.sql | 5 +-- internal/database/schema/game.sql | 44 ++++++++++++++++---------- internal/games/decision_engine.go | 28 ++-------------- internal/games/decision_engine_test.go | 8 ++--- internal/games/rounds.go | 2 +- internal/games/types.go | 22 +++++++++++++ 8 files changed, 72 insertions(+), 68 deletions(-) diff --git a/internal/database/game.sql.gen.go b/internal/database/game.sql.gen.go index 9e247c3..0396a1a 100644 --- a/internal/database/game.sql.gen.go +++ b/internal/database/game.sql.gen.go @@ -15,7 +15,6 @@ const createGame = `-- name: CreateGame :one insert into games (name, team_a, team_b, - tournament_id, results_log, status, rounds, @@ -24,18 +23,16 @@ values (?, ?, ?, ?, - ?, 'pending', ?, ?) -returning id, name, tournament_id, team_a, team_b, winner, status, rounds, current_round, results_log, created_at, updated_at +returning id, name, team_a, team_b, winner, status, rounds, current_round, results_log, created_at, updated_at ` type CreateGameParams struct { Name string TeamA sql.NullInt64 TeamB sql.NullInt64 - TournamentID sql.NullInt64 ResultsLog json.RawMessage Rounds json.RawMessage CurrentRound int64 @@ -46,7 +43,6 @@ func (q *Queries) CreateGame(ctx context.Context, arg CreateGameParams) (Game, e arg.Name, arg.TeamA, arg.TeamB, - arg.TournamentID, arg.ResultsLog, arg.Rounds, arg.CurrentRound, @@ -55,7 +51,6 @@ func (q *Queries) CreateGame(ctx context.Context, arg CreateGameParams) (Game, e err := row.Scan( &i.ID, &i.Name, - &i.TournamentID, &i.TeamA, &i.TeamB, &i.Winner, @@ -70,7 +65,7 @@ func (q *Queries) CreateGame(ctx context.Context, arg CreateGameParams) (Game, e } const getGameByID = `-- name: GetGameByID :one -select id, name, tournament_id, team_a, team_b, winner, status, rounds, current_round, results_log, created_at, updated_at +select id, name, team_a, team_b, winner, status, rounds, current_round, results_log, created_at, updated_at from games where id = ? ` @@ -81,7 +76,6 @@ func (q *Queries) GetGameByID(ctx context.Context, id int64) (Game, error) { err := row.Scan( &i.ID, &i.Name, - &i.TournamentID, &i.TeamA, &i.TeamB, &i.Winner, @@ -96,7 +90,7 @@ func (q *Queries) GetGameByID(ctx context.Context, id int64) (Game, error) { } const listCompletedGamesByTeam = `-- name: ListCompletedGamesByTeam :many -select id, name, tournament_id, team_a, team_b, winner, status, rounds, current_round, results_log, created_at, updated_at +select id, name, team_a, team_b, winner, status, rounds, current_round, results_log, created_at, updated_at from games where status = 'complete' and (team_a = ? or team_b = ?) @@ -120,7 +114,6 @@ func (q *Queries) ListCompletedGamesByTeam(ctx context.Context, arg ListComplete if err := rows.Scan( &i.ID, &i.Name, - &i.TournamentID, &i.TeamA, &i.TeamB, &i.Winner, @@ -153,10 +146,9 @@ set name = ?, status = ?, results_log = ?, rounds = ?, - current_round = ?, - tournament_id = ? + current_round = ? where id = ? -returning id, name, tournament_id, team_a, team_b, winner, status, rounds, current_round, results_log, created_at, updated_at +returning id, name, team_a, team_b, winner, status, rounds, current_round, results_log, created_at, updated_at ` type UpdateGameParams struct { @@ -168,7 +160,6 @@ type UpdateGameParams struct { ResultsLog json.RawMessage Rounds json.RawMessage CurrentRound int64 - TournamentID sql.NullInt64 ID int64 } @@ -182,14 +173,12 @@ func (q *Queries) UpdateGame(ctx context.Context, arg UpdateGameParams) (Game, e arg.ResultsLog, arg.Rounds, arg.CurrentRound, - arg.TournamentID, arg.ID, ) var i Game err := row.Scan( &i.ID, &i.Name, - &i.TournamentID, &i.TeamA, &i.TeamB, &i.Winner, diff --git a/internal/database/models.gen.go b/internal/database/models.gen.go index 946587c..f244a82 100644 --- a/internal/database/models.gen.go +++ b/internal/database/models.gen.go @@ -22,7 +22,6 @@ type Coach struct { type Game struct { ID int64 Name string - TournamentID sql.NullInt64 TeamA sql.NullInt64 TeamB sql.NullInt64 Winner sql.NullInt64 @@ -67,3 +66,12 @@ type Tournament struct { CreatedAt sql.NullTime UpdatedAt sql.NullTime } + +type TournamentGame struct { + ID int64 + TournamentID sql.NullInt64 + GameID sql.NullInt64 + Stage string + CreatedAt sql.NullTime + UpdatedAt sql.NullTime +} diff --git a/internal/database/queries/game.sql b/internal/database/queries/game.sql index b64fb14..a6addfc 100644 --- a/internal/database/queries/game.sql +++ b/internal/database/queries/game.sql @@ -2,7 +2,6 @@ insert into games (name, team_a, team_b, - tournament_id, results_log, status, rounds, @@ -11,7 +10,6 @@ values (?, ?, ?, ?, - ?, 'pending', ?, ?) @@ -31,8 +29,7 @@ set name = ?, status = ?, results_log = ?, rounds = ?, - current_round = ?, - tournament_id = ? + current_round = ? where id = ? returning *; diff --git a/internal/database/schema/game.sql b/internal/database/schema/game.sql index 4a23c88..28b1d2a 100644 --- a/internal/database/schema/game.sql +++ b/internal/database/schema/game.sql @@ -1,23 +1,33 @@ - -create table if not exists tournaments ( - id integer primary key autoincrement, - name text not null, +create table if not exists tournaments +( + id integer primary key autoincrement, + name text not null, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); -create table if not exists games ( - id integer primary key autoincrement, - name text not null, - tournament_id integer references tournaments(id), - team_a integer references teams(id), - team_b integer references teams(id), - winner integer references teams(id), - status varchar(255) not null, - rounds text not null, - current_round integer not null default 0, - results_log text not null, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +create table if not exists tournament_games +( + id integer primary key autoincrement, + tournament_id integer references tournaments (id), + game_id integer references games (id), + stage text not null, + created_at timestamp default CURRENT_TIMESTAMP, + updated_at timestamp default CURRENT_TIMESTAMP +); + +create table if not exists games +( + id integer primary key autoincrement, + name text not null, + team_a integer references teams (id), + team_b integer references teams (id), + winner integer references teams (id), + status varchar(255) not null, + rounds text not null, + current_round integer not null default 0, + results_log text not null, + created_at timestamp default CURRENT_TIMESTAMP, + updated_at timestamp default CURRENT_TIMESTAMP ); diff --git a/internal/games/decision_engine.go b/internal/games/decision_engine.go index 736e387..f558ac0 100644 --- a/internal/games/decision_engine.go +++ b/internal/games/decision_engine.go @@ -12,30 +12,8 @@ func DiceRoll() int { return rand.IntN(6) + 1 } -type TeamDecisionInput struct { - triggeredAugment augments.Name - passivesAugments []augments.Effect - player int64 - roll int -} - -type DecisionInput struct { - lastRound *DuelResult - teamA TeamDecisionInput - teamB TeamDecisionInput -} - -type DecisionEngineFunc func(input DecisionInput) DecisionInput - -type Engine struct { - beforeRoll []DecisionEngineFunc - afterRoll []DecisionEngineFunc - afterAugments []DecisionEngineFunc - rollFn RollFn -} - -func NewDecisionEngine() *Engine { - return &Engine{ +func NewDecisionEngine() *DecisionEngine { + return &DecisionEngine{ beforeRoll: []DecisionEngineFunc{ RulePocketSand, }, @@ -58,7 +36,7 @@ func NewDecisionEngine() *Engine { } } -func (e *Engine) Run(input DecisionInput) DuelResult { +func (e *DecisionEngine) Run(input DecisionInput) DuelResult { for _, ruleFn := range e.beforeRoll { input = ruleFn(input) } diff --git a/internal/games/decision_engine_test.go b/internal/games/decision_engine_test.go index b91e03c..7f73dd4 100644 --- a/internal/games/decision_engine_test.go +++ b/internal/games/decision_engine_test.go @@ -16,7 +16,7 @@ func TestEngineRun(t *testing.T) { afterCalled := false afterAugmentsCalled := false - engine := &Engine{ + engine := &DecisionEngine{ beforeRoll: []DecisionEngineFunc{ func(input DecisionInput) DecisionInput { beforeCalled = true @@ -54,7 +54,7 @@ func TestEngineRun(t *testing.T) { Test("should trigger twist of fate effect when rule matches", func(t *testing.T) { secondRolls := newSequentialRollFn([]int{6}) - engine := &Engine{ + engine := &DecisionEngine{ beforeRoll: []DecisionEngineFunc{}, afterRoll: []DecisionEngineFunc{ func(input DecisionInput) DecisionInput { @@ -82,7 +82,7 @@ func TestEngineRun(t *testing.T) { Test("should not trigger twist of fate effect when rule does not match", func(t *testing.T) { secondRolls := newSequentialRollFn([]int{6}) - engine := &Engine{ + engine := &DecisionEngine{ beforeRoll: []DecisionEngineFunc{}, afterRoll: []DecisionEngineFunc{ func(input DecisionInput) DecisionInput { @@ -108,7 +108,7 @@ func TestEngineRun(t *testing.T) { odize.AssertEqual(t, 3, result.RollDelta) }). Test("should return draw when both final rolls are equal", func(t *testing.T) { - engine := &Engine{ + engine := &DecisionEngine{ beforeRoll: []DecisionEngineFunc{}, afterRoll: []DecisionEngineFunc{}, afterAugments: []DecisionEngineFunc{}, diff --git a/internal/games/rounds.go b/internal/games/rounds.go index eaff6ee..baa1d5f 100644 --- a/internal/games/rounds.go +++ b/internal/games/rounds.go @@ -22,7 +22,7 @@ func (r *Round) FillSquad(a LanesConfig, b LanesConfig) { func (r *Round) ResolveLanes(rollFn RollStrategy) RoundResult { var result []RoundResult - for lane := 0; lane < len(r.TeamA.Lanes); lane++ { + for lane := range len(r.TeamA.Lanes) { laneResult := r.ResolveLane(lane, rollFn) result = append(result, laneResult) } diff --git a/internal/games/types.go b/internal/games/types.go index 89477e9..d95c66d 100644 --- a/internal/games/types.go +++ b/internal/games/types.go @@ -113,3 +113,25 @@ var ( ErrNoRounds = errors.New("no rounds left") ErrGameNotComplete = errors.New("game not complete") ) + +type TeamDecisionInput struct { + triggeredAugment augments.Name + passivesAugments []augments.Effect + player int64 + roll int +} + +type DecisionInput struct { + lastRound *DuelResult + teamA TeamDecisionInput + teamB TeamDecisionInput +} + +type DecisionEngineFunc func(input DecisionInput) DecisionInput + +type DecisionEngine struct { + beforeRoll []DecisionEngineFunc + afterRoll []DecisionEngineFunc + afterAugments []DecisionEngineFunc + rollFn RollFn +} From 42f486aaa108885c4770dd00d1257b539a72cfa7 Mon Sep 17 00:00:00 2001 From: frag223 Date: Fri, 17 Jul 2026 21:15:34 +1000 Subject: [PATCH 03/14] adding lifecycle to game to manage tournament mode --- internal/database/game.sql.gen.go | 11 +++++++++++ internal/database/queries/game.sql | 5 +++++ internal/games/game.go | 13 ++----------- internal/games/interfaces.go | 1 + internal/games/service.go | 13 ++++--------- internal/games/types.go | 1 - internal/tournaments/tournaments.go | 5 +++++ internal/tournaments/types.go | 18 ++++++++++++++++++ internal/ui/uibattle/page_battle_confirm.go | 4 ++++ 9 files changed, 50 insertions(+), 21 deletions(-) create mode 100644 internal/tournaments/tournaments.go create mode 100644 internal/tournaments/types.go diff --git a/internal/database/game.sql.gen.go b/internal/database/game.sql.gen.go index 0396a1a..6f000bf 100644 --- a/internal/database/game.sql.gen.go +++ b/internal/database/game.sql.gen.go @@ -137,6 +137,17 @@ func (q *Queries) ListCompletedGamesByTeam(ctx context.Context, arg ListComplete return items, nil } +const startGame = `-- name: StartGame :exec +update games +set status = 'running' +where id = ? +` + +func (q *Queries) StartGame(ctx context.Context, id int64) error { + _, err := q.db.ExecContext(ctx, startGame, id) + return err +} + const updateGame = `-- name: UpdateGame :one update games set name = ?, diff --git a/internal/database/queries/game.sql b/internal/database/queries/game.sql index a6addfc..364973e 100644 --- a/internal/database/queries/game.sql +++ b/internal/database/queries/game.sql @@ -20,6 +20,11 @@ select * from games where id = ?; +-- name: StartGame :exec +update games +set status = 'running' +where id = ?; + -- name: UpdateGame :one update games set name = ?, diff --git a/internal/games/game.go b/internal/games/game.go index 9147739..00118dc 100644 --- a/internal/games/game.go +++ b/internal/games/game.go @@ -139,7 +139,6 @@ func fromGameModel(m database.Game) (Game, error) { return Game{ id: m.ID, name: m.Name, - tournamentID: &m.TournamentID.Int64, teamA: m.TeamA.Int64, teamB: m.TeamB.Int64, winner: &m.Winner.Int64, @@ -153,13 +152,6 @@ func fromGameModel(m database.Game) (Game, error) { } func toGameModel(g Game) (database.Game, error) { - resolvedTournamentID := sql.NullInt64{} - if g.tournamentID != nil { - resolvedTournamentID = sql.NullInt64{ - Int64: *g.tournamentID, - Valid: true, - } - } resolvedWinner := sql.NullInt64{} if g.winner != nil { @@ -180,9 +172,8 @@ func toGameModel(g Game) (database.Game, error) { } return database.Game{ - ID: g.id, - Name: g.name, - TournamentID: resolvedTournamentID, + ID: g.id, + Name: g.name, TeamA: sql.NullInt64{ Int64: g.teamA, Valid: true, diff --git a/internal/games/interfaces.go b/internal/games/interfaces.go index 72f59c0..e1f736a 100644 --- a/internal/games/interfaces.go +++ b/internal/games/interfaces.go @@ -11,6 +11,7 @@ type Store interface { GetGameByID(ctx context.Context, id int64) (database.Game, error) ListCompletedGamesByTeam(ctx context.Context, arg database.ListCompletedGamesByTeamParams) ([]database.Game, error) UpdateGame(ctx context.Context, arg database.UpdateGameParams) (database.Game, error) + StartGame(ctx context.Context, id int64) error } type RollStrategy interface { diff --git a/internal/games/service.go b/internal/games/service.go index 8662677..7a6d762 100644 --- a/internal/games/service.go +++ b/internal/games/service.go @@ -22,13 +22,6 @@ type NewGameParams struct { } func (s *Service) NewGame(ctx context.Context, params NewGameParams) (Game, error) { - resolvedTournamentID := sql.NullInt64{} - if params.TournamentID != nil { - resolvedTournamentID = sql.NullInt64{ - Int64: *params.TournamentID, - Valid: true, - } - } roundsJsonData, rErr := json.Marshal(generateRounds(params.TeamA, params.TeamB)) if rErr != nil { @@ -45,7 +38,6 @@ func (s *Service) NewGame(ctx context.Context, params NewGameParams) (Game, erro Int64: params.TeamB.TeamID, Valid: true, }, - TournamentID: resolvedTournamentID, ResultsLog: []byte(`[]`), Rounds: roundsJsonData, CurrentRound: 0, @@ -72,7 +64,6 @@ func (s *Service) UpdateGame(ctx context.Context, game Game) (Game, error) { ResultsLog: model.ResultsLog, Rounds: model.Rounds, CurrentRound: model.CurrentRound, - TournamentID: model.TournamentID, ID: model.ID, }) if err != nil { @@ -91,6 +82,10 @@ func (s *Service) GetGame(ctx context.Context, id int64) (Game, error) { return fromGameModel(model) } +func (s *Service) StartGame(ctx context.Context, id int64) error { + return s.Store.StartGame(ctx, id) +} + func (s *Service) CompleteGame(ctx context.Context, game Game) (Game, error) { game.status = StatusComplete diff --git a/internal/games/types.go b/internal/games/types.go index d95c66d..1ee3929 100644 --- a/internal/games/types.go +++ b/internal/games/types.go @@ -31,7 +31,6 @@ const ( type Game struct { id int64 name string - tournamentID *int64 teamA int64 teamB int64 winner *int64 diff --git a/internal/tournaments/tournaments.go b/internal/tournaments/tournaments.go new file mode 100644 index 0000000..f0710d4 --- /dev/null +++ b/internal/tournaments/tournaments.go @@ -0,0 +1,5 @@ +package tournaments + +func NewTournament() *Service { + return &Service{} +} diff --git a/internal/tournaments/types.go b/internal/tournaments/types.go new file mode 100644 index 0000000..a2eb14d --- /dev/null +++ b/internal/tournaments/types.go @@ -0,0 +1,18 @@ +package tournaments + +import "github.com/code-gorilla-au/rush/internal/games" + +type Tournament struct { + ID int64 `json:"id"` + Name string `json:"name"` + Stages []Stages `json:"stages"` +} + +type Stages struct { + ID int64 `json:"id"` + Name string `json:"name"` + Games []games.Game `json:"games"` +} + +type Service struct { +} diff --git a/internal/ui/uibattle/page_battle_confirm.go b/internal/ui/uibattle/page_battle_confirm.go index e6f2bf1..b7cc1a2 100644 --- a/internal/ui/uibattle/page_battle_confirm.go +++ b/internal/ui/uibattle/page_battle_confirm.go @@ -108,6 +108,10 @@ func (m *PageBattleConfirmModel) createGame() tea.Msg { return err } + if err = m.gameSvc.StartGame(m.globalState.Context(), game.ID()); err != nil { + return fmt.Errorf("could not start game: %w", err) + } + return uistate.MsgSwitchPage{ NewPage: uistate.PageGame, GameID: game.ID(), From de3ab806fe2d1432c093ee2277f968ee04a04348 Mon Sep 17 00:00:00 2001 From: frag223 Date: Fri, 17 Jul 2026 21:45:44 +1000 Subject: [PATCH 04/14] adding base schema --- internal/database/models.gen.go | 9 +++++---- internal/database/schema/game.sql | 1 + internal/tournaments/types.go | 29 +++++++++++++++++++++++------ 3 files changed, 29 insertions(+), 10 deletions(-) diff --git a/internal/database/models.gen.go b/internal/database/models.gen.go index f244a82..18efc04 100644 --- a/internal/database/models.gen.go +++ b/internal/database/models.gen.go @@ -61,10 +61,11 @@ type Team struct { } type Tournament struct { - ID int64 - Name string - CreatedAt sql.NullTime - UpdatedAt sql.NullTime + ID int64 + Name string + NumberOfTeams int64 + CreatedAt sql.NullTime + UpdatedAt sql.NullTime } type TournamentGame struct { diff --git a/internal/database/schema/game.sql b/internal/database/schema/game.sql index 28b1d2a..f71750f 100644 --- a/internal/database/schema/game.sql +++ b/internal/database/schema/game.sql @@ -2,6 +2,7 @@ create table if not exists tournaments ( id integer primary key autoincrement, name text not null, + number_of_teams integer not null, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); diff --git a/internal/tournaments/types.go b/internal/tournaments/types.go index a2eb14d..68a5328 100644 --- a/internal/tournaments/types.go +++ b/internal/tournaments/types.go @@ -2,16 +2,33 @@ package tournaments import "github.com/code-gorilla-au/rush/internal/games" +type NumberOfTeams int64 + +const ( + Four NumberOfTeams = 4 + Eight NumberOfTeams = 8 +) + type Tournament struct { - ID int64 `json:"id"` - Name string `json:"name"` - Stages []Stages `json:"stages"` + ID int64 `json:"id"` + Name string `json:"name"` + Number NumberOfTeams `json:"number_of_teams"` + Stages []Stages `json:"stages"` } +type StageStatus string + +const ( + StageStatusActive StageStatus = "active" + StageStatusPending StageStatus = "pending" + StageStatusComplete StageStatus = "complete" +) + type Stages struct { - ID int64 `json:"id"` - Name string `json:"name"` - Games []games.Game `json:"games"` + ID int64 `json:"id"` + Name string `json:"name"` + Status StageStatus `json:"status"` + Games []games.Game `json:"games"` } type Service struct { From 514bb9ab4b4ee9141f7063ec64c314df1900ace1 Mon Sep 17 00:00:00 2001 From: frag223 Date: Sat, 18 Jul 2026 08:38:02 +1000 Subject: [PATCH 05/14] wip refactoring model --- cmd/rush/main.go | 10 +++ internal/database/db.go | 19 ++++++ internal/database/game.sql.gen.go | 101 ++++++++++++++++++++++++++++ internal/database/models.gen.go | 25 ++++--- internal/database/queries/game.sql | 26 ++++++- internal/database/schema/game.sql | 24 ++++--- internal/teams/ai_teams.go | 2 +- internal/teams/service.go | 2 +- internal/tournaments/interfaces.go | 14 ++++ internal/tournaments/tournaments.go | 43 +++++++++++- internal/tournaments/types.go | 12 +++- 11 files changed, 255 insertions(+), 23 deletions(-) create mode 100644 internal/tournaments/interfaces.go diff --git a/cmd/rush/main.go b/cmd/rush/main.go index a0515fe..ee2552f 100644 --- a/cmd/rush/main.go +++ b/cmd/rush/main.go @@ -2,6 +2,7 @@ package main import ( "context" + "database/sql" "log/slog" "os" @@ -11,6 +12,7 @@ import ( "github.com/code-gorilla-au/rush/internal/games" "github.com/code-gorilla-au/rush/internal/playbooks" "github.com/code-gorilla-au/rush/internal/teams" + "github.com/code-gorilla-au/rush/internal/tournaments" "github.com/code-gorilla-au/rush/internal/ui" ) @@ -42,6 +44,14 @@ func main() { playbooksSvc := playbooks.NewPlaybooksService(queries) teamsSvc := teams.NewTeamsService(queries, playbooksSvc) gameSvc := games.NewService(queries) + _ = tournaments.NewTournament(tournaments.ServiceDependencies{ + GamesSvc: gameSvc, + TeamsSvc: teamsSvc, + Store: queries, + TxnFunc: func(db *sql.Tx) tournaments.Store { + return database.New(db) + }, + }) go func() { hasAICoaches, tErr := teamsSvc.HasAICoaches(ctx) diff --git a/internal/database/db.go b/internal/database/db.go index 36936fd..6678d84 100644 --- a/internal/database/db.go +++ b/internal/database/db.go @@ -2,6 +2,7 @@ package database import ( "database/sql" + "fmt" _ "modernc.org/sqlite" ) @@ -15,3 +16,21 @@ func NewSqLiteProvider(dbPath string) (*sql.DB, error) { return db, nil } + +func WithTxnCtx(db *sql.DB, fn func(tx *sql.Tx) error) error { + tx, err := db.Begin() + if err != nil { + return err + } + + err = fn(tx) + if err != nil { + if rErr := tx.Rollback(); rErr != nil { + return fmt.Errorf("tx rollback failed: %v", rErr) + } + + return fmt.Errorf("txn rolled back due to error: %w", err) + } + + return tx.Commit() +} diff --git a/internal/database/game.sql.gen.go b/internal/database/game.sql.gen.go index 6f000bf..6140d2a 100644 --- a/internal/database/game.sql.gen.go +++ b/internal/database/game.sql.gen.go @@ -11,6 +11,30 @@ import ( "encoding/json" ) +const allocateGameToStage = `-- name: AllocateGameToStage :one +insert into stage_games (stage_id, game_id) +values (?, ?) +returning id, stage_id, game_id, created_at, updated_at +` + +type AllocateGameToStageParams struct { + StageID sql.NullInt64 + GameID sql.NullInt64 +} + +func (q *Queries) AllocateGameToStage(ctx context.Context, arg AllocateGameToStageParams) (StageGame, error) { + row := q.db.QueryRowContext(ctx, allocateGameToStage, arg.StageID, arg.GameID) + var i StageGame + err := row.Scan( + &i.ID, + &i.StageID, + &i.GameID, + &i.CreatedAt, + &i.UpdatedAt, + ) + return i, err +} + const createGame = `-- name: CreateGame :one insert into games (name, team_a, @@ -64,6 +88,56 @@ func (q *Queries) CreateGame(ctx context.Context, arg CreateGameParams) (Game, e return i, err } +const createStage = `-- name: CreateStage :one +insert into stages (name, status) +values (?, ?) +returning id, name, status, created_at, updated_at +` + +type CreateStageParams struct { + Name string + Status string +} + +func (q *Queries) CreateStage(ctx context.Context, arg CreateStageParams) (Stage, error) { + row := q.db.QueryRowContext(ctx, createStage, arg.Name, arg.Status) + var i Stage + err := row.Scan( + &i.ID, + &i.Name, + &i.Status, + &i.CreatedAt, + &i.UpdatedAt, + ) + return i, err +} + +const createTournament = `-- name: CreateTournament :one +insert into tournaments (name, + number_of_teams) +values (?, + ?) +returning id, name, number_of_teams, created_at, updated_at +` + +type CreateTournamentParams struct { + Name string + NumberOfTeams int64 +} + +func (q *Queries) CreateTournament(ctx context.Context, arg CreateTournamentParams) (Tournament, error) { + row := q.db.QueryRowContext(ctx, createTournament, arg.Name, arg.NumberOfTeams) + var i Tournament + err := row.Scan( + &i.ID, + &i.Name, + &i.NumberOfTeams, + &i.CreatedAt, + &i.UpdatedAt, + ) + return i, err +} + const getGameByID = `-- name: GetGameByID :one select id, name, team_a, team_b, winner, status, rounds, current_round, results_log, created_at, updated_at from games @@ -202,3 +276,30 @@ func (q *Queries) UpdateGame(ctx context.Context, arg UpdateGameParams) (Game, e ) return i, err } + +const updateStage = `-- name: UpdateStage :one +update stages +set name = ?, + status = ? +where id = ? +returning id, name, status, created_at, updated_at +` + +type UpdateStageParams struct { + Name string + Status string + ID int64 +} + +func (q *Queries) UpdateStage(ctx context.Context, arg UpdateStageParams) (Stage, error) { + row := q.db.QueryRowContext(ctx, updateStage, arg.Name, arg.Status, arg.ID) + var i Stage + err := row.Scan( + &i.ID, + &i.Name, + &i.Status, + &i.CreatedAt, + &i.UpdatedAt, + ) + return i, err +} diff --git a/internal/database/models.gen.go b/internal/database/models.gen.go index 18efc04..9cc3452 100644 --- a/internal/database/models.gen.go +++ b/internal/database/models.gen.go @@ -51,6 +51,22 @@ type Player struct { UpdatedAt sql.NullTime } +type Stage struct { + ID int64 + Name string + Status string + CreatedAt sql.NullTime + UpdatedAt sql.NullTime +} + +type StageGame struct { + ID int64 + StageID sql.NullInt64 + GameID sql.NullInt64 + CreatedAt sql.NullTime + UpdatedAt sql.NullTime +} + type Team struct { ID int64 Name string @@ -67,12 +83,3 @@ type Tournament struct { CreatedAt sql.NullTime UpdatedAt sql.NullTime } - -type TournamentGame struct { - ID int64 - TournamentID sql.NullInt64 - GameID sql.NullInt64 - Stage string - CreatedAt sql.NullTime - UpdatedAt sql.NullTime -} diff --git a/internal/database/queries/game.sql b/internal/database/queries/game.sql index 364973e..df9a7d3 100644 --- a/internal/database/queries/game.sql +++ b/internal/database/queries/game.sql @@ -43,4 +43,28 @@ select * from games where status = 'complete' and (team_a = ? or team_b = ?) -order by updated_at desc; \ No newline at end of file +order by updated_at desc; + +-- name: CreateTournament :one +insert into tournaments (name, + number_of_teams) +values (?, + ?) +returning *; + +-- name: CreateStage :one +insert into stages (name, status) +values (?, ?) +returning *; + +-- name: AllocateGameToStage :one +insert into stage_games (stage_id, game_id) +values (?, ?) +returning *; + +-- name: UpdateStage :one +update stages +set name = ?, + status = ? +where id = ? +returning *; \ No newline at end of file diff --git a/internal/database/schema/game.sql b/internal/database/schema/game.sql index f71750f..879087f 100644 --- a/internal/database/schema/game.sql +++ b/internal/database/schema/game.sql @@ -1,20 +1,28 @@ create table if not exists tournaments +( + id integer primary key autoincrement, + name text not null, + number_of_teams integer not null, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +create table if not exists stages ( id integer primary key autoincrement, name text not null, - number_of_teams integer not null, + status text not null, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); -create table if not exists tournament_games +create table if not exists stage_games ( - id integer primary key autoincrement, - tournament_id integer references tournaments (id), - game_id integer references games (id), - stage text not null, - created_at timestamp default CURRENT_TIMESTAMP, - updated_at timestamp default CURRENT_TIMESTAMP + id integer primary key autoincrement, + stage_id integer references stages (id), + game_id integer references games (id), + created_at timestamp default CURRENT_TIMESTAMP, + updated_at timestamp default CURRENT_TIMESTAMP ); create table if not exists games diff --git a/internal/teams/ai_teams.go b/internal/teams/ai_teams.go index c730a82..dce0bbe 100644 --- a/internal/teams/ai_teams.go +++ b/internal/teams/ai_teams.go @@ -248,7 +248,7 @@ func generateAITeams() ([]AIGenerationParams, error) { nameGen := NewTeamNameGenerator() teamNames := nameGen.GenerateUnique(totalTeams) - for i := 0; i < totalTeams; i++ { + for i := range totalTeams { tmpTeam := AIGenerationParams{} if err := faker.FakeData(&tmpTeam); err != nil { diff --git a/internal/teams/service.go b/internal/teams/service.go index 332eee7..8ae36ea 100644 --- a/internal/teams/service.go +++ b/internal/teams/service.go @@ -181,7 +181,7 @@ type createPlayerParams struct { func (s *Service) createPlayers(ctx context.Context, teamID int64) ([]database.Player, error) { modelPlayers := make([]database.Player, 5) - for i := 0; i < 5; i++ { + for i := range 5 { playerName := createPlayerParams{} if err := faker.FakeData(&playerName); err != nil { diff --git a/internal/tournaments/interfaces.go b/internal/tournaments/interfaces.go new file mode 100644 index 0000000..2cde5d8 --- /dev/null +++ b/internal/tournaments/interfaces.go @@ -0,0 +1,14 @@ +package tournaments + +import ( + "context" + + "github.com/code-gorilla-au/rush/internal/database" +) + +type Store interface { + AllocateGameToStage(ctx context.Context, arg database.AllocateGameToStageParams) (database.StageGame, error) + CreateStage(ctx context.Context, arg database.CreateStageParams) (database.Stage, error) + CreateTournament(ctx context.Context, arg database.CreateTournamentParams) (database.Tournament, error) + UpdateStage(ctx context.Context, arg database.UpdateStageParams) (database.Stage, error) +} diff --git a/internal/tournaments/tournaments.go b/internal/tournaments/tournaments.go index f0710d4..d2312f0 100644 --- a/internal/tournaments/tournaments.go +++ b/internal/tournaments/tournaments.go @@ -1,5 +1,44 @@ package tournaments -func NewTournament() *Service { - return &Service{} +import ( + "context" + "database/sql" + + "github.com/code-gorilla-au/rush/internal/games" + "github.com/code-gorilla-au/rush/internal/teams" +) + +type ServiceDependencies struct { + GamesSvc *games.Service + TeamsSvc *teams.Service + Store Store + DB *sql.DB + TxnFunc func(db *sql.Tx) Store +} + +func NewTournament(deps ServiceDependencies) *Service { + return &Service{ + gamesSvc: deps.GamesSvc, + teamsSvc: deps.TeamsSvc, + store: deps.Store, + DB: deps.DB, + txnFunc: deps.TxnFunc, + } +} + +func (s *Service) CreateTournament(ctx context.Context, coachId int64, numberOfTeams NumberOfTeams) error { + return nil +} + +func (s *Service) getTeamsForTournament(ctx context.Context, coachId int64) ([]teams.Team, error) { + var totalTeams []teams.Team + + humanTeam, err := s.teamsSvc.GetTeamByCoachID(ctx, coachId) + if err != nil { + return totalTeams, err + } + + totalTeams = append(totalTeams, humanTeam) + + return totalTeams, nil } diff --git a/internal/tournaments/types.go b/internal/tournaments/types.go index 68a5328..2e9259b 100644 --- a/internal/tournaments/types.go +++ b/internal/tournaments/types.go @@ -1,6 +1,11 @@ package tournaments -import "github.com/code-gorilla-au/rush/internal/games" +import ( + "database/sql" + + "github.com/code-gorilla-au/rush/internal/games" + "github.com/code-gorilla-au/rush/internal/teams" +) type NumberOfTeams int64 @@ -32,4 +37,9 @@ type Stages struct { } type Service struct { + teamsSvc *teams.Service + gamesSvc *games.Service + store Store + DB *sql.DB + txnFunc func(db *sql.Tx) Store } From 77d3895ca813a517b2a8b71d25287efb21275672 Mon Sep 17 00:00:00 2001 From: frag223 Date: Sun, 19 Jul 2026 07:19:34 +1000 Subject: [PATCH 06/14] adding dependabot --- .github/dependabot.yaml | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 .github/dependabot.yaml diff --git a/.github/dependabot.yaml b/.github/dependabot.yaml new file mode 100644 index 0000000..fe49577 --- /dev/null +++ b/.github/dependabot.yaml @@ -0,0 +1,20 @@ +--- +version: 2 +updates: + - package-ecosystem: gomod + directory: / + schedule: + interval: monthly + groups: + prod: + patterns: + - "*" + + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "monthly" + groups: + prod: + patterns: + - "*" \ No newline at end of file From 6362126328f74f3f52565cbfe3b5557f8d2e6bdd Mon Sep 17 00:00:00 2001 From: frag223 Date: Sun, 19 Jul 2026 15:09:37 +1000 Subject: [PATCH 07/14] small changes --- cmd/rush/main.go | 4 ++-- internal/games/rounds.go | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/cmd/rush/main.go b/cmd/rush/main.go index ee2552f..3f0177f 100644 --- a/cmd/rush/main.go +++ b/cmd/rush/main.go @@ -48,8 +48,8 @@ func main() { GamesSvc: gameSvc, TeamsSvc: teamsSvc, Store: queries, - TxnFunc: func(db *sql.Tx) tournaments.Store { - return database.New(db) + TxnFunc: func(txDB *sql.Tx) tournaments.Store { + return database.New(txDB) }, }) diff --git a/internal/games/rounds.go b/internal/games/rounds.go index baa1d5f..8d5d682 100644 --- a/internal/games/rounds.go +++ b/internal/games/rounds.go @@ -178,7 +178,7 @@ func (s *TeamFormation) FillLanes(f LanesConfig) { func (s *TeamFormation) LaneFill(lane int, players int, teamPlayers []int64) []int64 { remainder := teamPlayers - for i := 0; i < players; i++ { + for range players { if len(remainder) == 0 { break } From 6bb807b420c31867d4a7c595ae829c92bc7a734e Mon Sep 17 00:00:00 2001 From: frag223 Date: Mon, 20 Jul 2026 07:29:22 +1000 Subject: [PATCH 08/14] wip adding change --- cmd/rush/main.go | 2 +- internal/tournaments/tournaments.go | 20 +++++++++++++------- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/cmd/rush/main.go b/cmd/rush/main.go index 3f0177f..f2140f3 100644 --- a/cmd/rush/main.go +++ b/cmd/rush/main.go @@ -44,7 +44,7 @@ func main() { playbooksSvc := playbooks.NewPlaybooksService(queries) teamsSvc := teams.NewTeamsService(queries, playbooksSvc) gameSvc := games.NewService(queries) - _ = tournaments.NewTournament(tournaments.ServiceDependencies{ + _ = tournaments.NewService(tournaments.ServiceDependencies{ GamesSvc: gameSvc, TeamsSvc: teamsSvc, Store: queries, diff --git a/internal/tournaments/tournaments.go b/internal/tournaments/tournaments.go index d2312f0..d6affdf 100644 --- a/internal/tournaments/tournaments.go +++ b/internal/tournaments/tournaments.go @@ -3,6 +3,8 @@ package tournaments import ( "context" "database/sql" + "errors" + "fmt" "github.com/code-gorilla-au/rush/internal/games" "github.com/code-gorilla-au/rush/internal/teams" @@ -16,7 +18,7 @@ type ServiceDependencies struct { TxnFunc func(db *sql.Tx) Store } -func NewTournament(deps ServiceDependencies) *Service { +func NewService(deps ServiceDependencies) *Service { return &Service{ gamesSvc: deps.GamesSvc, teamsSvc: deps.TeamsSvc, @@ -30,15 +32,19 @@ func (s *Service) CreateTournament(ctx context.Context, coachId int64, numberOfT return nil } -func (s *Service) getTeamsForTournament(ctx context.Context, coachId int64) ([]teams.Team, error) { - var totalTeams []teams.Team +func (s *Service) getNonHumanTeams(ctx context.Context, aiTeams int) ([]teams.AITeam, error) { + var tournamentList []teams.AITeam + var err error - humanTeam, err := s.teamsSvc.GetTeamByCoachID(ctx, coachId) + tournamentList, err = s.teamsSvc.ListAITeams(ctx) if err != nil { - return totalTeams, err + return tournamentList, fmt.Errorf("failed to list teams: %w", err) } - totalTeams = append(totalTeams, humanTeam) + if len(tournamentList) < aiTeams { + return tournamentList, errors.New("not enough teams") + } + + return tournamentList[0:aiTeams], nil - return totalTeams, nil } From f2fbda639d26c0fd2c65d7c6fe61399d4fdf3aa9 Mon Sep 17 00:00:00 2001 From: frag223 Date: Mon, 20 Jul 2026 19:30:10 +1000 Subject: [PATCH 09/14] adding small demo --- README.md | 57 ++++++++++++++++++++++++++++++++++++++++++++- docs/rush-demo.gif | Bin 0 -> 334341 bytes 2 files changed, 56 insertions(+), 1 deletion(-) create mode 100644 docs/rush-demo.gif diff --git a/README.md b/README.md index 6ea6638..aa7f9d4 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,57 @@ # rush -simple game + +Rush is a five-player lane battle game that blends elements from risk and battle arena games. +Built in Go, the game emphasises deep, meaningful strategic decisions through [coach personas](internal/ui/rules.md), tactical playbooks, and dynamic lane-based combat. + +## Quick Demo + +![demo](./docs/rush-demo.gif) + +## Prerequisites + +- [Go](https://go.dev/doc/install) (latest stable version) +- [Task](https://taskfile.dev/installation/) (task runner) +- [golangci-lint](https://golangci-lint.run/usage/install/) (for linting) +- [sqlc](https://sqlc.dev/usage/install/) (for database code generation) + +## Getting Started + +### Installation + +1. Clone the repository: + ```bash + git clone + cd rush + ``` + +2. Tidy dependencies: + ```bash + task format + ``` + +### Running the Application + +To run the game: +```bash +task dev +``` + +### Development + +We use `Taskfile.yml` to manage project tasks. To see all available commands, run: +```bash +task --list +``` + +Key tasks include: +- `task test`: Run unit tests. +- `task lint`: Run linters. +- `task format`: Format code and tidy modules. +- `task db-reset`: Reset the database to a clean state. +- `task cover`: Generate and show test coverage. + +## Project Structure + +- `cmd/`: Application entrypoint. +- `internal/`: Core application logic. +- `docs/`: Documentation. diff --git a/docs/rush-demo.gif b/docs/rush-demo.gif new file mode 100644 index 0000000000000000000000000000000000000000..75f415a1560d9339e2c735b2c78972afe54dc359 GIT binary patch literal 334341 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