Skip to content

Commit

Permalink
Add GameTrasactions (Start/End)
Browse files Browse the repository at this point in the history
This allows us to track which User started a Game and which users 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
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 0967e4b
Show file tree
Hide file tree
Showing 15 changed files with 326 additions and 45 deletions.
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
23 changes: 16 additions & 7 deletions app/models/game.rb
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,10 @@ 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_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 @@ -115,27 +119,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 +170,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 +250,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 +293,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_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
99 changes: 99 additions & 0 deletions db/structure.sql
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,38 @@ CREATE SEQUENCE public.cells_id_seq
ALTER SEQUENCE public.cells_id_seq OWNED BY public.cells.id;


--
-- Name: game_transactions; Type: TABLE; Schema: public; Owner: -
--

CREATE TABLE public.game_transactions (
id bigint NOT NULL,
type character varying NOT NULL,
user_id uuid,
game_id bigint NOT NULL,
created_at timestamp(6) with time zone NOT NULL
);


--
-- Name: game_transactions_id_seq; Type: SEQUENCE; Schema: public; Owner: -
--

CREATE SEQUENCE public.game_transactions_id_seq
START WITH 1
INCREMENT BY 1
NO MINVALUE
NO MAXVALUE
CACHE 1;


--
-- Name: game_transactions_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: -
--

ALTER SEQUENCE public.game_transactions_id_seq OWNED BY public.game_transactions.id;


--
-- Name: games; Type: TABLE; Schema: public; Owner: -
--
Expand Down Expand Up @@ -250,6 +282,13 @@ ALTER TABLE ONLY public.cell_transactions ALTER COLUMN id SET DEFAULT nextval('p
ALTER TABLE ONLY public.cells ALTER COLUMN id SET DEFAULT nextval('public.cells_id_seq'::regclass);


--
-- Name: game_transactions id; Type: DEFAULT; Schema: public; Owner: -
--

ALTER TABLE ONLY public.game_transactions ALTER COLUMN id SET DEFAULT nextval('public.game_transactions_id_seq'::regclass);


--
-- Name: games id; Type: DEFAULT; Schema: public; Owner: -
--
Expand Down Expand Up @@ -296,6 +335,14 @@ ALTER TABLE ONLY public.cells
ADD CONSTRAINT cells_pkey PRIMARY KEY (id);


--
-- Name: game_transactions game_transactions_pkey; Type: CONSTRAINT; Schema: public; Owner: -
--

ALTER TABLE ONLY public.game_transactions
ADD CONSTRAINT game_transactions_pkey PRIMARY KEY (id);


--
-- Name: games games_pkey; Type: CONSTRAINT; Schema: public; Owner: -
--
Expand Down Expand Up @@ -426,6 +473,41 @@ CREATE INDEX index_cells_on_mine ON public.cells USING btree (mine);
CREATE INDEX index_cells_on_revealed ON public.cells USING btree (revealed);


--
-- Name: index_game_transactions_on_created_at; Type: INDEX; Schema: public; Owner: -
--

CREATE INDEX index_game_transactions_on_created_at ON public.game_transactions USING btree (created_at);


--
-- Name: index_game_transactions_on_game_id; Type: INDEX; Schema: public; Owner: -
--

CREATE INDEX index_game_transactions_on_game_id ON public.game_transactions USING btree (game_id);


--
-- Name: index_game_transactions_on_game_id_and_type; Type: INDEX; Schema: public; Owner: -
--

CREATE UNIQUE INDEX index_game_transactions_on_game_id_and_type ON public.game_transactions USING btree (game_id, type);


--
-- Name: index_game_transactions_on_type; Type: INDEX; Schema: public; Owner: -
--

CREATE INDEX index_game_transactions_on_type ON public.game_transactions USING btree (type);


--
-- Name: index_game_transactions_on_user_id; Type: INDEX; Schema: public; Owner: -
--

CREATE INDEX index_game_transactions_on_user_id ON public.game_transactions USING btree (user_id);


--
-- Name: index_games_on_bbbv; Type: INDEX; Schema: public; Owner: -
--
Expand Down Expand Up @@ -526,6 +608,14 @@ ALTER TABLE ONLY public.cell_transactions
ADD CONSTRAINT fk_rails_8ae22ea0ff FOREIGN KEY (cell_id) REFERENCES public.cells(id) ON DELETE CASCADE;


--
-- Name: game_transactions fk_rails_adddf1bd52; Type: FK CONSTRAINT; Schema: public; Owner: -
--

ALTER TABLE ONLY public.game_transactions
ADD CONSTRAINT fk_rails_adddf1bd52 FOREIGN KEY (game_id) REFERENCES public.games(id) ON DELETE CASCADE;


--
-- Name: cell_transactions fk_rails_baaa458a22; Type: FK CONSTRAINT; Schema: public; Owner: -
--
Expand All @@ -534,6 +624,14 @@ ALTER TABLE ONLY public.cell_transactions
ADD CONSTRAINT fk_rails_baaa458a22 FOREIGN KEY (user_id) REFERENCES public.users(id) ON DELETE SET NULL;


--
-- Name: game_transactions fk_rails_c020c31c6a; Type: FK CONSTRAINT; Schema: public; Owner: -
--

ALTER TABLE ONLY public.game_transactions
ADD CONSTRAINT fk_rails_c020c31c6a FOREIGN KEY (user_id) REFERENCES public.users(id) ON DELETE SET NULL;


--
-- Name: cells fk_rails_d04db06fd5; Type: FK CONSTRAINT; Schema: public; Owner: -
--
Expand All @@ -549,6 +647,7 @@ ALTER TABLE ONLY public.cells
SET search_path TO "$user", public;

INSERT INTO "schema_migrations" (version) VALUES
('20241112041937'),
('20240927195322'),
('20240912030247'),
('20240911180310'),
Expand Down
14 changes: 4 additions & 10 deletions lib/console_object_behaviors.rb
Original file line number Diff line number Diff line change
Expand Up @@ -19,16 +19,10 @@ module ConsoleObjectBehaviors
define_method(:__class__) { self.class.module_parent }

try(:reflections)&.each do |name, reflection|
if reflection.macro.in?(%i[has_one belongs_to])
define_method(name) do
__model__.public_send(name).try(:console)
end
elsif reflection.macro == :has_many
define_method(name) do
__model__.public_send(name).by_most_recent.map { |record|
record.try(:console)
}
end
next unless reflection.macro.in?(%i[has_one belongs_to])

define_method(name) do
__model__.public_send(name).try(:console)
end
end
end
Expand Down
Loading

0 comments on commit 0967e4b

Please sign in to comment.