Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add new game: Checkers #861

Closed
wants to merge 43 commits into from
Closed

Conversation

Jazeem
Copy link
Contributor

@Jazeem Jazeem commented Jun 10, 2022

No description provided.

@lanctot
Copy link
Collaborator

lanctot commented Jun 10, 2022

Thanks!

Seems like the checkers test is causing a seg fault. This usually means there's a bad memory access (out of bounds indexing into an array).

To find it, I recommend building in debug mode (compiler flags -O0 and possibly -g as well) and run the test using valgrind: https://stackoverflow.com/questions/5134891/how-do-i-use-valgrind-to-find-memory-leaks.

@lanctot
Copy link
Collaborator

lanctot commented Jun 10, 2022

Setting the BUILD_TYPE environment variable to Debug should have all the flags you need, see here https://github.com/deepmind/open_spiel/blob/c1fbaca644aaf6c7dd82eea4f62cccd9f6469a5d/open_spiel/CMakeLists.txt#L43. So you need to rebuild from scratch: (remove the build directory entirely, then set the env var and rerun the build) then run it manually through valgrind.

@Jazeem
Copy link
Contributor Author

Jazeem commented Jun 10, 2022

Thanks. Let me look into it!

@Jazeem
Copy link
Contributor Author

Jazeem commented Jun 13, 2022

Hi @lanctot, something I've noticed in clobber. In this line, shouldn't the rank of the TensorView always be 3 instead of kCellStates?

Maybe I'm missing something.

https://github.com/deepmind/open_spiel/blob/c1fbaca644aaf6c7dd82eea4f62cccd9f6469a5d/open_spiel/games/clobber.cc#L393

@lanctot
Copy link
Collaborator

lanctot commented Jun 13, 2022

Hi @lanctot, something I've noticed in clobber. In this line, shouldn't the rank of the TensorView always be 3 instead of kCellStates?

kCellStates is always 3: one for empty, one for white, and one for black: https://github.com/deepmind/open_spiel/blob/c1fbaca644aaf6c7dd82eea4f62cccd9f6469a5d/open_spiel/games/clobber.cc#L33

We used kCellStates here to be explicit that there's one plane per cell state (empty, white, black) and also for consistency with the other games that do it this way.

@lanctot
Copy link
Collaborator

lanctot commented Jun 13, 2022

Thanks for fixing. I have not looked at the code yet. Curious: how did you handle the case of multiple captures?

@Jazeem
Copy link
Contributor Author

Jazeem commented Jun 13, 2022

Thanks for fixing. I have not looked at the code yet. Curious: how did you handle the case of multiple captures?

I spread it across multiple turns to keep it simple. If a player makes a capture and the same piece has another capture move possible, then the turn will not go to the opponent. The player will get another turn with only the remaining capture move available (all capture moves are anyways mandatory).

But since you've mentioned it. I've noticed an edge case I've missed for multiple jumps. I'll update the code, add test cases and push again.

@Jazeem
Copy link
Contributor Author

Jazeem commented Jun 13, 2022

https://github.com/deepmind/open_spiel/blob/c1fbaca644aaf6c7dd82eea4f62cccd9f6469a5d/open_spiel/games/clobber.cc#L393

We used kCellStates here to be explicit that there's one plane per cell state (empty, white, black) and also for consistency with the other games that do it this way.

My doubt was, to do this wouldn't you have replaced kNumPlayers + 1 with kCellStates inside the braces?

Suppose if I were to add another dimension to the Tensor, a boolean to indicate whether the cell is on the boundary or not. Wouldn't that increase the rank from 3 to 4, although kCellStates value is 3?

Maybe I've got this jumbled up and should go through tensor_view.h

@lanctot
Copy link
Collaborator

lanctot commented Jun 13, 2022

Sorry -- you are indeed correct. I had forgotten the use of TensorView, but it refers to the number of dimensions and should not be confused with the cell states. (It just happens to be 3 in this case, so it works.)

This should read TensorView<3> in clobber. (Do you want to submit a separate PR? If not, very easy to change on our side).

Yeah, using Tensorview does make encoding the observation tensors easier and less error-prone. See the one in breakthrough for example: https://github.com/deepmind/open_spiel/blob/0ffd9584c8be560f4e0f659b3ad93933d9078b48/open_spiel/games/breakthrough.cc#L326

@Jazeem
Copy link
Contributor Author

Jazeem commented Jun 13, 2022

This should read TensorView<3> in clobber. (Do you want to submit a separate PR? If not, very easy to change on our side).

I can create a separate PR.

P.S. I was checking the contributor graph and noticed 1 commit against my name, even though when I click on it I can see several more. Curious to know if I'm missing any git configurations to have caused this discrepancy.

@lanctot
Copy link
Collaborator

lanctot commented Jun 13, 2022

P.S. I was checking the contributor graph and noticed 1 commit against my name, even though when I click on it I can see several more. Curious to know if I'm missing any git configurations to have caused this discrepancy.

Hmm, yeah.. I see that but I'm not really sure what the cause is.

I think our import/export tool (copybara) squashes all the commits from one PR into a single commit when it gets imported. But there has to be more to it than that, because you also updated games.md in a separate PR. Let's see what it looks like after this next PR and Checkers. Could also be how the views get summarized/aggregated on the github side.


// Implementation of the board game Checkers.
// https://en.wikipedia.org/wiki/Checkers
//
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I noticed your implementation has rows and columns as paramters. We usually list them all here (see headers of other games)

//
// Some notes about this implementation:
// - Drawing:
// Game is drawn if no pieces have been removed in 40 moves
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

40 Seems a bit arbitrary-- is this a commonly chosen value for this game? If so maybe mention that in the comments here?


Player current_player_ = 0; // Player zero (White, 'o') goes first.
Player outcome_ = kInvalidPlayer;
int multiple_jump_piece_ = 0; // Piece in the board who can do multiple jump. Represented by row * rows_ + column
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you say a bit more in the comments here about how this variable is used or why it's needed?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This variable is used to indicate that the last moved piece can make an additional jump (multiple jumps). In that case LegalActions is filtered to only represent moves available for this piece.

I'm not entirely happy with the implementation since I had to use a global variable, if you have other thoughts, do share them

Copy link
Collaborator

@lanctot lanctot Jul 5, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok great. But, what I meant was: could you elaborate on that in the comment here.

Also, can we please clarify what version of the rules you are using (at the top of the header file)? I wasn't aware of forced multi-captures, e.g. I don't see anything about it mentioned on https://en.wikipedia.org/wiki/Checkers. Maybe this is a common convention used in computer Checkers? If so, please clarify that in the documentation at the top of the file. Also e.g. the choice of 40 moves without capture, had not heard of it.. but maybe this is part of a specific rule set or checkers convention?

Copy link
Contributor Author

@Jazeem Jazeem Jul 9, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's mentioned in the Wiki link that

When capturing an opponent's piece is possible, capturing is mandatory in most official rules.

That's why I added those. About the 40 move rule, I could find some mentions of it online

http://www.flyordie.com/games/help/checkers/en/games_rules_checkers.html

Copy link
Member

@tewalds tewalds left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for submitting this game! It's a great addition!

Feel free to push back on some of the more design type comments/suggestions.

It's also worth taking a look at the Google C++ style guide. You followed most of it, but in particular the 80 char line length limit needs a closer look.

namespace open_spiel {
namespace checkers {

inline constexpr int kNumPlayers = 2;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Marking these as inline seems redundant.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's true. Removed it.


Player current_player_ = 0; // Player zero (White, 'o') goes first.
Player outcome_ = kInvalidPlayer;
int multiple_jump_piece_ = 0; // Piece in the board who can do multiple jump. Represented by row * rows_ + column
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We've got an 80 char line limit.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Have fixed these


Player current_player_ = 0; // Player zero (White, 'o') goes first.
Player outcome_ = kInvalidPlayer;
int multiple_jump_piece_ = 0; // Piece in the board who can do multiple jump. Represented by row * rows_ + column
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there only one piece that can jump multiple times?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. This variable is used to identify a piece with a multiple jump possibility that has completed it's first jump. Hence there'll be only one such piece.

@@ -0,0 +1,546 @@
// Copyright 2019 DeepMind Technologies Limited
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2022

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed

// Index 1: Direction is diagonally up-right.
// Index 2: Direction is diagonally down-right.
// Index 3: Direction is diagonally down-left.
constexpr std::array<int, kNumDirections> kDirRowOffsets = {{-1, -1, 1, 1}};
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it worth having this as
std:array<std::array<int, 2>, kNumDirection>
that includes both row and column?

Is it even worth having a Point/Location type struct with x/y or row/column values, possibly with a + operator.
Here are two that I wrote:
https://github.com/deepmind/open_spiel/blob/master/open_spiel/games/havannah.h#L67
https://github.com/deepmind/open_spiel/blob/master/open_spiel/games/quoridor.h#L54
though they might be overcomplicated for this use case. Feel free to push back on this if you don't think it's a good idea.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Keeping this as is improves readability IMO

const int start_column = values[1];
const int direction = values[2];
const int move_type = values[3];
const int captured_piece_type = values[4];
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, I see, this is why the piece types are in there. Is it better to put more details in the history and undo based on that instead of including it in the action?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great suggestion. Have introduced a TurnHistoryInfo struct to do this. Now actions have reduced to 1/4th.

void BasicSerializationTest() {
std::shared_ptr<const Game> game = LoadGame("checkers");
std::unique_ptr<State> state = game->NewInitialState();
std::unique_ptr<State> state2 = game->DeserializeState(state->Serialize());
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems worth testing a more complicated state.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added one more test with a board after a few moves

}

void BasicCheckersTests() {
testing::LoadGameTest("checkers");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you test different sizes as well?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added it

absl::StrAppend(&result, RowLabel(rows_, r));

for (int c = 0; c < columns_; c++) {
absl::StrAppend(&result, StateToString(BoardAt(r, c)));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it worth distinguishing between empty black/white squares, as in one that pieces can be on vs ones that are inaccessible? Maybe with dot vs space?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't understand this correctly. Can you elaborate?

SetBoard(end_row, end_column, CellState::kEmpty);
CellState captured_piece = OpponentState(PlayerToState(player));
SetBoard((start_row + end_row) / 2, (start_column + end_column) / 2,
captured_piece_type == PieceType::kMan ? captured_piece : CrownState(captured_piece));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This wrapping is a bit odd.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Have introduced a function here to improve readability

@lanctot lanctot added imported This PR has been imported and awaiting internal review. Please avoid any more local changes, thanks! merged internally The code is now submitted to our internal repo and will be merged in the next github sync. labels Jul 20, 2022
OpenSpiel pushed a commit that referenced this pull request Jul 25, 2022
PiperOrigin-RevId: 462677216
Change-Id: I2fc4b0b25d1265d7ff12df6c5b50a6defebbd496
@lanctot
Copy link
Collaborator

lanctot commented Jul 25, 2022

Merged in 808e54e

@lanctot lanctot closed this Jul 25, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
imported This PR has been imported and awaiting internal review. Please avoid any more local changes, thanks! merged internally The code is now submitted to our internal repo and will be merged in the next github sync.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants