Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ Sources live under `domains/<domain>/{libs,apis,apps}`.
picked up, reviewed, verified, and shipped here. It is the governing process
document; the highlights below are pointers, not a substitute.

The three rules that change behavior most:
The rules that change behavior most:

- **A test beats an argument, and it lands first.** If a behavior is worth
reasoning about, the deliverable is a CI test, not a paragraph — written
Expand All @@ -22,6 +22,8 @@ The three rules that change behavior most:
each refuting its own findings. Landing first makes the panel's work visible
as commits on a judged baseline rather than invisible inside the first diff.
If it didn't run, say so rather than letting the reader assume it did.
- **Never schedule check-ins on a PR** — hourly or otherwise. Subscribe to its
activity and end the turn; events wake the session.
- **Write the conclusion, not the journey.** Comments state the live rule and
never narrate deleted code. A commit subject is ten words at most and most
commits need no body at all; squash-merge concatenates them onto `main`, so
Expand Down
5 changes: 5 additions & 0 deletions docs/WORKING_AGREEMENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,11 @@ have a reason to record instead of an assumption.
the PR only on request. Reference the tracking issue and, when the issue is a
checklist, tick the item once merged.

**No scheduled check-ins on a PR.** Never schedule an hourly check-in — or
any timed one — to poll a PR's CI, mergeability or review threads. Subscribe
to the PR's activity and end the turn: its events wake the session, and a PR
with nothing happening on it waits for its owner, not for a timer.

**Update the tracking issue.** Fold new data — reproductions, measurements,
scope corrections — back into the issue so it stays the source of truth. File
follow-ups for what you deliberately left out rather than leaving it implicit.
Expand Down
2 changes: 1 addition & 1 deletion domains/ai/apis/deja/src/hub.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ const EVENTS: [&str; 7] = [
"game_started",
"game_finished",
];
const VARIANTS: [&str; 3] = ["golf", "castle", "rummy"];
const VARIANTS: [&str; 4] = ["golf", "castle", "rummy", "gin"];
const SURFACES: [&str; 3] = ["plane", "sphere", "glasshouse"];
const OUTCOMES: [&str; 2] = ["completed", "abandoned"];
/// The seats the engine deals a table, from golf_hub's kMaxSeats.
Expand Down
2 changes: 2 additions & 0 deletions domains/games/apis/games_hub/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -124,11 +124,13 @@ cc_test(
":id_generator",
":world",
"//domains/games/libs/cards",
"//domains/games/libs/cards:dealer",
"//domains/games/libs/cards/castle:game_state",
"//domains/games/libs/cards/castle:player",
"//domains/games/libs/cards/golf:game_state",
"//domains/games/libs/cards/golf:player",
"//domains/games/libs/cards/rummy:game_state",
"//domains/games/libs/cards/rummy:gin",
"//domains/games/libs/cards/rummy:table",
"@com_google_absl//absl/status:statusor",
"@com_google_absl//absl/time",
Expand Down
11 changes: 9 additions & 2 deletions domains/games/apis/games_hub/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -354,8 +354,15 @@ engine's `GameState`.
A rummy table is dealer's choice (#1609, `libs/cards/rummy`'s
`TableState`): `startGame` seats it between deals, the dealer's
`chooseVariant` deals, and each deal's end passes the deal to the next
seat. A dealer the room shows as not connected lets any seat deal. Each
deal is a game to the room's stats and the event log: its end credits the
seat. The variants are `7-card` and `10-card` rummy (one engine, dealt
seven or ten) and heads-up `gin` (`GinState`, #1610); the view's
`choosing.options` lists the ones that fit the seats. Gin's reckoning —
both hands arranged, the defender's lay-offs, the ending — rides
`lastDeal.gin`. A stored table spells
`7-card` as `basic`, its name before the others, so a rolled-back hub
still reads it. A dealer the room shows as not connected lets any seat deal. Each
deal is a game to the room's stats and the event log, recorded as
`rummy` or `gin`: its end credits the
seat that went out with a win and every seat with a game played. Its
points (what the others still held) ride the view's `lastDeal` and stay
out of the room's running total, which is golf's lower-is-better scale.
Expand Down
4 changes: 2 additions & 2 deletions domains/games/apis/games_hub/game_events.cc
Original file line number Diff line number Diff line change
Expand Up @@ -53,13 +53,13 @@ std::optional<GameFinished> FinishedOf(const HostedState& state, std::size_t pla
if (const auto* table = std::get_if<rummy::TableState>(&state)) {
const auto& deal = table->getDeal();
if (!deal.has_value()) return std::nullopt;
if (deal->getPhase() == rummy::Phase::Abandoned) {
if (rummy::dealPhase(*deal) == rummy::Phase::Abandoned) {
finished.outcome = kOutcomeAbandoned;
return finished;
}
// Won by play, and the table dealt on to choosing: the deal's end. A
// closed table's finished deal was recorded when it finished.
if (deal->getPhase() == rummy::Phase::Over &&
if (rummy::dealPhase(*deal) == rummy::Phase::Over &&
table->getPhase() == rummy::TablePhase::Choosing) {
return finished;
}
Expand Down
40 changes: 35 additions & 5 deletions domains/games/apis/games_hub/game_events_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,11 @@
#include "domains/games/libs/cards/card.h"
#include "domains/games/libs/cards/castle/game_state.h"
#include "domains/games/libs/cards/castle/player.h"
#include "domains/games/libs/cards/dealer.h"
#include "domains/games/libs/cards/golf/game_state.h"
#include "domains/games/libs/cards/golf/player.h"
#include "domains/games/libs/cards/rummy/game_state.h"
#include "domains/games/libs/cards/rummy/gin.h"
#include "domains/games/libs/cards/rummy/table.h"
#include "gmock/gmock.h"
#include "gtest/gtest.h"
Expand Down Expand Up @@ -74,12 +76,13 @@ rummy::TableState Rummy() {
std::nullopt,
"game",
"v0"};
return rummy::TableState{{"andy", "mercy"}, {0, 0}, 1, 1, rummy::TablePhase::Playing,
rummy::Variant::Basic, deal, "game", "v0"};
return rummy::TableState{
{"andy", "mercy"}, {0, 0}, 1, 1, rummy::TablePhase::Playing,
rummy::Variant::SevenCard, deal, "game", "v0"};
}

rummy::TableState RummyDealWon() {
auto won = Rummy().inDeal(
auto won = Rummy().inDeal<rummy::GameState>(
[](const rummy::GameState& deal) { return deal.discard(0, Card{Suit::Clubs, Rank::Five}); });
EXPECT_TRUE(won.ok()) << won.status();
return *won;
Expand Down Expand Up @@ -132,7 +135,7 @@ TEST(GameEvents, ACastleGameLeftBelowTwoSeatsIsAbandoned) {
}

// A rummy table's games are its deals (#1609): a deal won by play is a
// game completed, under the deal's variant word — basic rummy is "rummy".
// game completed, under the deal's variant word — seven-card rummy is "rummy".
TEST(GameEvents, ARummyDealPlayedOutIsCompleted) {
const rummy::TableState table = RummyDealWon();
ASSERT_EQ(table.getPhase(), rummy::TablePhase::Choosing);
Expand All @@ -145,6 +148,31 @@ TEST(GameEvents, ARummyDealPlayedOutIsCompleted) {
EXPECT_EQ(finished->players, 2u);
}

/// A gin deal (#1610) played out: from the pristine deck mercy, opening,
/// holds two runs; both pass, she draws the 9♥ and goes gin on it.
rummy::TableState GinDealWon() {
auto dealt =
rummy::TableState::open("game", {"andy", "mercy"})
->chooseVariant(0, rummy::Variant::Gin, cards::NoShuffleDealer().DealNewUnshuffledDeck());
EXPECT_TRUE(dealt.ok()) << dealt.status();
auto won = dealt->inDeal<rummy::GinState>([](const rummy::GinState& deal) {
auto drew = deal.pass(1)->pass(0)->drawStock(1);
return drew->knock(1, drew->getPlayer(1).hand.back());
});
EXPECT_TRUE(won.ok()) << won.status();
return *won;
}

// A gin deal is a game of its own: recorded as "gin", not "rummy".
TEST(GameEvents, AGinDealPlayedOutIsCompletedAsGin) {
const rummy::TableState table = GinDealWon();
ASSERT_EQ(table.getPhase(), rummy::TablePhase::Choosing);
const auto finished = FinishedOf(HostedState(table), 2);
ASSERT_TRUE(finished.has_value());
EXPECT_EQ(finished->variant, "gin");
EXPECT_EQ(finished->outcome, "completed");
}

TEST(GameEvents, ARummyDealBrokenUpByALeaveIsAbandoned) {
const absl::StatusOr<rummy::TableState> gone = Rummy().removePlayer(1);
ASSERT_TRUE(gone.ok()) << gone.status();
Expand Down Expand Up @@ -242,6 +270,7 @@ TEST(GameEvents, EveryEventsLineIsTextWithNothingToEscape) {
lines.emplace_back(GameStartedLine(when, room, "golf", 2), 16);
lines.emplace_back(GameStartedLine(when, room, "castle", 4), 16);
lines.emplace_back(GameStartedLine(when, room, "rummy", 3), 16);
lines.emplace_back(GameStartedLine(when, room, "gin", 2), 16);

std::vector<HostedState> endings;
endings.emplace_back(Golf(0));
Expand All @@ -250,13 +279,14 @@ TEST(GameEvents, EveryEventsLineIsTextWithNothingToEscape) {
endings.emplace_back(*Castle().removePlayer(1));
endings.emplace_back(RummyDealWon());
endings.emplace_back(*Rummy().removePlayer(1));
endings.emplace_back(GinDealWon());
for (const HostedState& state : endings) {
for (std::size_t players = 1; players <= 4; ++players) {
const GameFinished finished = *FinishedOf(state, players);
EXPECT_TRUE(finished.outcome == kOutcomeCompleted || finished.outcome == kOutcomeAbandoned)
<< finished.outcome;
EXPECT_TRUE(finished.variant == "golf" || finished.variant == "castle" ||
finished.variant == "rummy")
finished.variant == "rummy" || finished.variant == "gin")
<< finished.variant;
lines.emplace_back(GameFinishedLine(when, room, finished), 20);
}
Expand Down
Loading
Loading