diff --git a/docs/games.md b/docs/games.md
index 6f0625c252..ef3720f104 100644
--- a/docs/games.md
+++ b/docs/games.md
@@ -75,6 +75,7 @@ Status | Game
~ | [Slovenian Tarok](#slovenian-tarok)
~ | [Skat (simplified bidding)](#skat-simplified-bidding)
~ | [Solitaire (K+)](#solitaire-k)
+~ | [Squadro](#squadro)
![](_static/green_circ10.png "green circle") | [Tic-Tac-Toe](#tic-tac-toe)
![](_static/green_circ10.png "green circle") | [Tiny Bridge](#tiny-bridge)
![](_static/green_circ10.png "green circle") | [Tiny Hanabi](#tiny-hanabi)
@@ -777,6 +778,16 @@ Status | Game
* [Wikipedia](https://en.wikipedia.org/wiki/Klondike_\(solitaire\)) and
[Bjarnason et al. '07, Searching solitaire in real time](http://web.engr.oregonstate.edu/~afern/papers/solitaire.pdf)
+### Squadro
+
+* Each turn, players can move their one of their pieces on the board.
+* Uses tokens on a grid.
+* Modern game.
+* Deterministic.
+* Perfect information.
+* 2 players.
+* [bgg](https://boardgamegeek.com/boardgame/245222/squadro)
+
### Tic-Tac-Toe
* Players place tokens to try and form a pattern.
diff --git a/open_spiel/games/CMakeLists.txt b/open_spiel/games/CMakeLists.txt
index b0a3cb0018..8f536186ae 100644
--- a/open_spiel/games/CMakeLists.txt
+++ b/open_spiel/games/CMakeLists.txt
@@ -157,6 +157,8 @@ set(GAME_SOURCES
skat.h
solitaire.cc
solitaire.h
+ squadro.cc
+ squadro.h
stones_and_gems.cc
stones_and_gems.h
tarok.cc
@@ -560,6 +562,10 @@ add_executable(solitaire_test solitaire_test.cc ${OPEN_SPIEL_OBJECTS}
$)
add_test(solitaire_test solitaire_test)
+add_executable(squadro_test squadro_test.cc ${OPEN_SPIEL_OBJECTS}
+ $)
+add_test(squadro_test squadro_test)
+
add_executable(stones_and_gems_test stones_and_gems_test.cc
${OPEN_SPIEL_OBJECTS}
$)
diff --git a/open_spiel/games/squadro.cc b/open_spiel/games/squadro.cc
new file mode 100644
index 0000000000..f42eaf3b50
--- /dev/null
+++ b/open_spiel/games/squadro.cc
@@ -0,0 +1,310 @@
+// 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/squadro.h"
+
+#include
+#include
+#include
+
+#include "open_spiel/utils/tensor_view.h"
+
+namespace open_spiel {
+namespace squadro {
+namespace {
+
+// Facts about the game
+const GameType kGameType{
+ /*short_name=*/"squadro",
+ /*long_name=*/"Squadro",
+ GameType::Dynamics::kSequential,
+ GameType::ChanceMode::kDeterministic,
+ GameType::Information::kPerfectInformation,
+ GameType::Utility::kZeroSum,
+ GameType::RewardModel::kTerminal,
+ /*max_num_players=*/2,
+ /*min_num_players=*/2,
+ /*provides_information_state_string=*/true,
+ /*provides_information_state_tensor=*/false,
+ /*provides_observation_string=*/true,
+ /*provides_observation_tensor=*/true,
+ /*parameter_specification=*/{} // no parameters
+};
+
+std::shared_ptr Factory(const GameParameters& params) {
+ return std::shared_ptr(new SquadroGame(params));
+}
+
+REGISTER_SPIEL_GAME(kGameType, Factory);
+
+std::string PlayerToString(Player player) {
+ switch (player) {
+ case 0:
+ return "P0";
+ case 1:
+ return "P1";
+ default:
+ SpielFatalError(absl::StrCat("Invalid player id ", player));
+ }
+}
+
+std::string CellToString(int row, int col,
+ const std::array, kNumPlayers>& board) {
+ if (board[0][col].position == row) {
+ if (board[0][col].direction == TokenState::forward) {
+ return "^";
+ } else if (board[0][col].direction == TokenState::backward) {
+ return "v";
+ }
+ }
+ if (board[1][row].position == col) {
+ if (board[1][row].direction == TokenState::forward) {
+ return ">";
+ } else if (board[1][row].direction == TokenState::backward) {
+ return "<";
+ }
+ }
+ return ".";
+}
+
+} // namespace
+
+int SquadroState::CurrentPlayer() const {
+ if (IsTerminal()) {
+ return kTerminalPlayerId;
+ } else {
+ return current_player_;
+ }
+}
+
+bool SquadroState::OverpassOpponent(int opponent, int player_position, Action move) {
+ bool overpassOpponent = false;
+
+ while (board_[opponent][player_position].position == move + 1) {
+ overpassOpponent = true;
+ // Send opponent player back to default position
+ if (board_[opponent][player_position].direction == TokenState::forward) {
+ board_[opponent][player_position].position = 0;
+ } else {
+ board_[opponent][player_position].position = 6;
+ }
+ player_position += board_[current_player_][move + 1].direction == TokenState::forward ? 1 : -1;
+ }
+ board_[current_player_][move + 1].position = player_position;
+ return overpassOpponent;
+}
+
+void SquadroState::DoApplyAction(Action move) {
+ int playerPosition = board_[current_player_][move + 1].position;
+ TokenState playerDirection = board_[current_player_][move + 1].direction;
+ int steps = playerDirection == TokenState::forward ? movements_[current_player_][move].forward : movements_[current_player_][move].backward;
+ int other_player = 1 - current_player_;
+ bool overpassOpponent = false;
+ int unit_move = playerDirection == TokenState::forward ? 1 : -1;
+ int finalPosition = playerPosition + steps;
+
+ do {
+ playerPosition += unit_move;
+ board_[current_player_][move + 1].position = playerPosition;
+ overpassOpponent = OverpassOpponent(other_player, playerPosition, move);
+ } while (playerPosition > 0 && playerPosition < kRows - 1 && unit_move * playerPosition < unit_move * finalPosition && !overpassOpponent);
+
+ if (board_[current_player_][move + 1].position == 0) {
+ board_[current_player_][move + 1].direction = TokenState::missing; // Token removed from the board
+ ++missing_tokens_[current_player_];
+ } else if (board_[current_player_][move + 1].position == 6) {
+ board_[current_player_][move + 1].direction = TokenState::backward; // Invert token direction when it reaches the end of the board
+ }
+
+ if (missing_tokens_[current_player_] == 4) {
+ outcome_ = current_player_ == 0 ? Outcome::kPlayer1 : Outcome::kPlayer2;
+ }
+
+ ++moves_made_;
+ if (moves_made_ >= 200) {
+ outcome_ = Outcome::kDraw;
+ }
+
+ current_player_ = other_player;
+}
+
+std::vector SquadroState::LegalActions() const {
+ std::vector moves;
+ if (IsTerminal()) return moves;
+ for (int pos = 0; pos < kNumActions; ++pos) {
+ if (board_[current_player_][pos + 1].direction != TokenState::missing) moves.push_back(pos);
+ }
+ return moves;
+}
+
+std::string SquadroState::ActionToString(Player player,
+ Action action_id) const {
+ return absl::StrCat(PlayerToString(player), action_id);
+}
+
+SquadroState::SquadroState(std::shared_ptr game)
+ : State(game) {
+ for (int player = 0; player <= 1; ++player) {
+ for (int pos = 0; pos < kRows; ++pos) {
+ if (pos == 0 || pos == 6) {
+ // There are only 5 pieces per player. No pieces are present in the corners.
+ board_[player][pos] = {0, TokenState::missing};
+ } else {
+ board_[player][pos] = {0, TokenState::forward};
+ }
+ }
+ }
+}
+
+std::string SquadroState::ToString() const {
+ std::string str;
+ for (int row = kRows - 1; row >= 0; --row) {
+ for (int col = 0; col < kCols; ++col) {
+ str.append(CellToString(row, col, board_));
+ }
+ str.append("\n");
+ }
+ str.append("C");
+ int current_player = CurrentPlayer();
+ if (current_player == kTerminalPlayerId) {
+ str.append("2");
+ } else {
+ str.append(std::to_string(CurrentPlayer()));
+ }
+ return str;
+}
+
+bool SquadroState::IsTerminal() const {
+ return outcome_ != Outcome::kUnknown;
+}
+
+std::vector SquadroState::Returns() const {
+ if (outcome_ == Outcome::kPlayer1) return {1.0, -1.0};
+ if (outcome_ == Outcome::kPlayer2) return {-1.0, 1.0};
+ return {0.0, 0.0};
+}
+
+std::string SquadroState::InformationStateString(Player player) const {
+ SPIEL_CHECK_GE(player, 0);
+ SPIEL_CHECK_LT(player, num_players_);
+ return HistoryString();
+}
+
+std::string SquadroState::ObservationString(Player player) const {
+ SPIEL_CHECK_GE(player, 0);
+ SPIEL_CHECK_LT(player, num_players_);
+ return ToString();
+}
+
+int SquadroState::CellToInt(int row, int col) const{
+ std::string str = CellToString(row, col, board_);
+ return cell_state_map_.at(str);
+}
+
+void SquadroState::ObservationTensor(Player player,
+ absl::Span values) const {
+ SPIEL_CHECK_GE(player, 0);
+ SPIEL_CHECK_LT(player, num_players_);
+
+ TensorView<2> view(values, {kCellStates, kRows*kCols}, true);
+ for (int row = kRows - 1; row >= 0; --row) {
+ for (int col = 0; col < kCols; ++col) {
+ int cell = row * kCols + col;
+ view[{CellToInt(row, col), cell}] = 1.0;
+ }
+ }
+
+}
+
+std::unique_ptr SquadroState::Clone() const {
+ return std::unique_ptr(new SquadroState(*this));
+}
+
+SquadroGame::SquadroGame(const GameParameters& params)
+ : Game(kGameType, params) {}
+
+SquadroState::SquadroState(std::shared_ptr game,
+ const std::string& str)
+ : State(game) {
+
+ for (int player = 0; player <= 1; ++player) {
+ for (int pos = 0; pos < kRows; ++pos) {
+ board_[player][pos] = {0, TokenState::missing};
+ }
+ }
+
+ int xs = 0;
+ int os = 0;
+ int r = 6;
+ int c = 0;
+ for (const char ch : str) {
+ switch (ch) {
+ case '.':
+ break;
+ case '^':
+ board_[0][c].position = r;
+ board_[0][c].direction = TokenState::forward;
+ break;
+ case '>':
+ board_[1][r].position = c;
+ board_[1][r].direction = TokenState::forward;
+ break;
+ case 'v':
+ board_[0][c].position = r;
+ board_[0][c].direction = TokenState::backward;
+ break;
+ case '<':
+ board_[1][r].position = c;
+ board_[1][r].direction = TokenState::backward;
+ break;
+ case '0':
+ current_player_ = 0;
+ break;
+ case '1':
+ current_player_ = 1;
+ break;
+ case '2':
+ current_player_ = kTerminalPlayerId;
+ break;
+ }
+ if (ch == '.' || ch == '^' || ch == '>' || ch == 'v' || ch == '<' || ch == 'C') {
+ ++c;
+ if (c >= kCols) {
+ r--;
+ c = 0;
+ }
+ }
+ }
+ SPIEL_CHECK_TRUE(r == -1 && ("Problem parsing state (incorrect rows)."));
+ SPIEL_CHECK_TRUE(c == 1 &&
+ ("Problem parsing state (column value should be 0)"));
+
+ int count_p0_tokens = 0;
+ int count_p1_tokens = 0;
+ for (int i = 0; i < kNumActions; ++i) {
+ count_p0_tokens += board_[0][i + 1].direction == TokenState::missing ? 0 : 1;
+ count_p1_tokens += board_[1][i + 1].direction == TokenState::missing ? 0 : 1;
+ }
+
+ if (count_p0_tokens == 1) {
+ outcome_ = Outcome::kPlayer1;
+ } else if (count_p1_tokens == 1) {
+ outcome_ = Outcome::kPlayer2;
+ }
+ SPIEL_CHECK_FALSE(count_p0_tokens == 1 && count_p1_tokens == 1 &&
+ ("P1 and P2 cannot both have a single piece."));
+}
+
+} // namespace squadro
+} // namespace open_spiel
diff --git a/open_spiel/games/squadro.h b/open_spiel/games/squadro.h
new file mode 100644
index 0000000000..1ed38169c0
--- /dev/null
+++ b/open_spiel/games/squadro.h
@@ -0,0 +1,173 @@
+// 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_SQUADRO_H_
+#define OPEN_SPIEL_GAMES_SQUADRO_H_
+
+#include
+#include