Add Capture the Flag game#1544
Conversation
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.
|
Thanks! |
|
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. |
|
Hi @lanctot - bumping this PR up to get it into your radar! |
The tests are marked as failed. Can you look into it? Also I have incredibly busy with internal work deadlines. If you look at the PRs, the oldest one needing attention is #1519 , and I will look at them in chronological order. Also for OpenSpiel work I have been prioritizing 2.0 release items. So it will be a while before I get to this, sorry about that. |
|
@lanctot, the one red check ( The sibling No rush at all. Whenever you get to the PR, re-running the failed job should clear it; if it stalls again, I'm glad to dig into the apt setup. |
|
I can't seem to retrigger the test, could you make a one-line change (maybe add a comment or blank space -- or, better, pull recent changes from master.. there have been a few changes since) and push a commit which will allow me to restart them? |
|
I've synced the branch with current master and pushed a commit, so it's ready to re-run whenever you get to the PR. |
|
All checks passed. The PR is ready to review whenever you get a chance @lanctot |
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::kPerfectInformationUtility::kZeroSum(default) orkGeneralSumviazero_sumparameterRewardModel::kTerminal: returns+1to the winner and-1to the loser in zero-sum mode (or+1to the winner and0to the loser in general-sum); both0on drawGame parameters
horizon1000-1disables the horizon so the game ends only onscore_limitzero_sumtruetrue, utility is zero-sumscore_limit1grid.empty,*obstacle,aPlayer A's base (spawn + flag),bPlayer B's base. Exactly oneaand onebDefault grid:
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 greatlicense 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 ateam_sizeparameter to extend to team play; the currentGrid/ResolveMove/ResolveTagscode 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
capture_the_flag.hcapture_the_flag.cccapture_the_flag_test.ccCMakeLists.txtpyspiel_test.pydocs/games.mdplaythroughs/capture_the_flag.txthorizon=20to match the markov_soccer / laser_tag conventions)Testing
C++ test executable (
capture_the_flag_test) covers:BasicCaptureTheFlagTests—LoadGameTest,ChanceOutcomesTest,RandomSimTest(100)RandomSimGeneralSumTest— random sim withzero_sum=falseRandomSimHigherScoreLimitTest— random sim withscore_limit=3CarrierCapturesFlagTest— orchestrate full pickup + return + score; assert returns +1 / −1CarrierTaggedInDefenderTerritoryTest— orchestrate pickup, then defender intercepts in defender's home; assert flag back at home and carrier respawnedNoTagInCarrierHomeTerritoryTest— assert defender adjacent to carrier in carrier's home territory does NOT tagBlockingCollisionTest— assert that attempting to enter the opponent's cell is a no-opScoreLimitTerminationTest—score_limit=2; game continues after first capture, terminates after secondHorizonDrawTest—horizon=5; no scoring; assert draw with0, 0returnsObservationTensorShapeTest— verify the 5-plane tensor encodes positions correctlyGridWithObstaclesTest— random sim on a 5×5 grid with obstaclesIntegration:
playthrough_test.py—test_playthrough_capture_the_flag.txtpasses (replays the recorded action sequence)pyspiel_test.py—capture_the_flagis in the expected mandatory games listAll 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