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.

Also, fix an inconsistency in the test fixtures where the Game with
Status "Standing By" had a `started_at` value.
  • Loading branch information
Paul DobbinSchmaltz committed Nov 12, 2024
1 parent ca3e6cc commit 7f24481
Show file tree
Hide file tree
Showing 9 changed files with 278 additions and 3 deletions.
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
17 changes: 17 additions & 0 deletions test/fixtures/game_transactions.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
win1_game_start_transaction:
type: GameStartTransaction
user: user1
game: win1
win1_game_end_transaction:
type: GameEndTransaction
user: user1
game: win1

loss1_game_start_transaction:
type: GameStartTransaction
user: user1
game: loss1
loss1_game_end_transaction:
type: GameEndTransaction
user: user2
game: loss1
3 changes: 1 addition & 2 deletions test/fixtures/games.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,8 @@ loss1:
<<: *DEFAULTS
status: <%= Game.status_mines_win %>
started_at: <%= 30.seconds.ago %>
ended_at: <%= 10.second.ago %>
ended_at: <%= 10.seconds.ago %>

standing_by1:
<<: *DEFAULTS
status: <%= Game.status_standing_by %>
started_at: <%= Time.current %>
5 changes: 4 additions & 1 deletion test/models/game_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,10 @@ class GameTest < ActiveSupport::TestCase

context "GIVEN a Game#on? = true Game doesn't exist" do
context "GIVEN a recently-ended Game exists" do
before { recently_ended_game.end_in_victory }
before do
recently_ended_game.touch(:started_at)
recently_ended_game.end_in_victory
end

let(:recently_ended_game) { standing_by1 }

Expand Down
79 changes: 79 additions & 0 deletions test/models/game_transaction_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
# frozen_string_literal: true

require "test_helper"

class GameTransactionTest < ActiveSupport::TestCase
describe "GameTransaction" do
let(:unit_class) { GameTransaction }

let(:user1) { users(:user1) }
let(:user2) { users(:user2) }

let(:win1) { games(:win1) }
let(:standing_by1) { games(:standing_by1) }

describe "DB insertion (GIVEN no Rails validation)" do
subject { GameStartTransaction.new(user: user1, game: win1) }

it "raises ActiveRecord::RecordNotUnique" do
exception =
_(-> {
subject.save(validate: false)
}).must_raise(ActiveRecord::RecordNotUnique)

_(exception.message).must_include(
"PG::UniqueViolation: ERROR: "\
"duplicate key value violates unique constraint "\
'"index_game_transactions_on_game_id_and_type"')
end
end

describe ".create_between" do
context "GIVEN a new, unique pair" do
subject { GameStartTransaction }

it "creates the expected GameTransaction record, and returns it" do
result =
_(-> {
subject.create_between(user: user2, game: standing_by1)
}).must_change("GameTransaction.count")
_(result).must_be_instance_of(subject)
_(result.user).must_be_same_as(user2)
_(result.game).must_be_same_as(standing_by1)
end
end

context "GIVEN an existing, non-unique pair" do
subject { [GameStartTransaction, GameEndTransaction].sample }

it "raises ActiveRecord::RecordInvalid" do
exception =
_(-> {
subject.create_between(user: user1, game: win1)
}).must_raise(ActiveRecord::RecordInvalid)

_(exception.message).must_equal(
"Validation failed: Game has already been taken")
end
end
end

describe ".exists_between?" do
subject { unit_class }

context "GIVEN an existing pair" do
it "returns true" do
result = subject.exists_between?(user: user1, game: win1)
_(result).must_equal(true)
end
end

context "GIVEN a non-existent pair" do
it "returns false" do
result = subject.exists_between?(user: user2, game: win1)
_(result).must_equal(false)
end
end
end
end
end

0 comments on commit 7f24481

Please sign in to comment.