Skip to content

Commit

Permalink
Add GameTrasactions (Create/Start/End)
Browse files Browse the repository at this point in the history
This allows us to track which User:
- created a Game
- started a Game,
- made the final winning/losing move to end a Game.

To support this, we now have to pass a User object into the methods that
create, start, and end Games.

Note: For now, I'm keeping the Game#started_at and Game#ended_at
attributes. Though, I may remove these soon since they're redundant at
this point.

Also, fix an inconsistency in the test fixtures where the Game with
Status "Standing By" had a `started_at` value.

Also, fix ConsoleObjectBehaviors mix-in to not try to include has_many
associations as automatic Console objects. Because this gets in the way
of operations performed on the collection, such as `clear`.
  • Loading branch information
Paul DobbinSchmaltz committed Nov 12, 2024
1 parent ca3e6cc commit 15e6021
Show file tree
Hide file tree
Showing 17 changed files with 354 additions and 58 deletions.
16 changes: 9 additions & 7 deletions app/controllers/games/new/behaviors.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,16 @@ module Games::New::Behaviors
# :reek:FeatureEnvy

def find_or_create_current_game(settings:)
Game.find_or_create_current(settings:).tap { |current_game|
if current_game.just_created?
DutyRoster.clear
WarRoomChannel.broadcast_refresh
Game.
find_or_create_current(settings:, user: current_user).
tap { |current_game|
if current_game.just_created?
DutyRoster.clear
WarRoomChannel.broadcast_refresh

store_board_settings(current_game)
end
}
store_board_settings(current_game)
end
}
end

private
Expand Down
4 changes: 2 additions & 2 deletions app/models/board.rb
Original file line number Diff line number Diff line change
Expand Up @@ -63,10 +63,10 @@ def pattern
@pattern ||= Pattern.find_by!(name: settings.name)
end

def check_for_victory
def check_for_victory(user:)
return unless game.status_sweep_in_progress?

all_safe_cells_have_been_revealed? and game.end_in_victory
all_safe_cells_have_been_revealed? and game.end_in_victory(user:)
end

def cells_at(coordinates_array)
Expand Down
6 changes: 3 additions & 3 deletions app/models/cell/reveal.rb
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ def already_revealed?
end

def start_game_if_standing_by
game.start(seed_cell: cell)
game.start(seed_cell: cell, user:)
end

def reveal_cell
Expand All @@ -62,7 +62,7 @@ def reveal_cell
def end_game_in_defeat_if_mine_revealed
return unless cell.mine?

game.end_in_defeat
game.end_in_defeat(user:)
throw(:return, self)
end

Expand All @@ -74,6 +74,6 @@ def recursively_reveal_neighbors_if_revealed_cell_was_blank
end

def end_game_in_victory_if_all_safe_cells_revealed
board.check_for_victory
board.check_for_victory(user:)
end
end
6 changes: 3 additions & 3 deletions app/models/cell/reveal_neighbors.rb
Original file line number Diff line number Diff line change
Expand Up @@ -87,11 +87,11 @@ def reveal_neighbor(neighboring_cell)
def reveal(neighboring_cell)
neighboring_cell.reveal

end_in_defeat if neighboring_cell.mine?
end_in_defeat(user:) if neighboring_cell.mine?
end

def end_in_defeat
game.end_in_defeat
game.end_in_defeat(user:)
throw(:return, self)
end

Expand All @@ -102,6 +102,6 @@ def recursively_reveal_neighbors(neighboring_cell)
end

def end_game_in_victory_if_all_safe_cells_revealed
board.check_for_victory
board.check_for_victory(user:)
end
end
31 changes: 21 additions & 10 deletions app/models/game.rb
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
# @attr bbbvps [Float] The 3BV/s rating of a solved {Board}.
# @attr efficiency [Float] The ratio of actual clicks vs necessary clicks (3BV)
# used to solve the associated {Board}.
class Game < ApplicationRecord
class Game < ApplicationRecord # rubocop:disable Metrics/ClassLength
self.inheritance_column = nil
self.implicit_order_column = "created_at"

Expand Down Expand Up @@ -48,6 +48,11 @@ class Game < ApplicationRecord
has_many :cell_flag_transactions, through: :cells
has_many :cell_unflag_transactions, through: :cells

has_many :game_transactions, dependent: :delete_all
has_one :game_create_transaction
has_one :game_start_transaction
has_one :game_end_transaction

has_many :users,
-> { select("DISTINCT ON(users.id) users.*").order("users.id") },
through: :cell_transactions
Expand Down Expand Up @@ -91,10 +96,11 @@ def self.current(within: DEFAULT_JUST_ENDED_DURATION)
last
end

def self.create_for(...)
build_for(...).tap { |new_game|
def self.create_for(user:, **)
build_for(**).tap { |new_game|
transaction do
new_game.save!
GameCreateTransaction.create_between(user:, game: new_game)
new_game.board.on_create
end
}
Expand All @@ -115,27 +121,30 @@ def display_id = "##{id.to_s.rjust(self.class.display_id_width, "0")}"

# :reek:TooManyStatements

def start(seed_cell:)
def start(seed_cell:, user:)
return self unless status_standing_by?

transaction do
touch(:started_at)
GameStartTransaction.create_between(user:, game: self)
board.on_game_start(seed_cell:)
set_status_sweep_in_progress!
end

self
end

def end_in_victory
end_game {
def end_in_victory(user:)
end_game(user:) {
set_stats
set_status_alliance_wins!
}
end

def end_in_defeat
end_game { set_status_mines_win! }
def end_in_defeat(user:)
end_game(user:) {
set_status_mines_win!
}
end

def on?
Expand Down Expand Up @@ -163,11 +172,12 @@ def board_settings = board&.settings

private

def end_game
def end_game(user:)
return self if over?

transaction do
touch(:ended_at)
GameEndTransaction.create_between(user:, game: self)
yield
end

Expand Down Expand Up @@ -242,7 +252,7 @@ def reset
}
end

# Like {#reset} but also resets status to "Standing By" and reset mines on
# Like {#reset} but also resets status to "Standing By" and resets mines on
# the {Board}.
def reset!
do_reset {
Expand Down Expand Up @@ -285,6 +295,7 @@ def do_reset
bbbvps: nil,
efficiency: nil)

game_transactions.clear
CellTransaction.for_id(cell_transaction_ids).delete_all

yield
Expand Down
6 changes: 6 additions & 0 deletions app/models/transactions/game_create_transaction.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# frozen_string_literal: true

# GameCreateTransaction is a {GameTransaction} marking which {User} created the
# associated {Game}.
class GameCreateTransaction < GameTransaction
end
6 changes: 6 additions & 0 deletions app/models/transactions/game_end_transaction.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# frozen_string_literal: true

# GameEndTransaction is a {GameTransaction} marking which {User} made the final
# move during the associated {Game}.
class GameEndTransaction < GameTransaction
end
6 changes: 6 additions & 0 deletions app/models/transactions/game_start_transaction.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# frozen_string_literal: true

# GameStartTransaction is a {GameTransaction} marking which {User} started the
# associated {Game}.
class GameStartTransaction < GameTransaction
end
48 changes: 48 additions & 0 deletions app/models/transactions/game_transaction.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# frozen_string_literal: true

# GameTransaction records events transacted by {User}s on a {Game} and when they
# occurred.
#
# @attr type [String] The Subclass name.
# @attr user_id [Integer] References the {User} involved in this Transaction.
# @attr game_id [Integer] References the {Game} involved in this Transaction.
# @attr created_at [DateTime] When this Transaction occurred.
class GameTransaction < ApplicationRecord
self.implicit_order_column = "created_at"

include AbstractBaseClassBehaviors
include ConsoleBehaviors

as_abstract_class

belongs_to :user
belongs_to :game

scope :for_user, ->(user) { where(user:) }
scope :for_game, ->(game) { where(game:) }

validates :game, uniqueness: { scope: :type }

def self.create_between(user:, game:)
new(user:, game:).tap(&:save!)
end

def self.exists_between?(user:, game:)
for_user(user).for_game(game).exists?
end

# GameTransaction::Console acts like a {GameTransaction} but otherwise handles
# IRB Console-specific methods/logic.
class Console
include ConsoleObjectBehaviors

private

def inspect_info
[
[user.inspect, game.inspect].join(" -> "),
I18n.l(created_at, format: :debug),
].join(" @ ")
end
end
end
18 changes: 18 additions & 0 deletions db/migrate/20241112041937_create_game_transactions.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# frozen_string_literal: true

# Version: 20241112041937
class CreateGameTransactions < ActiveRecord::Migration[8.0]
def change
create_table(:game_transactions) do |t|
t.string(:type, null: false, index: true)
t.references(
:user, type: :uuid, foreign_key: { on_delete: :nullify }, index: true)
t.references(
:game, null: false, foreign_key: { on_delete: :cascade })

t.datetime(:created_at, null: false, index: true)
end

add_index(:game_transactions, %i[game_id type], unique: true)
end
end
Loading

0 comments on commit 15e6021

Please sign in to comment.