Skip to content

Commit

Permalink
#8 Arbiter.sol: finishGame
Browse files Browse the repository at this point in the history
  • Loading branch information
utgarda committed Sep 16, 2022
1 parent f815bd3 commit c3a0eee
Show file tree
Hide file tree
Showing 8 changed files with 291 additions and 20 deletions.
65 changes: 60 additions & 5 deletions contracts/Arbiter.sol
Original file line number Diff line number Diff line change
Expand Up @@ -83,8 +83,30 @@ contract Arbiter is IGameJutsuArbiter {
emit GamesStarted(gameId, games[gameId].stake, games[gameId].playersArray);
}

function finishGame(SignedGameMove calldata signedMove) external {
//TODO
function finishGame(SignedGameMove[] calldata signedMoves) external returns (address winner){
require(_isSignedByAllPlayers(signedMoves[0]), "Arbiter: first move not signed by all players");
address signer = recoverAddress(signedMoves[1].gameMove, signedMoves[1].signatures[0]);
require(signer == signedMoves[1].gameMove.player, "Arbiter: first signature must belong to the player making the move");

uint256 gameId = signedMoves[0].gameMove.gameId;
require(_isGameOn(gameId), "Arbiter: game not active");
require(signedMoves[1].gameMove.gameId == gameId, "Arbiter: game ids mismatch");
require(_isValidGameMove(signedMoves[1].gameMove), "Arbiter: invalid game move");
require(keccak256(signedMoves[0].gameMove.newState) == keccak256(signedMoves[1].gameMove.oldState), "Arbiter: game state mismatch");

IGameJutsuRules.GameState memory newState = IGameJutsuRules.GameState(gameId, signedMoves[1].gameMove.nonce + 1, signedMoves[1].gameMove.newState);
IGameJutsuRules rules = games[gameId].rules;
require(rules.isFinal(newState), "Arbiter: game state not final");
for (uint8 i = 0; i < NUM_PLAYERS; i++) {
if (rules.isWin(newState, i)) {
address winner = games[gameId].playersArray[i];
address loser = games[gameId].playersArray[1 - i];
_finishGame(gameId, winner, loser, false);
return winner;
}
}
_finishGame(gameId, address(0), address(0), true);
return address(0);
}

//TODO add dispute move version based on comparison to previously signed moves
Expand Down Expand Up @@ -220,12 +242,45 @@ contract Arbiter is IGameJutsuArbiter {
return a.gameId == b.gameId && a.nonce == b.nonce && keccak256(a.state) == keccak256(b.state);
}

function getSigners(SignedGameMove calldata signedMove) private pure returns (address[] memory) {//TODO lib
function publicGetSigners(SignedGameMove calldata signedMove) external view returns (address[] memory) {
return getSigners(signedMove);
}

function getSigners(SignedGameMove calldata signedMove) private view returns (address[] memory) {//TODO lib
address[] memory signers = new address[](signedMove.signatures.length);
for (uint256 i = 0; i < signedMove.signatures.length; i++) {
bytes32 signedHash = ECDSA.toEthSignedMessageHash(abi.encode(signedMove.gameMove.oldState, signedMove.gameMove.newState, signedMove.gameMove.move));
signers[i] = ECDSA.recover(signedHash, signedMove.signatures[i]);
signers[i] = recoverAddress(signedMove.gameMove, signedMove.signatures[i]);
}
return signers;
}

function _isGameOn(uint256 gameId) private view returns (bool) {
return games[gameId].started && !games[gameId].finished;
}

function _isSignedByAllPlayers(SignedGameMove calldata signedMove) private view returns (bool) {
address[] memory signers = getSigners(signedMove);
if (signers.length != NUM_PLAYERS) {
return false;
}
for (uint256 i = 0; i < signers.length; i++) {
if (games[signedMove.gameMove.gameId].players[signers[i]] == 0) {
return false;
}
}
return true;
}

function _finishGame(uint256 gameId, address winner, address loser, bool draw) private {
games[gameId].finished = true;
if (draw) {
uint256 half = games[gameId].stake / 2;
uint256 theOtherHalf = games[gameId].stake - half;
payable(games[gameId].playersArray[0]).transfer(half);
payable(games[gameId].playersArray[1]).transfer(theOtherHalf);
} else {
payable(winner).transfer(games[gameId].stake);
}
emit GameFinished(gameId, winner, loser, draw);
}
}
58 changes: 53 additions & 5 deletions contracts/tic-tac-toe/TicTacToeRules.sol
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,12 @@ import "../../interfaces/IGameJutsuRules.sol";
@author Gene A. Tsvigun
@dev The state encodes the board as a 3x3 array of uint8s with 0 for empty, 1 for X, and 2 for O
@dev explicitly keeping wins as `bool crossesWin` and `bool noughtsWin`
@dev yes we know the board can be packed more efficiently but we want to keep it simple
*/
contract TicTacToeRules is IGameJutsuRules {

struct Board {
uint8[9] cells; //TODO pack it into a single uint16
uint8[9] cells;
bool crossesWin;
bool naughtsWin;
}
Expand All @@ -36,26 +37,73 @@ type Move is uint8;
uint8 _m = abi.decode(_move, (uint8));
Move m = Move.wrap(_m);
bool playerIdMatchesTurn = _gameState.nonce % 2 == playerId;
return playerIdMatchesTurn && !b.crossesWin && !b.naughtsWin && isMoveWithinRange(m) && isCellEmpty(b, m);
return playerIdMatchesTurn && !b.crossesWin && !b.naughtsWin && _isMoveWithinRange(m) && _isCellEmpty(b, m);
}

function transition(GameState calldata _gameState, uint8 playerId, bytes calldata _move) external pure override returns (GameState memory) {
Board memory b = abi.decode(_gameState.state, (Board));
uint8 _m = abi.decode(_move, (uint8));
b.cells[_m] = uint8(1 + _gameState.nonce % 2);
Move move = Move.wrap(_m);
if (_isWinningMove(b, move)) {
if (playerId == 0) {
b.crossesWin = true;
} else {
b.naughtsWin = true;
}
}
return GameState(_gameState.gameId, _gameState.nonce + 1, abi.encode(b));
}

function defaultInitialGameState() external pure returns (bytes memory) {
return abi.encode(Board([0, 0, 0, 0, 0, 0, 0, 0, 0], false, false));
}

function isCellEmpty(Board memory b, Move move) private pure returns (bool) {
return b.cells[Move.unwrap(move)] == 0;
function isFinal(GameState calldata state) external pure returns (bool){
Board memory b = abi.decode(state.state, (Board));
return b.crossesWin || b.naughtsWin || _isBoardFull(b);
}

function isWin(GameState calldata state, uint8 playerId) external pure returns (bool){
Board memory b = abi.decode(state.state, (Board));
return playerId == 0 ? b.crossesWin : b.naughtsWin;
}

function _isCellEmpty(Board memory b, Move move) private pure returns (bool) {
return b.cells[Move.unwrap(move)] == 0;
}

function isMoveWithinRange(Move move) private pure returns (bool){
function _isMoveWithinRange(Move move) private pure returns (bool){
return Move.unwrap(move) < 9;
}

function _isBoardFull(Board memory b) private pure returns (bool) {
for (uint8 i = 0; i < 9; i++) {
if (b.cells[i] == 0) {
return false;
}
}
return true;
}

function _isWinningMove(Board memory b, Move move) private pure returns (bool) {
uint8 _m = Move.unwrap(move);
uint8 row = _m / 3;
uint8 col = _m % 3;
uint8 player = b.cells[_m];
return _isRowWin(b, row, player) || _isColWin(b, col, player) || _isDiagonalWin(b, player);
}

function _isRowWin(Board memory b, uint8 row, uint8 player) private pure returns (bool) {
return b.cells[row * 3] == player && b.cells[row * 3 + 1] == player && b.cells[row * 3 + 2] == player;
}

function _isColWin(Board memory b, uint8 col, uint8 player) private pure returns (bool) {
return b.cells[col] == player && b.cells[col + 3] == player && b.cells[col + 6] == player;
}

function _isDiagonalWin(Board memory b, uint8 player) private pure returns (bool) {
return (b.cells[0] == player && b.cells[4] == player && b.cells[8] == player) ||
(b.cells[2] == player && b.cells[4] == player && b.cells[6] == player);
}
}
2 changes: 1 addition & 1 deletion interfaces/IGameJutsuArbiter.sol
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ interface IGameJutsuArbiter {

function disputeMoveWithHistory(SignedGameMove[2] calldata signedMoves) external;

function finishGame(SignedGameMove calldata signedMove) external;
function finishGame(SignedGameMove[] calldata signedMoves) external returns (address winner);
//
// function initMoveTimeout(SignedMove calldata signedMove) payable external;
//
Expand Down
4 changes: 4 additions & 0 deletions interfaces/IGameJutsuRules.sol
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,8 @@ interface IGameJutsuRules {
function transition(GameState calldata state, uint8 playerId, bytes calldata move) external pure returns (GameState memory);

function defaultInitialGameState() external pure returns (bytes memory);

function isFinal(GameState calldata state) external pure returns (bool);

function isWin(GameState calldata state, uint8 playerId) external pure returns (bool);
}
11 changes: 11 additions & 0 deletions scripts/deploy.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,14 @@
# ________ ____. __
# / _____/_____ _____ ____ | |__ ___/ |_ ________ __
# / \ ___\__ \ / \_/ __ \ | | | \ __\/ ___/ | \
# \ \_\ \/ __ \| Y Y \ ___//\__| | | /| | \___ \| | /
# \______ (____ /__|_| /\___ >________|____/ |__| /____ >____/
# \/ \/ \/ \/ \/
# https://gamejutsu.app
# ETHOnline2022 submission by ChainHackers
__author__ = ["Gene A. Tsvigun"]
__license__ = "MIT"

from brownie import Arbiter, TicTacToeRules, accounts


Expand Down
116 changes: 115 additions & 1 deletion tests/arbiter_test.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,17 @@
# ________ ____. __
# / _____/_____ _____ ____ | |__ ___/ |_ ________ __
# / \ ___\__ \ / \_/ __ \ | | | \ __\/ ___/ | \
# \ \_\ \/ __ \| Y Y \ ___//\__| | | /| | \___ \| | /
# \______ (____ /__|_| /\___ >________|____/ |__| /____ >____/
# \/ \/ \/ \/ \/
# https://gamejutsu.app
# ETHOnline2022 submission by ChainHackers
__authors__ = ["Gene A. Tsvigun", "Vic G. Larson"]
__license__ = "MIT"

import pytest
from brownie import reverts, interface, Wei
from eth_abi import encode_abi, decode_abi
from eth_abi import encode_abi
from eth_account.messages import SignableMessage
from brownie.convert import to_bytes

Expand Down Expand Up @@ -419,3 +430,106 @@ def test_is_valid_signed_players_moves_in_right_sequence(arbiter, rules, start_g

with reverts():
arbiter.disputeMove(valid_signed_game_move2, {'from': player_a.address})


def test_finish_game(arbiter, rules, start_game, player_a, player_b):
stake = Wei('0.1 ether')
tx = arbiter.proposeGame(rules, {'value': stake, 'from': player_a.address})
game_id = tx.return_value
assert 'GameProposed' in tx.events
assert tx.events['GameProposed']['gameId'] == game_id

o_about_to_play_in_the_center_board = encode_abi(STATE_TYPES, [[1, 1, 0, 2, 0, 0, 0, 0, 0], False, False])
o_center_move_data = to_bytes("0x04")

# ╭───┬───┬───╮
# │ X │ X │ . │
# ├───┼───┼───┤
# │ 0 │ 0 │ │
# ├───┼───┼───┤
# │ │ │ │
# ╰───┴───┴───╯

x_almost_won_board = encode_abi(STATE_TYPES, [[1, 1, 0, 2, 2, 0, 0, 0, 0], False, False])
nonce = 4
x_won_board = encode_abi(STATE_TYPES, [[1, 1, 1, 2, 2, 0, 0, 0, 0], True, False])
x_winning_move_data = to_bytes("0x02")
x_non_winning_move_data = to_bytes("0x08")
x_not_won_board = encode_abi(STATE_TYPES, [[1, 1, 0, 2, 2, 0, 0, 0, 1], False, False])

o_center_move = [
game_id,
3,
player_b.address,
o_about_to_play_in_the_center_board,
x_almost_won_board,
o_center_move_data
]
x_winning_move = [
game_id,
nonce,
player_a.address,
x_almost_won_board,
x_won_board,
x_winning_move_data
]
x_non_winning_move = [
game_id,
nonce,
player_a.address,
x_almost_won_board,
x_not_won_board,
x_non_winning_move_data
]

encoded_o_center_move = encode_move(*o_center_move)
signature_a = player_a.sign_message(encoded_o_center_move).signature
signature_b = player_b.sign_message(encoded_o_center_move).signature
signed_by_both_players_move = [
o_center_move,
[signature_a, signature_b]
]
print(f"player_a: {player_a.address}")
print(f"player_b: {player_b.address}")
print(arbiter.publicGetSigners(signed_by_both_players_move))

signed_x_winning_move = [
x_winning_move,
[player_a.sign_message(encode_move(*x_winning_move)).signature]
]
signed_x_non_winning_move = [
x_non_winning_move,
[player_a.sign_message(encode_move(*x_non_winning_move)).signature]
]

with reverts():
arbiter.finishGame(
[signed_by_both_players_move, signed_x_winning_move],
{'from': player_a.address}
)

arbiter.acceptGame(game_id, {'value': stake, 'from': player_b.address})
rules, stake, started, finished = arbiter.games(game_id)
assert started
assert not finished

with reverts():
arbiter.finishGame(
[signed_by_both_players_move, signed_x_non_winning_move],
{'from': player_a.address}
)

tx = arbiter.finishGame(
[signed_by_both_players_move, signed_x_winning_move],
{'from': player_a.address}
)
assert 'GameFinished' in tx.events
e = tx.events['GameFinished']
assert e['gameId'] == game_id
assert e['winner'] == player_a.address
assert e['loser'] == player_b.address
assert not e['isDraw']
assert 'PlayerDisqualified' not in tx.events

rules, stake, started, finished = arbiter.games(game_id)
assert finished
13 changes: 12 additions & 1 deletion tests/conftest.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,18 @@
# ________ ____. __
# / _____/_____ _____ ____ | |__ ___/ |_ ________ __
# / \ ___\__ \ / \_/ __ \ | | | \ __\/ ___/ | \
# \ \_\ \/ __ \| Y Y \ ___//\__| | | /| | \___ \| | /
# \______ (____ /__|_| /\___ >________|____/ |__| /____ >____/
# \/ \/ \/ \/ \/
# https://gamejutsu.app
# ETHOnline2022 submission by ChainHackers
__author__ = ["Gene A. Tsvigun"]
__license__ = "MIT"

import pytest

from eth_account import Account
from brownie.network.gas.strategies import GasNowScalingStrategy


@pytest.fixture(scope="module")
def dev(accounts):
Expand Down
Loading

0 comments on commit c3a0eee

Please sign in to comment.