Skip to content

Commit

Permalink
Merge pull request #72 from jamesdfrost:backgammonnotation
Browse files Browse the repository at this point in the history
PiperOrigin-RevId: 271164614
Change-Id: I589fb59012add826b5945527b0f7839734df949c
  • Loading branch information
open_spiel@google.com authored and open_spiel@google.com committed Sep 26, 2019
2 parents a7a7702 + 04b249b commit 2aeee5a
Show file tree
Hide file tree
Showing 4 changed files with 1,655 additions and 1,815 deletions.
142 changes: 138 additions & 4 deletions open_spiel/games/backgammon.cc
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,13 @@
namespace open_spiel {
namespace backgammon {
namespace {

// A few constants to help with the conversion to human-readable string formats.
// TODO: remove these once we've changed kBarPos and kScorePos (see TODO in
// header).
constexpr int kNumBarPosHumanReadable = 25;
constexpr int kNumOffPosHumanReadable = -2;

const std::vector<std::pair<Action, double>> kChanceOutcomes = {
std::pair<Action, double>(0, 1.0 / 18),
std::pair<Action, double>(1, 1.0 / 18),
Expand Down Expand Up @@ -106,18 +113,145 @@ std::string PositionToString(int pos) {
}
}

std::string PositionToStringHumanReadable(int pos) {
if (pos == kNumBarPosHumanReadable) {
return "Bar";
} else if (pos == kNumOffPosHumanReadable) {
return "Off";
} else {
return PositionToString(pos);
}
}

int BackgammonState::AugmentCheckerMove(CheckerMove* cmove, int player,
int start) const {
int end = cmove->num;
if (end != kPassPos) {
// Not a pass, so work out where the piece finished
end = start - cmove->num;
if (end <= 0) {
end = kNumOffPosHumanReadable; // Off
} else if (board_[Opponent(player)]
[player == kOPlayerId ? (end - 1)
: (kNumPoints - end)] == 1) {
cmove->hit = true; // Check to see if move is a hit
}
}
return end;
}

std::string BackgammonState::ActionToString(Player player,
Action move_id) const {
if (player == kChancePlayerId) {
return absl::StrCat("chance outcome ", move_id,
" (roll: ", kChanceOutcomeValues[move_id][0],
kChanceOutcomeValues[move_id][1], ")");
} else {
// Assemble a human-readable string representation of the move using
// standard backgammon notation:
//
// - Always show the numbering going from Bar->24->0->Off, irrespective of
// which player is moving.
// - Show the start position followed by end position.
// - Show hits with an asterisk, e.g. 9/7*.
// - Order the moves by highest number first, e.g. 22/7 10/8 not 10/8 22/7.
// Not an official requirement, but seems to be standard convention.
// - Show duplicate moves as 10/8(2).
// - Show moves on a single piece as 10/8/5 not 10/8 8/5
//
// Note that there are tests to ensure the ActionToString follows this
// output format. Any changes would need to be reflected in the tests as
// well.
std::vector<CheckerMove> cmoves = SpielMoveToCheckerMoves(player, move_id);
return absl::StrCat(move_id, " (", PositionToString(cmoves[0].pos), "-",
cmoves[0].num, cmoves[0].hit ? "*" : "", " ",
PositionToString(cmoves[1].pos), "-", cmoves[1].num,
cmoves[1].hit ? "*" : "", ")");

int cmove0_start;
int cmove1_start;
if (player == kOPlayerId) {
cmove0_start = (cmoves[0].pos == kBarPos ?
kNumBarPosHumanReadable : cmoves[0].pos + 1);
cmove1_start = (cmoves[1].pos == kBarPos ?
kNumBarPosHumanReadable : cmoves[1].pos + 1);
} else {
// swap the board numbering round for Player X so player is moving
// from 24->0
cmove0_start = (cmoves[0].pos == kBarPos ?
kNumBarPosHumanReadable : kNumPoints - cmoves[0].pos);
cmove1_start = (cmoves[1].pos == kBarPos ?
kNumBarPosHumanReadable : kNumPoints - cmoves[1].pos);
}

// Add hit information and compute whether the moves go off the board.
int cmove0_end = AugmentCheckerMove(&cmoves[0], player, cmove0_start);
int cmove1_end = AugmentCheckerMove(&cmoves[1], player, cmove1_start);

// check for 2 pieces hitting on the same point.
bool double_hit =
(cmoves[1].hit && cmoves[0].hit && cmove1_end == cmove0_end);

std::string returnVal = "";
if (cmove0_start == cmove1_start &&
cmove0_end == cmove1_end) { // same move, show as (2).
if (cmoves[1].num == kPassPos) { // Player can't move at all!
returnVal = "Pass";
} else {
returnVal = absl::StrCat(move_id, " - ",
PositionToStringHumanReadable(cmove0_start),
"/", PositionToStringHumanReadable(cmove0_end),
cmoves[0].hit ? "*" : "", "(2)");
}
} else if ((cmove0_start < cmove1_start ||
(cmove0_start == cmove1_start && cmove0_end < cmove1_end) ||
cmoves[0].num == kPassPos) &&
cmoves[1].num != kPassPos) {
// tradition to start with higher numbers first,
// so swap moves round if this not the case. If
// there is a pass move, put it last.
if (cmove1_end == cmove0_start) {
// Check to see if the same piece is moving for both
// moves, as this changes the format of the output.
returnVal = absl::StrCat(
move_id, " - ", PositionToStringHumanReadable(cmove1_start), "/",
PositionToStringHumanReadable(cmove1_end), cmoves[1].hit ? "*" : "",
"/", PositionToStringHumanReadable(cmove0_end),
cmoves[0].hit ? "*" : "");
} else {
returnVal = absl::StrCat(
move_id, " - ", PositionToStringHumanReadable(cmove1_start), "/",
PositionToStringHumanReadable(cmove1_end), cmoves[1].hit ? "*" : "",
" ",
(cmoves[0].num != kPassPos)
? PositionToStringHumanReadable(cmove0_start)
: "",
(cmoves[0].num != kPassPos) ? "/" : "",
PositionToStringHumanReadable(cmove0_end),
(cmoves[0].hit && !double_hit) ? "*" : "");
}
} else {
if (cmove0_end == cmove1_start) {
// Check to see if the same piece is moving for both
// moves, as this changes the format of the output.
returnVal = absl::StrCat(move_id, " - ",
PositionToStringHumanReadable(cmove0_start),
"/", PositionToStringHumanReadable(cmove0_end),
cmoves[0].hit ? "*" : "", "/",
PositionToStringHumanReadable(cmove1_end),
cmoves[1].hit ? "*" : "");
} else {
returnVal =
absl::StrCat(move_id, " - ",
PositionToStringHumanReadable(cmove0_start), "/",
PositionToStringHumanReadable(cmove0_end),
cmoves[0].hit ? "*" : "", " ",
(cmoves[1].num != kPassPos)
? PositionToStringHumanReadable(cmove1_start)
: "",
(cmoves[1].num != kPassPos) ? "/" : "",
PositionToStringHumanReadable(cmove1_end),
(cmoves[1].hit && !double_hit) ? "*" : "");
}
}

return returnVal;
}
}

Expand Down
10 changes: 9 additions & 1 deletion open_spiel/games/backgammon.h
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,13 @@ constexpr const int kNumDiceOutcomes = 6;
constexpr const int kNumCheckersPerPlayer = 15;
constexpr const int kXPlayerId = 0;
constexpr const int kOPlayerId = 1;
constexpr const int kPassPos = -1;

// TODO: look into whether these can be set to 25 and -2 to avoid having a
// separate helper function (PositionToStringHumanReadable) to convert moves
// to strings.
constexpr const int kBarPos = 100;
constexpr const int kScorePos = 101;
constexpr const int kPassPos = -1;

// The action encoding stores a number in { 0, 1, ..., 1351 }. If the high
// roll is to move first, then the number is encoded as a 2-digit number in
Expand Down Expand Up @@ -199,6 +203,10 @@ class BackgammonState : public State {
Action EncodedPassMove() const;
Action EncodedBarMove() const;

// A helper function used by ActionToString to add necessary hit information
// and compute whether the move goes off the board.
int AugmentCheckerMove(CheckerMove* cmove, int player, int start) const;

// Returns the position of the furthest checker in the home of this player.
// Returns -1 if none found.
int FurthestCheckerInHome(int player) const;
Expand Down
189 changes: 189 additions & 0 deletions open_spiel/games/backgammon_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -332,6 +332,194 @@ void DoublesBearOffOutsideHome() {
action = bstate->CheckerMovesToSpielMove({{20, 4, false}, {20, 4, false}});
SPIEL_CHECK_TRUE(ActionsContains(legal_actions, action));
}
void HumanReadableNotation() {
std::unique_ptr<Game> game = LoadGame("backgammon");
std::unique_ptr<State> state = game->NewInitialState();
BackgammonState* bstate = static_cast<BackgammonState*>(state.get());

// Check double repeated move and moving on from Bar displayed correctly
bstate->SetState(
kXPlayerId, false, {1, 1}, {13, 5}, {0, 0},
{{2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
{0, 0, 0, 0, 0, 5, 5, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}});
std::cout << bstate->ToString();
std::vector<Action> legal_actions = bstate->LegalActions();
std::cout << "First legal action:" << std::endl;
std::string notation = bstate->ActionToString(kXPlayerId, legal_actions[0]);
std::cout << notation << std::endl;
SPIEL_CHECK_EQ(notation, absl::StrCat(legal_actions[0], " - Bar/24(2)"));

// Check hits displayed correctly
bstate->SetState(
kXPlayerId, false, {2, 1}, {13, 5}, {0, 0},
{{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1},
{1, 1, 1, 1, 1, 5, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}});
std::cout << bstate->ToString();
legal_actions = bstate->LegalActions();
std::cout << "First legal action:" << std::endl;
notation = bstate->ActionToString(kXPlayerId, legal_actions[0]);
std::cout << notation << std::endl;
SPIEL_CHECK_EQ(notation,
absl::StrCat(legal_actions[0], " - Bar/24* Bar/23*"));

// Check moving off displayed correctly
bstate->SetState(
kXPlayerId, false, {2, 1}, {0, 0}, {13, 5},
{{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1},
{0, 0, 0, 0, 0, 5, 5, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}});
std::cout << bstate->ToString();
legal_actions = bstate->LegalActions();
std::cout << "First legal action:" << std::endl;
notation = bstate->ActionToString(kXPlayerId, legal_actions[0]);
std::cout << notation << std::endl;
SPIEL_CHECK_EQ(notation, absl::StrCat(legal_actions[0], " - 2/Off 1/Off"));

// Check die order doesnt impact narrative
bstate->SetState(
kXPlayerId, false, {1, 2}, {0, 0}, {13, 5},
{{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1},
{0, 0, 0, 0, 0, 5, 5, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}});
std::cout << bstate->ToString();
legal_actions = bstate->LegalActions();
std::cout << "First legal action:" << std::endl;
notation = bstate->ActionToString(kXPlayerId, legal_actions[0]);
std::cout << notation << std::endl;
SPIEL_CHECK_EQ(notation, absl::StrCat(legal_actions[0], " - 2/Off 1/Off"));

// Check double move
bstate->SetState(
kXPlayerId, false, {6, 5}, {0, 0}, {13, 5},
{{2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
{0, 2, 2, 2, 2, 2, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}});
std::cout << bstate->ToString();
legal_actions = bstate->LegalActions();
std::cout << "First legal action" << std::endl;
notation = bstate->ActionToString(kXPlayerId, legal_actions[0]);
std::cout << notation << std::endl;
SPIEL_CHECK_EQ(notation, absl::StrCat(legal_actions[0], " - 24/18/13"));

// Check double move with hit
bstate->SetState(
kXPlayerId, false, {6, 5}, {0, 0}, {13, 4},
{{2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
{0, 2, 2, 2, 2, 2, 1, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}});
std::cout << bstate->ToString();
legal_actions = bstate->LegalActions();
std::cout << "First legal action:" << std::endl;
notation = bstate->ActionToString(kXPlayerId, legal_actions[0]);
std::cout << notation << std::endl;
SPIEL_CHECK_EQ(notation, absl::StrCat(legal_actions[0], " - 24/18*/13"));

// Check double move with double hit
bstate->SetState(
kXPlayerId, false, {6, 5}, {0, 0}, {13, 3},
{{2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
{0, 2, 2, 2, 2, 2, 1, 0, 0, 0, 0, 1,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}});
std::cout << bstate->ToString();
legal_actions = bstate->LegalActions();
std::cout << "First legal action:" << std::endl;
notation = bstate->ActionToString(kXPlayerId, legal_actions[0]);
std::cout << notation << std::endl;
SPIEL_CHECK_EQ(notation, absl::StrCat(legal_actions[0], " - 24/18*/13*"));

// Check ordinary move!
bstate->SetState(
kXPlayerId, false, {6, 5}, {0, 0}, {13, 3},
{{2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
{0, 2, 2, 2, 4, 0, 0, 0, 0, 0, 0, 2,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}});
std::cout << bstate->ToString();
legal_actions = bstate->LegalActions();
std::cout << "First legal action:" << std::endl;
notation = bstate->ActionToString(kXPlayerId, legal_actions[0]);
std::cout << notation << std::endl;
SPIEL_CHECK_EQ(notation, absl::StrCat(legal_actions[0], " - 24/19 24/18"));

// Check ordinary move with die reversed
bstate->SetState(
kXPlayerId, false, {5, 6}, {0, 0}, {13, 3},
{{2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
{0, 2, 2, 2, 4, 0, 0, 0, 0, 0, 0, 2,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}});
std::cout << bstate->ToString();
legal_actions = bstate->LegalActions();
std::cout << "First legal actions:" << std::endl;
notation = bstate->ActionToString(kXPlayerId, legal_actions[0]);
std::cout << notation << std::endl;
SPIEL_CHECK_EQ(notation, absl::StrCat(legal_actions[0], " - 24/19 24/18"));

// Check ordinary move with 1st hit
bstate->SetState(
kXPlayerId, false, {6, 5}, {0, 0}, {13, 3},
{{2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
{0, 2, 2, 2, 3, 1, 0, 0, 0, 0, 0, 2,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}});
std::cout << bstate->ToString();
legal_actions = bstate->LegalActions();
std::cout << "First legal action:" << std::endl;
notation = bstate->ActionToString(kXPlayerId, legal_actions[0]);
std::cout << notation << std::endl;
SPIEL_CHECK_EQ(notation, absl::StrCat(legal_actions[0], " - 24/19* 24/18"));

// Check ordinary move with 2nd hit
bstate->SetState(
kXPlayerId, false, {5, 6}, {0, 0}, {13, 3},
{{2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
{0, 2, 2, 2, 3, 0, 1, 0, 0, 0, 0, 2,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}});
std::cout << bstate->ToString();
legal_actions = bstate->LegalActions();
std::cout << "First legal action:" << std::endl;
notation = bstate->ActionToString(kXPlayerId, legal_actions[0]);
std::cout << notation << std::endl;
SPIEL_CHECK_EQ(notation, absl::StrCat(legal_actions[0], " - 24/19 24/18*"));

// Check ordinary move with double hit
bstate->SetState(
kXPlayerId, false, {5, 6}, {0, 0}, {13, 3},
{{2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
{0, 2, 2, 2, 2, 1, 1, 0, 0, 0, 0, 2,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}});
std::cout << bstate->ToString();
legal_actions = bstate->LegalActions();
std::cout << "First legal action:" << std::endl;
notation = bstate->ActionToString(kXPlayerId, legal_actions[0]);
std::cout << notation << std::endl;
SPIEL_CHECK_EQ(notation, absl::StrCat(legal_actions[0], " - 24/19* 24/18*"));

// Check double pass
bstate->SetState(
kXPlayerId, false, {5, 3}, {0, 0}, {13, 3},
{{2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
{0, 2, 2, 2, 2, 2, 0, 0, 0, 0, 0, 2,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}});
std::cout << bstate->ToString();
legal_actions = bstate->LegalActions();
std::cout << "First legal action:" << std::endl;
notation = bstate->ActionToString(kXPlayerId, legal_actions[0]);
std::cout << notation << std::endl;
SPIEL_CHECK_EQ(notation, "Pass");

// Check single pass
bstate->SetState(
kXPlayerId, false, {5, 6}, {0, 0}, {13, 3},
{{2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
{0, 2, 2, 2, 2, 2, 0, 0, 0, 0, 0, 2,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}});
std::cout << bstate->ToString();
legal_actions = bstate->LegalActions();
std::cout << "First legal action:" << std::endl;
notation = bstate->ActionToString(kXPlayerId, legal_actions[0]);
std::cout << notation << std::endl;
SPIEL_CHECK_EQ(notation, absl::StrCat(legal_actions[0], " - 24/18 Pass"));
}

} // namespace
} // namespace backgammon
Expand All @@ -346,4 +534,5 @@ int main(int argc, char** argv) {
open_spiel::backgammon::BearOffOutsideHome();
open_spiel::backgammon::DoublesBearOffOutsideHome();
open_spiel::backgammon::BasicBackgammonTestsVaryScoring();
open_spiel::backgammon::HumanReadableNotation();
}
Loading

0 comments on commit 2aeee5a

Please sign in to comment.