From 7c02e7aa7526146e6961d4800ca9cd2342bf71f6 Mon Sep 17 00:00:00 2001 From: Kaushik Rajan Date: Tue, 12 May 2026 20:34:40 +0530 Subject: [PATCH] Add Capture-the-Flag game 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 #843. --- docs/games.md | 1 + open_spiel/games/CMakeLists.txt | 6 + .../capture_the_flag/capture_the_flag.cc | 467 ++++++++++++++++++ .../games/capture_the_flag/capture_the_flag.h | 232 +++++++++ .../capture_the_flag/capture_the_flag_test.cc | 284 +++++++++++ .../playthroughs/capture_the_flag.txt | 429 ++++++++++++++++ open_spiel/python/tests/pyspiel_test.py | 1 + 7 files changed, 1420 insertions(+) create mode 100644 open_spiel/games/capture_the_flag/capture_the_flag.cc create mode 100644 open_spiel/games/capture_the_flag/capture_the_flag.h create mode 100644 open_spiel/games/capture_the_flag/capture_the_flag_test.cc create mode 100644 open_spiel/integration_tests/playthroughs/capture_the_flag.txt diff --git a/docs/games.md b/docs/games.md index 076aa8fde5..7e9184b433 100644 --- a/docs/games.md +++ b/docs/games.md @@ -22,6 +22,7 @@ Status | Game 🟢 | [Breakthrough](https://en.wikipedia.org/wiki/Breakthrough_\(board_game\)) | 2 | ✅ | ✅ | Simplified chess using only pawns. 🟢 | [Bridge](https://en.wikipedia.org/wiki/Contract_bridge) | 4 | ❌ | ❌ | A card game where players compete in pairs. 🟢 | [(Uncontested) Bridge bidding](https://en.wikipedia.org/wiki/Contract_bridge) | 2 | ❌ | ❌ | Players score points by forming specific sets with the cards in their hands. +🔶 | [Capture the Flag](https://en.wikipedia.org/wiki/Capture_the_flag) | 2 | ❌ | ✅ | Simultaneous-move adversarial grid game. Each player guards a flag and tries to bring the opponent's flag back to their own base. A carrier in the defender's home territory is vulnerable to being tagged, which returns the flag to its base. References: [Agapiou et al. '22, Melting Pot 2.0](https://arxiv.org/abs/2211.13746). 🔶 | Catch | 1 | ❌ | ✅ | Agent must move horizontally to 'catch' a descending ball. Designed to test basic learning. References: [Mnih et al. 2014, Recurrent Models of Visual Attention](https://papers.nips.cc/paper/5542-recurrent-models-of-visual-attention.pdf). [Osband et al '19, Behaviour Suite for Reinforcement Learning, Appendix A](https://arxiv.org/abs/1908.03568). 🔶 | [Checkers](https://en.wikipedia.org/wiki/Checkers) | 2 | ✅ | ✅ | Players move pieces around the board with the goal of eliminating the opposing pieces. 🔶 | [Chinese Checkers](https://en.wikipedia.org/wiki/Chinese_checkers) | 2-6 | ✅ | ✅ | Star-shaped board game where players race to move all 10 pieces to the opposite triangle via steps and chain hops. Supports 2, 3, 4, or 6 players. diff --git a/open_spiel/games/CMakeLists.txt b/open_spiel/games/CMakeLists.txt index 5f60cac96b..ba0ad93e2a 100644 --- a/open_spiel/games/CMakeLists.txt +++ b/open_spiel/games/CMakeLists.txt @@ -26,6 +26,8 @@ set(GAME_SOURCES bridge/bridge_scoring.h bridge/bridge_uncontested_bidding.cc bridge/bridge_uncontested_bidding.h + capture_the_flag/capture_the_flag.cc + capture_the_flag/capture_the_flag.h catch/catch.cc catch/catch.h checkers/checkers.cc @@ -375,6 +377,10 @@ add_executable(bridge_test bridge/bridge_test.cc ${OPEN_SPIEL_OBJECTS} $) add_test(bridge_test bridge_test) +add_executable(capture_the_flag_test capture_the_flag/capture_the_flag_test.cc + ${OPEN_SPIEL_OBJECTS} $) +add_test(capture_the_flag_test capture_the_flag_test) + add_executable(catch_test catch/catch_test.cc ${OPEN_SPIEL_OBJECTS} $) add_test(catch_test catch_test) diff --git a/open_spiel/games/capture_the_flag/capture_the_flag.cc b/open_spiel/games/capture_the_flag/capture_the_flag.cc new file mode 100644 index 0000000000..d4949022ae --- /dev/null +++ b/open_spiel/games/capture_the_flag/capture_the_flag.cc @@ -0,0 +1,467 @@ +// Copyright 2019 DeepMind Technologies Limited +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "open_spiel/games/capture_the_flag/capture_the_flag.h" + +#include +#include +#include +#include +#include +#include + +#include "open_spiel/abseil-cpp/absl/strings/str_cat.h" +#include "open_spiel/spiel.h" +#include "open_spiel/spiel_utils.h" +#include "open_spiel/utils/tensor_view.h" + +namespace open_spiel { +namespace capture_the_flag { +namespace { + +// Default parameter values. +constexpr int kDefaultHorizon = 1000; +constexpr bool kDefaultZeroSum = true; +constexpr int kDefaultScoreLimit = 1; + +const GameType kGameType{ + /*short_name=*/"capture_the_flag", + /*long_name=*/"Capture the Flag", + GameType::Dynamics::kSimultaneous, + GameType::ChanceMode::kExplicitStochastic, + GameType::Information::kPerfectInformation, + GameType::Utility::kZeroSum, + GameType::RewardModel::kTerminal, + /*max_num_players=*/2, + /*min_num_players=*/2, + /*provides_information_state_string=*/false, + /*provides_information_state_tensor=*/false, + /*provides_observation_string=*/true, + /*provides_observation_tensor=*/true, + /*parameter_specification=*/ + {{"horizon", GameParameter(kDefaultHorizon)}, + {"zero_sum", GameParameter(kDefaultZeroSum)}, + {"score_limit", GameParameter(kDefaultScoreLimit)}, + {"grid", GameParameter(std::string(kDefaultGrid))}}}; + +GameType GameTypeForParams(const GameParameters& params) { + GameType game_type = kGameType; + bool is_zero_sum = kDefaultZeroSum; + auto it = params.find("zero_sum"); + if (it != params.end()) is_zero_sum = it->second.bool_value(); + game_type.utility = is_zero_sum ? GameType::Utility::kZeroSum + : GameType::Utility::kGeneralSum; + return game_type; +} + +std::shared_ptr Factory(const GameParameters& params) { + return std::shared_ptr(new CaptureTheFlagGame(params)); +} + +REGISTER_SPIEL_GAME(kGameType, Factory); + +Grid ParseGrid(const std::string& grid_string) { + Grid grid; + int row = 0; + int col = 0; + int count_a = 0; + int count_b = 0; + for (char c : grid_string) { + if (c == '\n') { + row += 1; + col = 0; + continue; + } + if (row >= grid.num_rows) grid.num_rows = row + 1; + if (col >= grid.num_cols) grid.num_cols = col + 1; + switch (c) { + case '.': + break; + case '*': + grid.obstacles.emplace_back(row, col); + break; + case 'a': + grid.a_base = {row, col}; + ++count_a; + break; + case 'b': + grid.b_base = {row, col}; + ++count_b; + break; + default: + SpielFatalError(absl::StrCat("Invalid char '", std::string(1, c), + "' at grid (", row, ",", col, ")")); + } + col += 1; + } + SPIEL_CHECK_EQ(count_a, 1); + SPIEL_CHECK_EQ(count_b, 1); + SPIEL_CHECK_GE(grid.num_cols, 3); // Need at least one column per territory. + return grid; +} + +// Action -> (row offset, col offset). 'Stay' is (0, 0). +constexpr std::array kRowOffset = {-1, 0, 1, 0, 0}; +constexpr std::array kColOffset = {0, 1, 0, -1, 0}; + +const char* ActionName(int action) { + switch (action) { + case kMoveNorth: + return "North"; + case kMoveEast: + return "East"; + case kMoveSouth: + return "South"; + case kMoveWest: + return "West"; + case kStay: + return "Stay"; + default: + return "Invalid"; + } +} + +} // namespace + +CaptureTheFlagState::CaptureTheFlagState(std::shared_ptr game, + const Grid& grid, int horizon, + bool zero_sum, int score_limit) + : SimMoveState(game), + grid_(grid), + horizon_(horizon), + zero_sum_(zero_sum), + score_limit_(score_limit) { + cur_player_ = kSimultaneousPlayerId; + player_row_[0] = grid_.a_base.first; + player_col_[0] = grid_.a_base.second; + player_row_[1] = grid_.b_base.first; + player_col_[1] = grid_.b_base.second; + flag_row_[0] = grid_.a_base.first; + flag_col_[0] = grid_.a_base.second; + flag_row_[1] = grid_.b_base.first; + flag_col_[1] = grid_.b_base.second; +} + +std::string CaptureTheFlagState::ActionToString(Player player, + Action action_id) const { + if (player == kSimultaneousPlayerId) { + return FlatJointActionToString(action_id); + } + if (player == kChancePlayerId) { + if (action_id == kChanceInit0Action) return "(A resolves first)"; + if (action_id == kChanceInit1Action) return "(B resolves first)"; + SpielFatalError(absl::StrCat("Invalid chance action: ", action_id)); + } + SPIEL_CHECK_GE(action_id, 0); + SPIEL_CHECK_LT(action_id, kNumDistinctActions); + return ActionName(action_id); +} + +bool CaptureTheFlagState::InBounds(int r, int c) const { + return r >= 0 && c >= 0 && r < grid_.num_rows && c < grid_.num_cols; +} + +bool CaptureTheFlagState::IsObstacle(int r, int c) const { + for (const auto& obs : grid_.obstacles) { + if (obs.first == r && obs.second == c) return true; + } + return false; +} + +bool CaptureTheFlagState::InHomeTerritory(Player player, int r, int c) const { + // A's home territory is the columns strictly left of the centre; B's home + // territory is the columns strictly right of the centre. With an + // odd-width grid the centre column is neutral; with an even-width grid + // there is no neutral column and the split is exactly in half. + const int centre = grid_.num_cols / 2; + if (grid_.num_cols % 2 == 1) { + if (player == 0) return c < centre; + return c > centre; + } + if (player == 0) return c < centre; + return c >= centre; +} + +void CaptureTheFlagState::ResetFlagToHome(Player flag_owner) { + flag_holder_[flag_owner] = -1; + if (flag_owner == 0) { + flag_row_[0] = grid_.a_base.first; + flag_col_[0] = grid_.a_base.second; + } else { + flag_row_[1] = grid_.b_base.first; + flag_col_[1] = grid_.b_base.second; + } +} + +void CaptureTheFlagState::RespawnPlayer(Player player) { + if (player == 0) { + player_row_[0] = grid_.a_base.first; + player_col_[0] = grid_.a_base.second; + } else { + player_row_[1] = grid_.b_base.first; + player_col_[1] = grid_.b_base.second; + } +} + +void CaptureTheFlagState::ResolveMove(Player player, int move) { + SPIEL_CHECK_GE(move, 0); + SPIEL_CHECK_LT(move, kNumDistinctActions); + + if (move == kStay) return; + + const int new_r = player_row_[player] + kRowOffset[move]; + const int new_c = player_col_[player] + kColOffset[move]; + if (!InBounds(new_r, new_c)) return; + if (IsObstacle(new_r, new_c)) return; + + const Player opponent = 1 - player; + if (player_row_[opponent] == new_r && player_col_[opponent] == new_c) { + // Cell is blocked by the opponent: no tag, no move. + return; + } + + // Move succeeds. Update player position. If the player is carrying a flag, + // move the flag with them. + player_row_[player] = new_r; + player_col_[player] = new_c; + const Player opponent_flag = opponent; + if (flag_holder_[opponent_flag] == player) { + flag_row_[opponent_flag] = new_r; + flag_col_[opponent_flag] = new_c; + } + + // Pickup: stepping onto the opponent's flag while the flag is loose (no + // current carrier) and at its home base. + const int opp_flag_home_r = + (opponent == 0) ? grid_.a_base.first : grid_.b_base.first; + const int opp_flag_home_c = + (opponent == 0) ? grid_.a_base.second : grid_.b_base.second; + if (flag_holder_[opponent_flag] == -1 && + new_r == opp_flag_home_r && new_c == opp_flag_home_c) { + flag_holder_[opponent_flag] = player; + flag_row_[opponent_flag] = new_r; + flag_col_[opponent_flag] = new_c; + } + + // Capture: carrier arrives at own base, and own flag is at its home base. + const int own_base_r = + (player == 0) ? grid_.a_base.first : grid_.b_base.first; + const int own_base_c = + (player == 0) ? grid_.a_base.second : grid_.b_base.second; + const bool own_flag_home = (flag_holder_[player] == -1 && + flag_row_[player] == own_base_r && + flag_col_[player] == own_base_c); + if (flag_holder_[opponent_flag] == player && new_r == own_base_r && + new_c == own_base_c && own_flag_home) { + score_[player] += 1; + ResetFlagToHome(opponent_flag); + if (score_[player] >= score_limit_) { + winner_ = player; + rewards_[player] += zero_sum_ ? 1.0 : 1.0; + rewards_[opponent] += zero_sum_ ? -1.0 : 0.0; + returns_[player] += zero_sum_ ? 1.0 : 1.0; + returns_[opponent] += zero_sum_ ? -1.0 : 0.0; + } + } +} + +void CaptureTheFlagState::ResolveTags() { + // A carrier in the defender's home territory and Manhattan-adjacent to the + // defender is tagged. Both players carrying are handled, but in practice + // at most one flag is loose per side at any moment in 1v1 CTF. + for (Player carrier = 0; carrier < 2; ++carrier) { + const Player flag_owner = 1 - carrier; + if (flag_holder_[flag_owner] != carrier) continue; + const Player defender = flag_owner; + if (!InHomeTerritory(defender, player_row_[carrier], + player_col_[carrier])) { + continue; + } + const int dr = std::abs(player_row_[carrier] - player_row_[defender]); + const int dc = std::abs(player_col_[carrier] - player_col_[defender]); + if (dr + dc == 1) { + ResetFlagToHome(flag_owner); + RespawnPlayer(carrier); + } + } +} + +void CaptureTheFlagState::DoApplyActions(const std::vector& moves) { + SPIEL_CHECK_EQ(moves.size(), 2); + SPIEL_CHECK_EQ(cur_player_, kSimultaneousPlayerId); + moves_[0] = moves[0]; + moves_[1] = moves[1]; + cur_player_ = kChancePlayerId; +} + +void CaptureTheFlagState::DoApplyAction(Action action_id) { + if (IsSimultaneousNode()) { + ApplyFlatJointAction(action_id); + return; + } + SPIEL_CHECK_TRUE(IsChanceNode()); + SPIEL_CHECK_GE(action_id, 0); + SPIEL_CHECK_LT(action_id, kNumChanceOutcomes); + + rewards_ = {0.0, 0.0}; + if (action_id == kChanceInit0Action) { + ResolveMove(0, moves_[0]); + if (winner_ == -1) ResolveMove(1, moves_[1]); + } else { + ResolveMove(1, moves_[1]); + if (winner_ == -1) ResolveMove(0, moves_[0]); + } + if (winner_ == -1) ResolveTags(); + total_moves_ += 1; + cur_player_ = kSimultaneousPlayerId; +} + +std::vector CaptureTheFlagState::LegalActions(Player player) const { + if (IsTerminal()) return {}; + if (IsChanceNode()) return LegalChanceOutcomes(); + std::vector actions(kNumDistinctActions); + for (int i = 0; i < kNumDistinctActions; ++i) actions[i] = i; + return actions; +} + +ActionsAndProbs CaptureTheFlagState::ChanceOutcomes() const { + SPIEL_CHECK_TRUE(IsChanceNode()); + return {{kChanceInit0Action, 0.5}, {kChanceInit1Action, 0.5}}; +} + +std::string CaptureTheFlagState::ToString() const { + std::string result; + for (int r = 0; r < grid_.num_rows; ++r) { + for (int c = 0; c < grid_.num_cols; ++c) { + char ch = '.'; + if (IsObstacle(r, c)) { + ch = '*'; + } + // Players overwrite obstacles in the rendering, but obstacles can't + // share cells with players, so this is just for safety. + if (player_row_[0] == r && player_col_[0] == c) ch = 'A'; + if (player_row_[1] == r && player_col_[1] == c) ch = 'B'; + // Loose flags render with their owner's lowercase letter. + if (flag_holder_[0] == -1 && flag_row_[0] == r && flag_col_[0] == c) { + if (ch == 'A') { + ch = 'A'; // A is standing on their own flag at base; show player. + } else { + ch = 'a'; + } + } + if (flag_holder_[1] == -1 && flag_row_[1] == r && flag_col_[1] == c) { + if (ch == 'B') { + ch = 'B'; + } else { + ch = 'b'; + } + } + result += ch; + } + absl::StrAppend(&result, "\n"); + } + absl::StrAppend(&result, "Carrier(A's flag): ", flag_holder_[0], + " Carrier(B's flag): ", flag_holder_[1], "\n"); + absl::StrAppend(&result, "Score: A=", score_[0], " B=", score_[1], "\n"); + absl::StrAppend(&result, "Moves: ", total_moves_, "/", + horizon_ < 0 ? -1 : horizon_, "\n"); + if (IsChanceNode()) absl::StrAppend(&result, "Chance Node\n"); + return result; +} + +std::string CaptureTheFlagState::ObservationString(Player player) const { + SPIEL_CHECK_GE(player, 0); + SPIEL_CHECK_LT(player, num_players_); + return ToString(); +} + +bool CaptureTheFlagState::IsTerminal() const { + if (winner_ != -1) return true; + if (horizon_ >= 0 && total_moves_ >= horizon_) return true; + return false; +} + +std::vector CaptureTheFlagState::Rewards() const { return rewards_; } + +std::vector CaptureTheFlagState::Returns() const { return returns_; } + +int CaptureTheFlagState::ObservationPlaneForPlayer(int r, int c, + Player player) const { + if (player_row_[player] == r && player_col_[player] == c) return player; + return -1; +} + +int CaptureTheFlagState::ObservationPlaneForFlag(int r, int c, + Player flag_owner) const { + if (flag_row_[flag_owner] == r && flag_col_[flag_owner] == c) { + return 2 + flag_owner; + } + return -1; +} + +void CaptureTheFlagState::ObservationTensor(Player player, + absl::Span values) const { + SPIEL_CHECK_GE(player, 0); + SPIEL_CHECK_LT(player, num_players_); + TensorView<3> view(values, {kCellStates, grid_.num_rows, grid_.num_cols}, + /*reset=*/true); + for (int r = 0; r < grid_.num_rows; ++r) { + for (int c = 0; c < grid_.num_cols; ++c) { + if (IsObstacle(r, c)) view[{4, r, c}] = 1.0; + } + } + for (Player p = 0; p < 2; ++p) { + view[{p, player_row_[p], player_col_[p]}] = 1.0; + } + for (Player f = 0; f < 2; ++f) { + view[{2 + f, flag_row_[f], flag_col_[f]}] = 1.0; + } +} + +std::unique_ptr CaptureTheFlagState::Clone() const { + return std::unique_ptr(new CaptureTheFlagState(*this)); +} + +CaptureTheFlagGame::CaptureTheFlagGame(const GameParameters& params) + : SimMoveGame(GameTypeForParams(params), params), + grid_(ParseGrid(ParameterValue("grid"))), + horizon_(ParameterValue("horizon")), + zero_sum_(ParameterValue("zero_sum")), + score_limit_(ParameterValue("score_limit")) { + SPIEL_CHECK_GE(score_limit_, 1); +} + +std::unique_ptr CaptureTheFlagGame::NewInitialState() const { + return std::unique_ptr(new CaptureTheFlagState( + shared_from_this(), grid_, horizon_, zero_sum_, score_limit_)); +} + +double CaptureTheFlagGame::MinUtility() const { + if (zero_sum_) return -1.0; + return 0.0; +} + +double CaptureTheFlagGame::MaxUtility() const { return 1.0; } + +absl::optional CaptureTheFlagGame::UtilitySum() const { + if (zero_sum_) return 0.0; + return absl::nullopt; +} + +std::vector CaptureTheFlagGame::ObservationTensorShape() const { + return {kCellStates, grid_.num_rows, grid_.num_cols}; +} + +} // namespace capture_the_flag +} // namespace open_spiel diff --git a/open_spiel/games/capture_the_flag/capture_the_flag.h b/open_spiel/games/capture_the_flag/capture_the_flag.h new file mode 100644 index 0000000000..52627a23fd --- /dev/null +++ b/open_spiel/games/capture_the_flag/capture_the_flag.h @@ -0,0 +1,232 @@ +// Copyright 2019 DeepMind Technologies Limited +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef OPEN_SPIEL_GAMES_CAPTURE_THE_FLAG_H_ +#define OPEN_SPIEL_GAMES_CAPTURE_THE_FLAG_H_ + +#include +#include +#include +#include +#include + +#include "open_spiel/simultaneous_move_game.h" +#include "open_spiel/spiel.h" + +// Simultaneous-move adversarial Capture-the-Flag (CTF) on a 2D grid. +// +// Two players occupy opposite ends of a symmetric grid. Each side owns a flag +// at its home base. A player scores a "capture" by stepping onto the +// opponent's flag (picking it up), then returning that flag to their own base +// while their own flag is still home. A flag carrier is vulnerable: if the +// carrier ends a step adjacent to the defender while inside the defender's +// home territory, the carrier is "tagged" and respawns at their own base, +// and the carried flag returns to its home base. Empty-handed players cannot +// tag each other (defense matters only against carriers). +// +// The default grid is symmetric and split into thirds. Player A's home +// territory is the left third, player B's is the right third, the middle +// column is neutral. Players spawn at their bases and the flags start at +// their owners' bases. +// +// Each step: +// 1. Both players submit one of {North, East, South, West, Stay}. +// 2. A chance node resolves move-order initiative (50/50) so the joint +// action is unambiguous when the two players try to enter the same +// cell. +// 3. The first-to-resolve player attempts to move (no-op if blocked by +// bounds, obstacle, or the opponent's cell). Stepping onto the +// opponent's flag at its home base picks it up. Stepping onto own +// base while carrying the opponent's flag (and with own flag at home) +// scores a capture: capture count increments, both flags reset, and +// the carrier becomes empty-handed. +// 4. The second player resolves the same way against the updated state. +// 5. Tag resolution: for each player carrying the opponent's flag, if +// the opponent is Manhattan-adjacent (distance == 1) and the carrier +// is in the defender's home territory, the carrier is tagged. The +// flag returns to its home base and the carrier respawns at their +// own base. +// +// Termination: +// * First player to reach `score_limit` captures wins. +// * If `horizon >= 0` and the step count hits `horizon` without a winner, +// the game ends in a draw. +// +// References: +// * Leibo et al. Multi-agent Reinforcement Learning in Sequential Social +// Dilemmas. https://arxiv.org/abs/1702.03037 +// * Agapiou et al. (DeepMind). Melting Pot 2.0. +// https://arxiv.org/abs/2211.13746 (CTF appears as a substrate). +// +// Parameters: +// "horizon" int Maximum number of steps before a draw. +// Use -1 to disable the horizon (game ends +// only when score_limit is reached). +// (default = 1000). +// "zero_sum" bool If true, returns at termination are {+1, -1} +// for a winner and 0 each for a draw. If false, +// only the winner gets +1; the loser gets 0. +// (default = true). +// "score_limit" int Number of captures needed to win. +// (default = 1). +// "grid" string Newline-separated grid. Cells: +// '.' empty; +// '*' obstacle; +// 'a' Player A's base (flag and spawn); +// 'b' Player B's base (flag and spawn). +// Each grid must have exactly one 'a' and one +// 'b' cell. + +namespace open_spiel { +namespace capture_the_flag { + +inline constexpr char kDefaultGrid[] = + ".......\n" + ".......\n" + "a.....b\n" + ".......\n" + "......."; + +struct Grid { + int num_rows = 0; + int num_cols = 0; + std::vector> obstacles; + std::pair a_base = {-1, -1}; + std::pair b_base = {-1, -1}; +}; + +// Movement actions. +enum MovementType { + kMoveNorth = 0, + kMoveEast = 1, + kMoveSouth = 2, + kMoveWest = 3, + kStay = 4 +}; + +inline constexpr int kNumDistinctActions = 5; + +// Two chance outcomes encode which player resolves first after each +// simultaneous action. +inline constexpr int kNumChanceOutcomes = 2; +inline constexpr Action kChanceInit0Action = 0; +inline constexpr Action kChanceInit1Action = 1; +enum class ChanceOutcome { kChanceInit0, kChanceInit1 }; + +// Five observation planes: player A, player B, A's flag, B's flag, obstacle. +inline constexpr int kCellStates = 5; + +class CaptureTheFlagGame; + +class CaptureTheFlagState : public SimMoveState { + public: + CaptureTheFlagState(std::shared_ptr game, const Grid& grid, + int horizon, bool zero_sum, int score_limit); + CaptureTheFlagState(const CaptureTheFlagState&) = default; + + std::string ActionToString(Player player, Action action_id) const override; + std::string ToString() const override; + bool IsTerminal() const override; + std::vector Rewards() const override; + std::vector Returns() const override; + std::string ObservationString(Player player) const override; + void ObservationTensor(Player player, + absl::Span values) const override; + Player CurrentPlayer() const override { + return IsTerminal() ? kTerminalPlayerId : cur_player_; + } + std::unique_ptr Clone() const override; + + ActionsAndProbs ChanceOutcomes() const override; + std::vector LegalActions(Player player) const override; + + protected: + void DoApplyAction(Action action_id) override; + void DoApplyActions(const std::vector& moves) override; + + private: + bool InBounds(int r, int c) const; + bool IsObstacle(int r, int c) const; + // Apply one player's move to the current state. Pickups and captures + // happen here; tagging is deferred until both players have moved. + void ResolveMove(Player player, int move); + // After both players have resolved their moves for this step, check + // whether a flag carrier should be tagged. + void ResolveTags(); + void RespawnPlayer(Player player); + void ResetFlagToHome(Player flag_owner); + // True if (r, c) is in `player`'s home territory. + bool InHomeTerritory(Player player, int r, int c) const; + + int ObservationPlaneForPlayer(int r, int c, Player player) const; + int ObservationPlaneForFlag(int r, int c, Player flag_owner) const; + + const Grid& grid_; + const int horizon_; + const bool zero_sum_; + const int score_limit_; + + // Set to invalid values; populated in the constructor. + Player cur_player_ = -1; + int total_moves_ = 0; + int winner_ = -1; // -1 = no winner yet; 0 or 1 if decided. + + std::array player_row_ = {{-1, -1}}; + std::array player_col_ = {{-1, -1}}; + + // Flag positions are tracked separately from carriers; a carrier's flag + // coordinates equal the carrier's coordinates. + std::array flag_row_ = {{-1, -1}}; + std::array flag_col_ = {{-1, -1}}; + // `holder_[k] = p` means player p is carrying flag k (k = 0 is A's flag, + // k = 1 is B's flag). -1 means the flag is loose (at its home base). + std::array flag_holder_ = {{-1, -1}}; + + std::array score_ = {{0, 0}}; + std::array moves_ = {{-1, -1}}; + std::vector rewards_ = {0.0, 0.0}; + std::vector returns_ = {0.0, 0.0}; +}; + +class CaptureTheFlagGame : public SimMoveGame { + public: + explicit CaptureTheFlagGame(const GameParameters& params); + int NumDistinctActions() const override { return kNumDistinctActions; } + std::unique_ptr NewInitialState() const override; + int MaxChanceOutcomes() const override { return kNumChanceOutcomes; } + int NumPlayers() const override { return 2; } + double MinUtility() const override; + double MaxUtility() const override; + absl::optional UtilitySum() const override; + std::vector ObservationTensorShape() const override; + int MaxGameLength() const override { + // When `horizon_ < 0`, the game can run until score_limit is reached. + // There is no finite hard cap in that case beyond the score-limit + // dynamics, so we report a generous default consistent with similar + // simultaneous-move games (e.g. laser_tag's horizon=-1 behaviour). + return horizon_ >= 0 ? horizon_ : 1000; + } + int MaxChanceNodesInHistory() const override { return MaxGameLength(); } + + private: + Grid grid_; + int horizon_; + bool zero_sum_; + int score_limit_; +}; + +} // namespace capture_the_flag +} // namespace open_spiel + +#endif // OPEN_SPIEL_GAMES_CAPTURE_THE_FLAG_H_ diff --git a/open_spiel/games/capture_the_flag/capture_the_flag_test.cc b/open_spiel/games/capture_the_flag/capture_the_flag_test.cc new file mode 100644 index 0000000000..7df5bceb4c --- /dev/null +++ b/open_spiel/games/capture_the_flag/capture_the_flag_test.cc @@ -0,0 +1,284 @@ +// Copyright 2019 DeepMind Technologies Limited +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "open_spiel/games/capture_the_flag/capture_the_flag.h" + +#include +#include +#include + +#include "open_spiel/spiel.h" +#include "open_spiel/spiel_utils.h" +#include "open_spiel/tests/basic_tests.h" + +namespace open_spiel { +namespace capture_the_flag { +namespace { + +namespace testing = open_spiel::testing; + +void BasicCaptureTheFlagTests() { + testing::LoadGameTest("capture_the_flag"); + testing::ChanceOutcomesTest(*LoadGame("capture_the_flag")); + testing::RandomSimTest(*LoadGame("capture_the_flag"), 100); +} + +void RandomSimGeneralSumTest() { + testing::RandomSimTest( + *LoadGame("capture_the_flag", {{"zero_sum", GameParameter(false)}}), 50); +} + +void RandomSimHigherScoreLimitTest() { + testing::RandomSimTest( + *LoadGame("capture_the_flag", {{"score_limit", GameParameter(3)}}), 50); +} + +// Helper: apply a joint move and resolve initiative deterministically. +void Step(State* state, Action a_move, Action b_move, Action initiative) { + state->ApplyActions({a_move, b_move}); + SPIEL_CHECK_TRUE(state->IsChanceNode()); + state->ApplyAction(initiative); +} + +// Drive A to pick up B's flag and return to base. B vacates the flag area +// first so the carrier has a safe pickup. Default grid is 5 rows x 7 cols. +void CarrierCapturesFlagTest() { + std::shared_ptr game = + LoadGame("capture_the_flag", {{"horizon", GameParameter(100)}, + {"zero_sum", GameParameter(true)}, + {"score_limit", GameParameter(1)}}); + std::unique_ptr state = game->NewInitialState(); + SPIEL_CHECK_TRUE(state->IsSimultaneousNode()); + + // B walks to the far corner so A has a safe approach to (2, 6). + // B: (2,6) -> (1,6) -> (0,6); then stays. A: east twice -> (2,2). + Step(state.get(), kMoveEast, kMoveNorth, kChanceInit0Action); + Step(state.get(), kMoveEast, kMoveNorth, kChanceInit0Action); + // B now at (0,6); A at (2,2). + for (int i = 0; i < 4; ++i) { + Step(state.get(), kMoveEast, kStay, kChanceInit0Action); + } + // A should have stepped through (2,3), (2,4), (2,5), and arrived at (2,6) + // where it picks up B's flag. Distance to B at (0,6) is 2 -> safe. + + // A walks back six west steps to (2,0). B stays at (0,6) throughout. + for (int i = 0; i < 5; ++i) { + Step(state.get(), kMoveWest, kStay, kChanceInit0Action); + } + SPIEL_CHECK_FALSE(state->IsTerminal()); + Step(state.get(), kMoveWest, kStay, kChanceInit0Action); + SPIEL_CHECK_TRUE(state->IsTerminal()); + SPIEL_CHECK_EQ(state->PlayerReturn(0), 1.0); + SPIEL_CHECK_EQ(state->PlayerReturn(1), -1.0); +} + +// A picks up B's flag, then B intercepts A in B's home territory. Expect +// the flag to return to its home base and A to respawn at A's base. +void CarrierTaggedInDefenderTerritoryTest() { + std::shared_ptr game = + LoadGame("capture_the_flag", {{"horizon", GameParameter(100)}}); + std::unique_ptr state = game->NewInitialState(); + + // B walks far enough away for A to pick up the flag safely. + // B: (2,6) -> (1,6) -> (0,6) -> (0,5); then stays. + Step(state.get(), kMoveEast, kMoveNorth, kChanceInit0Action); + Step(state.get(), kMoveEast, kMoveNorth, kChanceInit0Action); + Step(state.get(), kMoveEast, kMoveWest, kChanceInit0Action); + // B at (0,5); A at (2,3). + for (int i = 0; i < 3; ++i) { + Step(state.get(), kMoveEast, kStay, kChanceInit0Action); + } + // A at (2,6) carrying B's flag; B at (0,5). Distance = 2+1 = 3 -> safe. + + // Now have B intercept A. B moves south, south, then east to land + // Manhattan-adjacent to A while A walks west into B's territory. + // Step: A west to (2,5); B south to (1,5). A=(2,5), B=(1,5), dist=1. + // A is in B's territory (col 5). Tag fires. + Step(state.get(), kMoveWest, kMoveSouth, kChanceInit0Action); + + // After the tag: A should be back at (2,0) and B's flag back at (2,6). + // Verify via the observation tensor. + std::vector obs; + obs.resize(5 * 5 * 7); + state->ObservationTensor(0, absl::MakeSpan(obs)); + // Plane 0 (player A) marks (2, 0). + SPIEL_CHECK_EQ(obs[0 * 5 * 7 + 2 * 7 + 0], 1.0); + // Plane 3 (B's flag) marks (2, 6). + SPIEL_CHECK_EQ(obs[3 * 5 * 7 + 2 * 7 + 6], 1.0); + // No capture should have happened. + SPIEL_CHECK_FALSE(state->IsTerminal()); + SPIEL_CHECK_EQ(state->PlayerReturn(0), 0.0); + SPIEL_CHECK_EQ(state->PlayerReturn(1), 0.0); +} + +// Carrier safe in own home territory: defender cannot tag the carrier +// while the carrier is in the carrier's own half. +void NoTagInCarrierHomeTerritoryTest() { + std::shared_ptr game = + LoadGame("capture_the_flag", {{"horizon", GameParameter(100)}}); + std::unique_ptr state = game->NewInitialState(); + + // B vacates so A can grab. + Step(state.get(), kMoveEast, kMoveNorth, kChanceInit0Action); + Step(state.get(), kMoveEast, kMoveNorth, kChanceInit0Action); + for (int i = 0; i < 4; ++i) { + Step(state.get(), kMoveEast, kStay, kChanceInit0Action); + } + // A at (2,6) carrying B's flag; B at (0,6). + + // A walks back to (2,2) -- A's home territory. B trails A but stays + // far enough that adjacency in A's territory does NOT tag. + for (int i = 0; i < 4; ++i) { + Step(state.get(), kMoveWest, kStay, kChanceInit0Action); + } + // A at (2,2), in A's home territory. B at (0,6) -- far away. + // Walk B into A's territory to confirm no tag fires there. + for (int i = 0; i < 4; ++i) { + Step(state.get(), kStay, kMoveWest, kChanceInit0Action); + } + // B at (0,2) now, A at (2,2). Move B adjacent: south to (1,2). + Step(state.get(), kStay, kMoveSouth, kChanceInit0Action); + // B at (1,2), A at (2,2). Adjacent. But A is in A's home (col 2 < 3). + // Tag check uses InHomeTerritory(defender=B, carrier_pos=A). Col 2 is + // not in B's home (B's home = cols 4..6). So no tag. + + // A walks to base to capture. + Step(state.get(), kMoveWest, kStay, kChanceInit0Action); + Step(state.get(), kMoveWest, kStay, kChanceInit0Action); + SPIEL_CHECK_TRUE(state->IsTerminal()); + SPIEL_CHECK_EQ(state->PlayerReturn(0), 1.0); +} + +// Attempting to move into the opponent's cell is a no-op. +void BlockingCollisionTest() { + std::shared_ptr game = + LoadGame("capture_the_flag", {{"horizon", GameParameter(100)}}); + std::unique_ptr state = game->NewInitialState(); + + // Drive A and B toward each other so they end up in adjacent cells: + // A: (2,0) east three times -> (2,3). + // B: (2,6) west three times -> (2,3)? Initiative 0 means A resolves + // first; at step 2 A enters (2,3) and B is blocked. After three steps: + // A=(2,3), B=(2,4). + for (int i = 0; i < 3; ++i) { + Step(state.get(), kMoveEast, kMoveWest, kChanceInit0Action); + } + + std::vector obs; + obs.resize(5 * 5 * 7); + state->ObservationTensor(0, absl::MakeSpan(obs)); + SPIEL_CHECK_EQ(obs[0 * 5 * 7 + 2 * 7 + 3], 1.0); // A at (2,3) + SPIEL_CHECK_EQ(obs[1 * 5 * 7 + 2 * 7 + 4], 1.0); // B at (2,4) + + // Now A tries to step into B's cell. With initiative 0, A resolves + // first and is blocked by B at (2,4); B stays. A stays at (2,3). + Step(state.get(), kMoveEast, kStay, kChanceInit0Action); + state->ObservationTensor(0, absl::MakeSpan(obs)); + SPIEL_CHECK_EQ(obs[0 * 5 * 7 + 2 * 7 + 3], 1.0); + SPIEL_CHECK_EQ(obs[1 * 5 * 7 + 2 * 7 + 4], 1.0); +} + +void ScoreLimitTerminationTest() { + std::shared_ptr game = + LoadGame("capture_the_flag", {{"horizon", GameParameter(200)}, + {"score_limit", GameParameter(2)}}); + std::unique_ptr state = game->NewInitialState(); + + // Drive one full capture cycle. + Step(state.get(), kMoveEast, kMoveNorth, kChanceInit0Action); + Step(state.get(), kMoveEast, kMoveNorth, kChanceInit0Action); + for (int i = 0; i < 4; ++i) { + Step(state.get(), kMoveEast, kStay, kChanceInit0Action); + } + for (int i = 0; i < 6; ++i) { + Step(state.get(), kMoveWest, kStay, kChanceInit0Action); + } + // A captured once. Score is 1; game should continue. + SPIEL_CHECK_FALSE(state->IsTerminal()); + + // Drive a second capture. After scoring, A is at (2,0) and B is at + // (0,6). Flags are at home. Repeat the pattern. + for (int i = 0; i < 6; ++i) { + Step(state.get(), kMoveEast, kStay, kChanceInit0Action); + } + for (int i = 0; i < 6; ++i) { + Step(state.get(), kMoveWest, kStay, kChanceInit0Action); + } + SPIEL_CHECK_TRUE(state->IsTerminal()); + SPIEL_CHECK_EQ(state->PlayerReturn(0), 1.0); + SPIEL_CHECK_EQ(state->PlayerReturn(1), -1.0); +} + +void HorizonDrawTest() { + std::shared_ptr game = + LoadGame("capture_the_flag", {{"horizon", GameParameter(5)}}); + std::unique_ptr state = game->NewInitialState(); + for (int step = 0; step < 5; ++step) { + Step(state.get(), kStay, kStay, kChanceInit0Action); + } + SPIEL_CHECK_TRUE(state->IsTerminal()); + SPIEL_CHECK_EQ(state->PlayerReturn(0), 0.0); + SPIEL_CHECK_EQ(state->PlayerReturn(1), 0.0); +} + +void ObservationTensorShapeTest() { + std::shared_ptr game = LoadGame("capture_the_flag"); + std::vector shape = game->ObservationTensorShape(); + SPIEL_CHECK_EQ(shape.size(), 3); + SPIEL_CHECK_EQ(shape[0], 5); // kCellStates + SPIEL_CHECK_EQ(shape[1], 5); // default grid rows + SPIEL_CHECK_EQ(shape[2], 7); // default grid cols + + std::unique_ptr state = game->NewInitialState(); + std::vector obs; + obs.resize(5 * 5 * 7); + state->ObservationTensor(0, absl::MakeSpan(obs)); + SPIEL_CHECK_EQ(obs[0 * 5 * 7 + 2 * 7 + 0], 1.0); // Plane 0: A at (2,0) + SPIEL_CHECK_EQ(obs[1 * 5 * 7 + 2 * 7 + 6], 1.0); // Plane 1: B at (2,6) + SPIEL_CHECK_EQ(obs[2 * 5 * 7 + 2 * 7 + 0], 1.0); // Plane 2: A's flag + SPIEL_CHECK_EQ(obs[3 * 5 * 7 + 2 * 7 + 6], 1.0); // Plane 3: B's flag + for (int i = 0; i < 5 * 7; ++i) { + SPIEL_CHECK_EQ(obs[4 * 5 * 7 + i], 0.0); // Plane 4: no obstacles + } +} + +void GridWithObstaclesTest() { + const std::string grid = "a...b\n.....\n.***.\n.....\n....."; + std::shared_ptr game = + LoadGame("capture_the_flag", {{"grid", GameParameter(grid)}, + {"horizon", GameParameter(100)}}); + testing::RandomSimTest(*game, 20); + std::unique_ptr state = game->NewInitialState(); + std::vector shape = game->ObservationTensorShape(); + SPIEL_CHECK_EQ(shape[1], 5); + SPIEL_CHECK_EQ(shape[2], 5); +} + +} // namespace +} // namespace capture_the_flag +} // namespace open_spiel + +int main(int /*argc*/, char** /*argv*/) { + open_spiel::capture_the_flag::BasicCaptureTheFlagTests(); + open_spiel::capture_the_flag::RandomSimGeneralSumTest(); + open_spiel::capture_the_flag::RandomSimHigherScoreLimitTest(); + open_spiel::capture_the_flag::CarrierCapturesFlagTest(); + open_spiel::capture_the_flag::CarrierTaggedInDefenderTerritoryTest(); + open_spiel::capture_the_flag::NoTagInCarrierHomeTerritoryTest(); + open_spiel::capture_the_flag::BlockingCollisionTest(); + open_spiel::capture_the_flag::ScoreLimitTerminationTest(); + open_spiel::capture_the_flag::HorizonDrawTest(); + open_spiel::capture_the_flag::ObservationTensorShapeTest(); + open_spiel::capture_the_flag::GridWithObstaclesTest(); +} diff --git a/open_spiel/integration_tests/playthroughs/capture_the_flag.txt b/open_spiel/integration_tests/playthroughs/capture_the_flag.txt new file mode 100644 index 0000000000..8eddfc44fc --- /dev/null +++ b/open_spiel/integration_tests/playthroughs/capture_the_flag.txt @@ -0,0 +1,429 @@ +game: capture_the_flag(horizon=20) + +GameType.chance_mode = ChanceMode.EXPLICIT_STOCHASTIC +GameType.dynamics = Dynamics.SIMULTANEOUS +GameType.information = Information.PERFECT_INFORMATION +GameType.long_name = "Capture the Flag" +GameType.max_num_players = 2 +GameType.min_num_players = 2 +GameType.parameter_specification = ["grid", "horizon", "score_limit", "zero_sum"] +GameType.provides_information_state_string = False +GameType.provides_information_state_tensor = False +GameType.provides_observation_string = True +GameType.provides_observation_tensor = True +GameType.provides_factored_observation_string = False +GameType.reward_model = RewardModel.TERMINAL +GameType.short_name = "capture_the_flag" +GameType.utility = Utility.ZERO_SUM + +NumDistinctActions() = 5 +PolicyTensorShape() = [5] +MaxChanceOutcomes() = 2 +GetParameters() = {grid=.......\n.......\na.....b\n.......\n.......,horizon=20,score_limit=1,zero_sum=True} +NumPlayers() = 2 +MinUtility() = -1.0 +MaxUtility() = 1.0 +UtilitySum() = 0.0 +ObservationTensorShape() = [5, 5, 7] +ObservationTensorLayout() = TensorLayout.CHW +ObservationTensorSize() = 175 +MaxGameLength() = 20 +ToString() = "capture_the_flag(horizon=20)" + +# State 0 +# ....... +# ....... +# A.....B +# ....... +# ....... +# Carrier(A's flag): -1 Carrier(B's flag): -1 +# Score: A=0 B=0 +# Moves: 0/20 +IsTerminal() = False +History() = [] +HistoryString() = "" +IsChanceNode() = False +IsSimultaneousNode() = True +CurrentPlayer() = -2 +ObservationString(0) = ".......\n.......\nA.....B\n.......\n.......\nCarrier(A's flag): -1 Carrier(B's flag): -1\nScore: A=0 B=0\nMoves: 0/20\n" +ObservationString(1) = ".......\n.......\nA.....B\n.......\n.......\nCarrier(A's flag): -1 Carrier(B's flag): -1\nScore: A=0 B=0\nMoves: 0/20\n" +ObservationTensor(0): +◯◯◯◯◯◯◯ ◯◯◯◯◯◯◯ ◯◯◯◯◯◯◯ ◯◯◯◯◯◯◯ ◯◯◯◯◯◯◯ +◯◯◯◯◯◯◯ ◯◯◯◯◯◯◯ ◯◯◯◯◯◯◯ ◯◯◯◯◯◯◯ ◯◯◯◯◯◯◯ +◉◯◯◯◯◯◯ ◯◯◯◯◯◯◉ ◉◯◯◯◯◯◯ ◯◯◯◯◯◯◉ ◯◯◯◯◯◯◯ +◯◯◯◯◯◯◯ ◯◯◯◯◯◯◯ ◯◯◯◯◯◯◯ ◯◯◯◯◯◯◯ ◯◯◯◯◯◯◯ +◯◯◯◯◯◯◯ ◯◯◯◯◯◯◯ ◯◯◯◯◯◯◯ ◯◯◯◯◯◯◯ ◯◯◯◯◯◯◯ +ObservationTensor(1): +◯◯◯◯◯◯◯ ◯◯◯◯◯◯◯ ◯◯◯◯◯◯◯ ◯◯◯◯◯◯◯ ◯◯◯◯◯◯◯ +◯◯◯◯◯◯◯ ◯◯◯◯◯◯◯ ◯◯◯◯◯◯◯ ◯◯◯◯◯◯◯ ◯◯◯◯◯◯◯ +◉◯◯◯◯◯◯ ◯◯◯◯◯◯◉ ◉◯◯◯◯◯◯ ◯◯◯◯◯◯◉ ◯◯◯◯◯◯◯ +◯◯◯◯◯◯◯ ◯◯◯◯◯◯◯ ◯◯◯◯◯◯◯ ◯◯◯◯◯◯◯ ◯◯◯◯◯◯◯ +◯◯◯◯◯◯◯ ◯◯◯◯◯◯◯ ◯◯◯◯◯◯◯ ◯◯◯◯◯◯◯ ◯◯◯◯◯◯◯ +Rewards() = [0, 0] +Returns() = [0, 0] +LegalActions(0) = [0, 1, 2, 3, 4] +LegalActions(1) = [0, 1, 2, 3, 4] +StringLegalActions(0) = ["North", "East", "South", "West", "Stay"] +StringLegalActions(1) = ["North", "East", "South", "West", "Stay"] + +# Apply joint action ["Stay", "Stay"] +actions: [4, 4] + +# State 1 +# ....... +# ....... +# A.....B +# ....... +# ....... +# Carrier(A's flag): -1 Carrier(B's flag): -1 +# Score: A=0 B=0 +# Moves: 0/20 +# Chance Node +IsTerminal() = False +History() = [4, 4] +HistoryString() = "4, 4" +IsChanceNode() = True +IsSimultaneousNode() = False +CurrentPlayer() = -1 +ObservationString(0) = ".......\n.......\nA.....B\n.......\n.......\nCarrier(A's flag): -1 Carrier(B's flag): -1\nScore: A=0 B=0\nMoves: 0/20\nChance Node\n" +ObservationString(1) = ".......\n.......\nA.....B\n.......\n.......\nCarrier(A's flag): -1 Carrier(B's flag): -1\nScore: A=0 B=0\nMoves: 0/20\nChance Node\n" +ObservationTensor(0): +◯◯◯◯◯◯◯ ◯◯◯◯◯◯◯ ◯◯◯◯◯◯◯ ◯◯◯◯◯◯◯ ◯◯◯◯◯◯◯ +◯◯◯◯◯◯◯ ◯◯◯◯◯◯◯ ◯◯◯◯◯◯◯ ◯◯◯◯◯◯◯ ◯◯◯◯◯◯◯ +◉◯◯◯◯◯◯ ◯◯◯◯◯◯◉ ◉◯◯◯◯◯◯ ◯◯◯◯◯◯◉ ◯◯◯◯◯◯◯ +◯◯◯◯◯◯◯ ◯◯◯◯◯◯◯ ◯◯◯◯◯◯◯ ◯◯◯◯◯◯◯ ◯◯◯◯◯◯◯ +◯◯◯◯◯◯◯ ◯◯◯◯◯◯◯ ◯◯◯◯◯◯◯ ◯◯◯◯◯◯◯ ◯◯◯◯◯◯◯ +ObservationTensor(1): +◯◯◯◯◯◯◯ ◯◯◯◯◯◯◯ ◯◯◯◯◯◯◯ ◯◯◯◯◯◯◯ ◯◯◯◯◯◯◯ +◯◯◯◯◯◯◯ ◯◯◯◯◯◯◯ ◯◯◯◯◯◯◯ ◯◯◯◯◯◯◯ ◯◯◯◯◯◯◯ +◉◯◯◯◯◯◯ ◯◯◯◯◯◯◉ ◉◯◯◯◯◯◯ ◯◯◯◯◯◯◉ ◯◯◯◯◯◯◯ +◯◯◯◯◯◯◯ ◯◯◯◯◯◯◯ ◯◯◯◯◯◯◯ ◯◯◯◯◯◯◯ ◯◯◯◯◯◯◯ +◯◯◯◯◯◯◯ ◯◯◯◯◯◯◯ ◯◯◯◯◯◯◯ ◯◯◯◯◯◯◯ ◯◯◯◯◯◯◯ +ChanceOutcomes() = [(0,0.5), (1,0.5)] +LegalActions() = [0, 1] +StringLegalActions() = ["(A resolves first)", "(B resolves first)"] + +# Apply action "(A resolves first)" +action: 0 + +# State 2 +# ....... +# ....... +# A.....B +# ....... +# ....... +# Carrier(A's flag): -1 Carrier(B's flag): -1 +# Score: A=0 B=0 +# Moves: 1/20 +IsTerminal() = False +History() = [4, 4, 0] +HistoryString() = "4, 4, 0" +IsChanceNode() = False +IsSimultaneousNode() = True +CurrentPlayer() = -2 +ObservationString(0) = ".......\n.......\nA.....B\n.......\n.......\nCarrier(A's flag): -1 Carrier(B's flag): -1\nScore: A=0 B=0\nMoves: 1/20\n" +ObservationString(1) = ".......\n.......\nA.....B\n.......\n.......\nCarrier(A's flag): -1 Carrier(B's flag): -1\nScore: A=0 B=0\nMoves: 1/20\n" +ObservationTensor(0): +◯◯◯◯◯◯◯ ◯◯◯◯◯◯◯ ◯◯◯◯◯◯◯ ◯◯◯◯◯◯◯ ◯◯◯◯◯◯◯ +◯◯◯◯◯◯◯ ◯◯◯◯◯◯◯ ◯◯◯◯◯◯◯ ◯◯◯◯◯◯◯ ◯◯◯◯◯◯◯ +◉◯◯◯◯◯◯ ◯◯◯◯◯◯◉ ◉◯◯◯◯◯◯ ◯◯◯◯◯◯◉ ◯◯◯◯◯◯◯ +◯◯◯◯◯◯◯ ◯◯◯◯◯◯◯ ◯◯◯◯◯◯◯ ◯◯◯◯◯◯◯ ◯◯◯◯◯◯◯ +◯◯◯◯◯◯◯ ◯◯◯◯◯◯◯ ◯◯◯◯◯◯◯ ◯◯◯◯◯◯◯ ◯◯◯◯◯◯◯ +ObservationTensor(1): +◯◯◯◯◯◯◯ ◯◯◯◯◯◯◯ ◯◯◯◯◯◯◯ ◯◯◯◯◯◯◯ ◯◯◯◯◯◯◯ +◯◯◯◯◯◯◯ ◯◯◯◯◯◯◯ ◯◯◯◯◯◯◯ ◯◯◯◯◯◯◯ ◯◯◯◯◯◯◯ +◉◯◯◯◯◯◯ ◯◯◯◯◯◯◉ ◉◯◯◯◯◯◯ ◯◯◯◯◯◯◉ ◯◯◯◯◯◯◯ +◯◯◯◯◯◯◯ ◯◯◯◯◯◯◯ ◯◯◯◯◯◯◯ ◯◯◯◯◯◯◯ ◯◯◯◯◯◯◯ +◯◯◯◯◯◯◯ ◯◯◯◯◯◯◯ ◯◯◯◯◯◯◯ ◯◯◯◯◯◯◯ ◯◯◯◯◯◯◯ +Rewards() = [0, 0] +Returns() = [0, 0] +LegalActions(0) = [0, 1, 2, 3, 4] +LegalActions(1) = [0, 1, 2, 3, 4] +StringLegalActions(0) = ["North", "East", "South", "West", "Stay"] +StringLegalActions(1) = ["North", "East", "South", "West", "Stay"] + +# Apply joint action ["South", "Stay"] +actions: [2, 4] + +# State 3 +# ....... +# ....... +# A.....B +# ....... +# ....... +# Carrier(A's flag): -1 Carrier(B's flag): -1 +# Score: A=0 B=0 +# Moves: 1/20 +# Chance Node +IsTerminal() = False +History() = [4, 4, 0, 2, 4] +HistoryString() = "4, 4, 0, 2, 4" +IsChanceNode() = True +IsSimultaneousNode() = False +CurrentPlayer() = -1 +ObservationString(0) = ".......\n.......\nA.....B\n.......\n.......\nCarrier(A's flag): -1 Carrier(B's flag): -1\nScore: A=0 B=0\nMoves: 1/20\nChance Node\n" +ObservationString(1) = ".......\n.......\nA.....B\n.......\n.......\nCarrier(A's flag): -1 Carrier(B's flag): -1\nScore: A=0 B=0\nMoves: 1/20\nChance Node\n" +ObservationTensor(0): +◯◯◯◯◯◯◯ ◯◯◯◯◯◯◯ ◯◯◯◯◯◯◯ ◯◯◯◯◯◯◯ ◯◯◯◯◯◯◯ +◯◯◯◯◯◯◯ ◯◯◯◯◯◯◯ ◯◯◯◯◯◯◯ ◯◯◯◯◯◯◯ ◯◯◯◯◯◯◯ +◉◯◯◯◯◯◯ ◯◯◯◯◯◯◉ ◉◯◯◯◯◯◯ ◯◯◯◯◯◯◉ ◯◯◯◯◯◯◯ +◯◯◯◯◯◯◯ ◯◯◯◯◯◯◯ ◯◯◯◯◯◯◯ ◯◯◯◯◯◯◯ ◯◯◯◯◯◯◯ +◯◯◯◯◯◯◯ ◯◯◯◯◯◯◯ ◯◯◯◯◯◯◯ ◯◯◯◯◯◯◯ ◯◯◯◯◯◯◯ +ObservationTensor(1): +◯◯◯◯◯◯◯ ◯◯◯◯◯◯◯ ◯◯◯◯◯◯◯ ◯◯◯◯◯◯◯ ◯◯◯◯◯◯◯ +◯◯◯◯◯◯◯ ◯◯◯◯◯◯◯ ◯◯◯◯◯◯◯ ◯◯◯◯◯◯◯ ◯◯◯◯◯◯◯ +◉◯◯◯◯◯◯ ◯◯◯◯◯◯◉ ◉◯◯◯◯◯◯ ◯◯◯◯◯◯◉ ◯◯◯◯◯◯◯ +◯◯◯◯◯◯◯ ◯◯◯◯◯◯◯ ◯◯◯◯◯◯◯ ◯◯◯◯◯◯◯ ◯◯◯◯◯◯◯ +◯◯◯◯◯◯◯ ◯◯◯◯◯◯◯ ◯◯◯◯◯◯◯ ◯◯◯◯◯◯◯ ◯◯◯◯◯◯◯ +ChanceOutcomes() = [(0,0.5), (1,0.5)] +LegalActions() = [0, 1] +StringLegalActions() = ["(A resolves first)", "(B resolves first)"] + +# Apply action "(B resolves first)" +action: 1 + +# State 4 +# ....... +# ....... +# a.....B +# A...... +# ....... +# Carrier(A's flag): -1 Carrier(B's flag): -1 +# Score: A=0 B=0 +# Moves: 2/20 +IsTerminal() = False +History() = [4, 4, 0, 2, 4, 1] +HistoryString() = "4, 4, 0, 2, 4, 1" +IsChanceNode() = False +IsSimultaneousNode() = True +CurrentPlayer() = -2 +ObservationString(0) = ".......\n.......\na.....B\nA......\n.......\nCarrier(A's flag): -1 Carrier(B's flag): -1\nScore: A=0 B=0\nMoves: 2/20\n" +ObservationString(1) = ".......\n.......\na.....B\nA......\n.......\nCarrier(A's flag): -1 Carrier(B's flag): -1\nScore: A=0 B=0\nMoves: 2/20\n" +ObservationTensor(0): +◯◯◯◯◯◯◯ ◯◯◯◯◯◯◯ ◯◯◯◯◯◯◯ ◯◯◯◯◯◯◯ ◯◯◯◯◯◯◯ +◯◯◯◯◯◯◯ ◯◯◯◯◯◯◯ ◯◯◯◯◯◯◯ ◯◯◯◯◯◯◯ ◯◯◯◯◯◯◯ +◯◯◯◯◯◯◯ ◯◯◯◯◯◯◉ ◉◯◯◯◯◯◯ ◯◯◯◯◯◯◉ ◯◯◯◯◯◯◯ +◉◯◯◯◯◯◯ ◯◯◯◯◯◯◯ ◯◯◯◯◯◯◯ ◯◯◯◯◯◯◯ ◯◯◯◯◯◯◯ +◯◯◯◯◯◯◯ ◯◯◯◯◯◯◯ ◯◯◯◯◯◯◯ ◯◯◯◯◯◯◯ ◯◯◯◯◯◯◯ +ObservationTensor(1): +◯◯◯◯◯◯◯ ◯◯◯◯◯◯◯ ◯◯◯◯◯◯◯ ◯◯◯◯◯◯◯ ◯◯◯◯◯◯◯ +◯◯◯◯◯◯◯ ◯◯◯◯◯◯◯ ◯◯◯◯◯◯◯ ◯◯◯◯◯◯◯ ◯◯◯◯◯◯◯ +◯◯◯◯◯◯◯ ◯◯◯◯◯◯◉ ◉◯◯◯◯◯◯ ◯◯◯◯◯◯◉ ◯◯◯◯◯◯◯ +◉◯◯◯◯◯◯ ◯◯◯◯◯◯◯ ◯◯◯◯◯◯◯ ◯◯◯◯◯◯◯ ◯◯◯◯◯◯◯ +◯◯◯◯◯◯◯ ◯◯◯◯◯◯◯ ◯◯◯◯◯◯◯ ◯◯◯◯◯◯◯ ◯◯◯◯◯◯◯ +Rewards() = [0, 0] +Returns() = [0, 0] +LegalActions(0) = [0, 1, 2, 3, 4] +LegalActions(1) = [0, 1, 2, 3, 4] +StringLegalActions(0) = ["North", "East", "South", "West", "Stay"] +StringLegalActions(1) = ["North", "East", "South", "West", "Stay"] + +# Apply joint action ["South", "Stay"] +actions: [2, 4] + +# State 5 +# Apply action "(A resolves first)" +action: 0 + +# State 6 +# Apply joint action ["East", "East"] +actions: [1, 1] + +# State 7 +# Apply action "(A resolves first)" +action: 0 + +# State 8 +# Apply joint action ["West", "North"] +actions: [3, 0] + +# State 9 +# Apply action "(A resolves first)" +action: 0 + +# State 10 +# Apply joint action ["North", "West"] +actions: [0, 3] + +# State 11 +# Apply action "(B resolves first)" +action: 1 + +# State 12 +# Apply joint action ["West", "East"] +actions: [3, 1] + +# State 13 +# Apply action "(B resolves first)" +action: 1 + +# State 14 +# Apply joint action ["East", "West"] +actions: [1, 3] + +# State 15 +# Apply action "(B resolves first)" +action: 1 + +# State 16 +# Apply joint action ["South", "West"] +actions: [2, 3] + +# State 17 +# Apply action "(A resolves first)" +action: 0 + +# State 18 +# Apply joint action ["North", "South"] +actions: [0, 2] + +# State 19 +# Apply action "(A resolves first)" +action: 0 + +# State 20 +# ....... +# ....... +# a...B.b +# .A..... +# ....... +# Carrier(A's flag): -1 Carrier(B's flag): -1 +# Score: A=0 B=0 +# Moves: 10/20 +IsTerminal() = False +History() = [4, 4, 0, 2, 4, 1, 2, 4, 0, 1, 1, 0, 3, 0, 0, 0, 3, 1, 3, 1, 1, 1, 3, 1, 2, 3, 0, 0, 2, 0] +HistoryString() = "4, 4, 0, 2, 4, 1, 2, 4, 0, 1, 1, 0, 3, 0, 0, 0, 3, 1, 3, 1, 1, 1, 3, 1, 2, 3, 0, 0, 2, 0" +IsChanceNode() = False +IsSimultaneousNode() = True +CurrentPlayer() = -2 +ObservationString(0) = ".......\n.......\na...B.b\n.A.....\n.......\nCarrier(A's flag): -1 Carrier(B's flag): -1\nScore: A=0 B=0\nMoves: 10/20\n" +ObservationString(1) = ".......\n.......\na...B.b\n.A.....\n.......\nCarrier(A's flag): -1 Carrier(B's flag): -1\nScore: A=0 B=0\nMoves: 10/20\n" +ObservationTensor(0): +◯◯◯◯◯◯◯ ◯◯◯◯◯◯◯ ◯◯◯◯◯◯◯ ◯◯◯◯◯◯◯ ◯◯◯◯◯◯◯ +◯◯◯◯◯◯◯ ◯◯◯◯◯◯◯ ◯◯◯◯◯◯◯ ◯◯◯◯◯◯◯ ◯◯◯◯◯◯◯ +◯◯◯◯◯◯◯ ◯◯◯◯◉◯◯ ◉◯◯◯◯◯◯ ◯◯◯◯◯◯◉ ◯◯◯◯◯◯◯ +◯◉◯◯◯◯◯ ◯◯◯◯◯◯◯ ◯◯◯◯◯◯◯ ◯◯◯◯◯◯◯ ◯◯◯◯◯◯◯ +◯◯◯◯◯◯◯ ◯◯◯◯◯◯◯ ◯◯◯◯◯◯◯ ◯◯◯◯◯◯◯ ◯◯◯◯◯◯◯ +ObservationTensor(1): +◯◯◯◯◯◯◯ ◯◯◯◯◯◯◯ ◯◯◯◯◯◯◯ ◯◯◯◯◯◯◯ ◯◯◯◯◯◯◯ +◯◯◯◯◯◯◯ ◯◯◯◯◯◯◯ ◯◯◯◯◯◯◯ ◯◯◯◯◯◯◯ ◯◯◯◯◯◯◯ +◯◯◯◯◯◯◯ ◯◯◯◯◉◯◯ ◉◯◯◯◯◯◯ ◯◯◯◯◯◯◉ ◯◯◯◯◯◯◯ +◯◉◯◯◯◯◯ ◯◯◯◯◯◯◯ ◯◯◯◯◯◯◯ ◯◯◯◯◯◯◯ ◯◯◯◯◯◯◯ +◯◯◯◯◯◯◯ ◯◯◯◯◯◯◯ ◯◯◯◯◯◯◯ ◯◯◯◯◯◯◯ ◯◯◯◯◯◯◯ +Rewards() = [0, 0] +Returns() = [0, 0] +LegalActions(0) = [0, 1, 2, 3, 4] +LegalActions(1) = [0, 1, 2, 3, 4] +StringLegalActions(0) = ["North", "East", "South", "West", "Stay"] +StringLegalActions(1) = ["North", "East", "South", "West", "Stay"] + +# Apply joint action ["North", "South"] +actions: [0, 2] + +# State 21 +# Apply action "(A resolves first)" +action: 0 + +# State 22 +# Apply joint action ["South", "North"] +actions: [2, 0] + +# State 23 +# Apply action "(B resolves first)" +action: 1 + +# State 24 +# Apply joint action ["South", "East"] +actions: [2, 1] + +# State 25 +# Apply action "(B resolves first)" +action: 1 + +# State 26 +# Apply joint action ["West", "North"] +actions: [3, 0] + +# State 27 +# Apply action "(A resolves first)" +action: 0 + +# State 28 +# Apply joint action ["South", "West"] +actions: [2, 3] + +# State 29 +# Apply action "(A resolves first)" +action: 0 + +# State 30 +# Apply joint action ["South", "East"] +actions: [2, 1] + +# State 31 +# Apply action "(A resolves first)" +action: 0 + +# State 32 +# Apply joint action ["Stay", "West"] +actions: [4, 3] + +# State 33 +# Apply action "(A resolves first)" +action: 0 + +# State 34 +# Apply joint action ["East", "South"] +actions: [1, 2] + +# State 35 +# Apply action "(B resolves first)" +action: 1 + +# State 36 +# Apply joint action ["South", "West"] +actions: [2, 3] + +# State 37 +# Apply action "(A resolves first)" +action: 0 + +# State 38 +# Apply joint action ["North", "West"] +actions: [0, 3] + +# State 39 +# Apply action "(A resolves first)" +action: 0 + +# State 40 +# ....... +# ....... +# a.B...b +# .A..... +# ....... +# Carrier(A's flag): -1 Carrier(B's flag): -1 +# Score: A=0 B=0 +# Moves: 20/20 +IsTerminal() = True +History() = [4, 4, 0, 2, 4, 1, 2, 4, 0, 1, 1, 0, 3, 0, 0, 0, 3, 1, 3, 1, 1, 1, 3, 1, 2, 3, 0, 0, 2, 0, 0, 2, 0, 2, 0, 1, 2, 1, 1, 3, 0, 0, 2, 3, 0, 2, 1, 0, 4, 3, 0, 1, 2, 1, 2, 3, 0, 0, 3, 0] +HistoryString() = "4, 4, 0, 2, 4, 1, 2, 4, 0, 1, 1, 0, 3, 0, 0, 0, 3, 1, 3, 1, 1, 1, 3, 1, 2, 3, 0, 0, 2, 0, 0, 2, 0, 2, 0, 1, 2, 1, 1, 3, 0, 0, 2, 3, 0, 2, 1, 0, 4, 3, 0, 1, 2, 1, 2, 3, 0, 0, 3, 0" +IsChanceNode() = False +IsSimultaneousNode() = False +CurrentPlayer() = -4 +ObservationString(0) = ".......\n.......\na.B...b\n.A.....\n.......\nCarrier(A's flag): -1 Carrier(B's flag): -1\nScore: A=0 B=0\nMoves: 20/20\n" +ObservationString(1) = ".......\n.......\na.B...b\n.A.....\n.......\nCarrier(A's flag): -1 Carrier(B's flag): -1\nScore: A=0 B=0\nMoves: 20/20\n" +ObservationTensor(0): +◯◯◯◯◯◯◯ ◯◯◯◯◯◯◯ ◯◯◯◯◯◯◯ ◯◯◯◯◯◯◯ ◯◯◯◯◯◯◯ +◯◯◯◯◯◯◯ ◯◯◯◯◯◯◯ ◯◯◯◯◯◯◯ ◯◯◯◯◯◯◯ ◯◯◯◯◯◯◯ +◯◯◯◯◯◯◯ ◯◯◉◯◯◯◯ ◉◯◯◯◯◯◯ ◯◯◯◯◯◯◉ ◯◯◯◯◯◯◯ +◯◉◯◯◯◯◯ ◯◯◯◯◯◯◯ ◯◯◯◯◯◯◯ ◯◯◯◯◯◯◯ ◯◯◯◯◯◯◯ +◯◯◯◯◯◯◯ ◯◯◯◯◯◯◯ ◯◯◯◯◯◯◯ ◯◯◯◯◯◯◯ ◯◯◯◯◯◯◯ +ObservationTensor(1): +◯◯◯◯◯◯◯ ◯◯◯◯◯◯◯ ◯◯◯◯◯◯◯ ◯◯◯◯◯◯◯ ◯◯◯◯◯◯◯ +◯◯◯◯◯◯◯ ◯◯◯◯◯◯◯ ◯◯◯◯◯◯◯ ◯◯◯◯◯◯◯ ◯◯◯◯◯◯◯ +◯◯◯◯◯◯◯ ◯◯◉◯◯◯◯ ◉◯◯◯◯◯◯ ◯◯◯◯◯◯◉ ◯◯◯◯◯◯◯ +◯◉◯◯◯◯◯ ◯◯◯◯◯◯◯ ◯◯◯◯◯◯◯ ◯◯◯◯◯◯◯ ◯◯◯◯◯◯◯ +◯◯◯◯◯◯◯ ◯◯◯◯◯◯◯ ◯◯◯◯◯◯◯ ◯◯◯◯◯◯◯ ◯◯◯◯◯◯◯ +Rewards() = [0, 0] +Returns() = [0, 0] diff --git a/open_spiel/python/tests/pyspiel_test.py b/open_spiel/python/tests/pyspiel_test.py index 21eb61790f..7b07b8bc68 100644 --- a/open_spiel/python/tests/pyspiel_test.py +++ b/open_spiel/python/tests/pyspiel_test.py @@ -47,6 +47,7 @@ "bridge", "bridge_uncontested_bidding", "cached_tree", + "capture_the_flag", "catch", "chat_game", # python game locating in python/games/chat_games/ "checkers",