Skip to content

Add Capture the Flag game#1544

Open
kvr06-ai wants to merge 1 commit into
google-deepmind:masterfrom
kvr06-ai:add-capture-the-flag
Open

Add Capture the Flag game#1544
kvr06-ai wants to merge 1 commit into
google-deepmind:masterfrom
kvr06-ai:add-capture-the-flag

Conversation

@kvr06-ai

Copy link
Copy Markdown
Contributor

Summary

Adds Capture-the-Flag, a simultaneous-move adversarial gridworld game requested in the Call for New Games umbrella. Two players occupy opposite ends of a symmetric grid and race to grab the opponent's flag and return it to their own base. A flag carrier in the defender's home territory is vulnerable: if the carrier ends a step Manhattan-adjacent to the defender while inside the defender's home, the carrier is tagged, the flag returns to its home base, and the carrier respawns at their own base. Empty-handed players cannot tag each other.

Partially addresses #843. Picks up from the Capture-the-Flag thread (Feb 2026, @lanctot's Feb 9 comment with detailed implementation guidance) after the original volunteer went inactive.

Modeling

  • Dynamics::kSimultaneous, ChanceMode::kExplicitStochastic, Information::kPerfectInformation
  • Utility::kZeroSum (default) or kGeneralSum via zero_sum parameter
  • RewardModel::kTerminal: returns +1 to the winner and -1 to the loser in zero-sum mode (or +1 to the winner and 0 to the loser in general-sum); both 0 on draw
  • 5 actions per player: North, East, South, West, Stay
  • Each step: simultaneous joint action → chance node resolves move-order initiative (50/50) → moves resolve in chosen order (with blocking-on-opponent-cell semantics) → tag check fires for any carrier in the defender's home territory who ends Manhattan-adjacent to the defender
  • 5-plane observation tensor: player A, player B, A's flag, B's flag, obstacle. A carrier holding a flag marks both its player plane and the corresponding flag plane at the same cell

Game parameters

Param Default Description
horizon 1000 Max steps before draw. -1 disables the horizon so the game ends only on score_limit
zero_sum true If true, utility is zero-sum
score_limit 1 Captures needed to win
grid 7×5 symmetric Grid spec: . empty, * obstacle, a Player A's base (spawn + flag), b Player B's base. Exactly one a and one b

Default grid:

.......
.......
a.....b
.......
.......

Home territory split: A owns cols 0–2, B owns cols 4–6, col 3 is neutral (with odd-width grids). Even-width grids split exactly in half with no neutral column.

Tradeoffs

I picked a 1v1 design rather than a multi-agent team variant (2v2) for the first cut, on the strength of the even if it's a pretty basic version would be great license in @lanctot's Feb 9 comment. The two-player setup captures the core CTF dynamics (asymmetric flag-grab-and-return + territory-aware tagging) with the smallest action space. A future PR can add a team_size parameter to extend to team play; the current Grid / ResolveMove / ResolveTags code paths only assume 2 players in the move-resolution ordering, so the extension would be additive.

The other notable design choice: tag resolution fires after both players have moved, including the pickup step. This means a defender camping next to their flag can deny pickup, which matches standard CTF rules and creates the intended tension where the defender must vacate to attack.

Files

File Lines Purpose
capture_the_flag.h 196 State and Game class declarations, action enum, chance-outcome enum, default grid
capture_the_flag.cc 359 Game registration, grid parser, move/tag/capture resolution, observation tensor
capture_the_flag_test.cc 244 Test suite (see below)
CMakeLists.txt +6 Game and test target registration
pyspiel_test.py +1 Expected mandatory games list
docs/games.md +1 Game documentation entry
playthroughs/capture_the_flag.txt 429 Integration-test playthrough (generated with horizon=20 to match the markov_soccer / laser_tag conventions)

Testing

C++ test executable (capture_the_flag_test) covers:

  • BasicCaptureTheFlagTestsLoadGameTest, ChanceOutcomesTest, RandomSimTest(100)
  • RandomSimGeneralSumTest — random sim with zero_sum=false
  • RandomSimHigherScoreLimitTest — random sim with score_limit=3
  • CarrierCapturesFlagTest — orchestrate full pickup + return + score; assert returns +1 / −1
  • CarrierTaggedInDefenderTerritoryTest — orchestrate pickup, then defender intercepts in defender's home; assert flag back at home and carrier respawned
  • NoTagInCarrierHomeTerritoryTest — assert defender adjacent to carrier in carrier's home territory does NOT tag
  • BlockingCollisionTest — assert that attempting to enter the opponent's cell is a no-op
  • ScoreLimitTerminationTestscore_limit=2; game continues after first capture, terminates after second
  • HorizonDrawTesthorizon=5; no scoring; assert draw with 0, 0 returns
  • ObservationTensorShapeTest — verify the 5-plane tensor encodes positions correctly
  • GridWithObstaclesTest — random sim on a 5×5 grid with obstacles

Integration:

  • playthrough_test.pytest_playthrough_capture_the_flag.txt passes (replays the recorded action sequence)
  • pyspiel_test.pycapture_the_flag is in the expected mandatory games list

All tests pass locally on macOS (Apple Silicon, clang++ 17, Python 3.12). I also ran the surrounding sim-move and game-addition tests as a regression check (laser_tag_test, markov_soccer_test, coop_box_pushing_test, chinese_checkers_test, banqi_test, catch_test, bridge_test) — all pass.

References

cc @lanctot

Simultaneous-move adversarial gridworld where two players race to grab
the opponent's flag and return it to their own base. A carrier in the
defender's home territory is vulnerable to being tagged, which returns
the flag and respawns the carrier. Partially addresses google-deepmind#843.
@kvr06-ai kvr06-ai mentioned this pull request May 12, 2026
@lanctot

lanctot commented May 12, 2026

Copy link
Copy Markdown
Collaborator

Thanks!

@lanctot

lanctot commented May 12, 2026

Copy link
Copy Markdown
Collaborator

I'm overloaded at the moment. For fairness, I'll be doing the PRs in order they were received, so this might sit for a few weeks, just a heads up.

@lanctot lanctot added the waiting Waiting to hear back from contributor (tests failed or thread reply / code update required) label May 21, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

waiting Waiting to hear back from contributor (tests failed or thread reply / code update required)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants