Battleship Simple Version
This Compact contract implements a simple version of the game Battleship. Ships are represented by a number and the game board is represented as a single number line. This contract offers demonstration of the following features:
- Explicit state management
- Verification of private state data
- Access control to circuits
- Operations on a
List - Intermediate witness functionality
pragma language_version 0.22;
import CompactStandardLibrary;
export enum BoardState { UNSET, SET }
export enum ShotState { MISS, HIT }
export enum TurnState {
PLAYER_1_SHOOT,
PLAYER_1_CHECK,
PLAYER_2_SHOOT,
PLAYER_2_CHECK,
}
export enum WinState {
CONTINUE_PLAY,
PLAYER_1_WINS,
PLAYER_2_WINS
}
export ledger player1: ZswapCoinPublicKey;
export ledger player2: ZswapCoinPublicKey;
export ledger turn: TurnState;
export ledger board1: Set<Bytes<32>>;// linear board shape
export ledger board2: Set<Bytes<32>>;// hashed storage of ship locations
export ledger board1State: BoardState;
export ledger board2State: BoardState;
export ledger player1Shot: List<Uint<8>>;// current shot
export ledger player2Shot: List<Uint<8>>;
export ledger board1Hits: Set<Uint<8>>;// previous hits stored for later assertions
export ledger board2Hits: Set<Uint<8>>;
export ledger winState: WinState;
export ledger board1HitCount: Counter;
export ledger board2HitCount: Counter;
witness localSetBoard(_x1: Uint<8>, _x2: Uint<8>) : BoardState;
witness localCheckBoard(x: Uint<8>) : ShotState;
witness localSecretKey(): Bytes<32>;
constructor(_x1: Uint<8>, _x2: Uint<8>) {
assert(_x1 != _x2, "Cannot use the same number twice");
assert(_x1 > 0 && _x2 > 0, "No zero index, board starts at 1");
assert(_x1 <= 20 && _x2 <= 20, "Out of bounds, please keep ships on the board");
player1 = ownPublicKey();
const _sk = localSecretKey();
// hash the inputs to verify them later, user needs to provide the same value and _sk
const hash1 = commitBoardSpace(_x1 as Bytes<32>, _sk);
board1.insert(hash1);
const hash2 = commitBoardSpace(_x2 as Bytes<32>, _sk);
board1.insert(hash2);
// best practice example -- don't disclose(localSetBoard1(_x1, _x2)),
// disclose only what you need (localBoardState);
const localBoardState = localSetBoard(_x1, _x2);
assert(localBoardState == BoardState.SET, "Please update the state of board1 to SET");
board1State = disclose(localBoardState);
board2State = BoardState.UNSET;
winState = WinState.CONTINUE_PLAY;
}
export circuit acceptGame(_x1: Uint<8>, _x2: Uint<8>) : [] {
assert(player1 != ownPublicKey(), "You cannot play against yourself");
assert(board2State == BoardState.UNSET, "There is already a player2");
assert(_x1 != _x2, "Cannot use the same number twice");
assert(_x1 > 0 && _x2 > 0, "No zero index, please keep ships on the board");
assert(_x1 <= 20 && _x2 <= 20, "Out of bounds, please keep ships on the board");
player2 = ownPublicKey();
const _sk = localSecretKey();
// hash inputs and store them to the ledger for lookup later
const hash1 = commitBoardSpace(_x1 as Bytes<32>, _sk);
board2.insert(hash1);
const hash2 = commitBoardSpace(_x2 as Bytes<32>, _sk);
board2.insert(hash2);
const localBoardState = localSetBoard(_x1, _x2);
assert(localBoardState == BoardState.SET, "Please update the state of your board to SET");
board2State = disclose(localBoardState);
turn = TurnState.PLAYER_1_SHOOT;
}
export circuit player1Shoot (x: Uint<8>): [] {
assert(player1 == ownPublicKey(), "You are not player1");
assert(board2State == BoardState.SET, "Player 2 has not yet set their board");
assert(turn == TurnState.PLAYER_1_SHOOT, "It is not player1 turn to shoot");
assert(winState == WinState.CONTINUE_PLAY, "A winner has already been declared");
assert(x > 0 && x <= 20, "Shot out of bounds, please shoot on the board");
// shots are public knowledge
const currentShot = disclose(x);
assert(!board2Hits.member(currentShot), "Cheat Detected: Player1: Attempt to repeat a previous HIT");
player1Shot.pushFront(currentShot);
turn = TurnState.PLAYER_2_CHECK;
}
export circuit player2Shoot(x: Uint<8>): [] {
assert(player2 == ownPublicKey(), "You are not player2");
assert(turn == TurnState.PLAYER_2_SHOOT, "It is not player2 turn to shoot");
assert(winState == WinState.CONTINUE_PLAY, "A winner has already been declared");
assert(x > 0 && x <= 20, "Shot out of bounds, please shoot on the board");
// shots are public knowledge
const currentShot = disclose(x);
assert(!board1Hits.member(currentShot), "Cheat Detected: Player2: Attempt to repeat a previous HIT");
player2Shot.pushFront(currentShot);
turn = TurnState.PLAYER_1_CHECK;
}
export circuit checkBoard1 () : [] {
assert(player1 == ownPublicKey(), "You are not player1");
assert(winState == WinState.CONTINUE_PLAY, "A winner has already been declared");
assert(turn == TurnState.PLAYER_1_CHECK, "It is not Player 1 turn to CHECK");
assert(!player2Shot.isEmpty(), "No shot to check");
const _sk = localSecretKey();
const currentShot = player2Shot.head().value;
assert(!board1Hits.member(currentShot), "Cheat Detected: Player2: Attempt to repeat a previous HIT");
player2Shot.popFront();
const honestyCheckHash = commitBoardSpace(currentShot as Bytes<32>, _sk);
// currentShot has already been exposed, but we need to satisfy the compiler here too
const shotState = disclose(localCheckBoard(currentShot));
assert(shotState == ShotState.HIT || shotState == ShotState.MISS, "Please provide a valid state");
if(shotState == ShotState.MISS){
// don't trust, verify
assert(!board1.member(honestyCheckHash), "Cheat Detected: Player 1: claimed a MISS, when it was in fact a HIT");
turn = TurnState.PLAYER_1_SHOOT;
} else {
// don't trust, verify
assert(board1.member(honestyCheckHash), "Cheat Detected: Player 2: claimed a HIT, when is was in fact a MISS. Why would they do that?");
board1HitCount.increment(1);
board1Hits.insert(currentShot);
turn = TurnState.PLAYER_1_SHOOT;
// did someone win?
winState = board1HitCount == 2 ? WinState.PLAYER_2_WINS : WinState.CONTINUE_PLAY;
}
}
export circuit checkBoard2 () : [] {
assert(player2 == ownPublicKey(), "You are not player2");
assert(board2State == BoardState.SET, "Player 2 has not set the board yet");
assert(winState == WinState.CONTINUE_PLAY, "A winner has already been declared");
assert(turn == TurnState.PLAYER_2_CHECK, "It is not Player 2 turn to CHECK");
assert(!player1Shot.isEmpty(), "No shot to check");
const _sk = localSecretKey();
const currentShot = player1Shot.head().value;
assert(!board2Hits.member(currentShot), "Cheat Detected: Player 1: Attempt to repeat a previous HIT");
player1Shot.popFront();
const honestyCheckHash = commitBoardSpace(currentShot as Bytes<32>, _sk);
const shotState = disclose(localCheckBoard(currentShot));
assert(shotState == ShotState.HIT || shotState == ShotState.MISS, "Please provide a valid state");
if(shotState == ShotState.MISS){
// don't trust, verify
assert(!board2.member(honestyCheckHash), "Cheat Detected: Player 2: claimed a MISS, when it was in fact a HIT");
turn = TurnState.PLAYER_2_SHOOT;
} else {
// dont trust, verify
assert(board2.member(honestyCheckHash), "Cheat Detected: Player 2: claimed a HIT, when is was in fact a MISS. Why would they do that?");
board2HitCount.increment(1);
board2Hits.insert(currentShot);
turn = TurnState.PLAYER_2_SHOOT;
// did someone win?
winState = board2HitCount == 2 ? WinState.PLAYER_1_WINS : WinState.CONTINUE_PLAY;
}
}
circuit commitBoardSpace(_x: Bytes<32>, _sk: Bytes<32>) : Bytes<32> {
return disclose(persistentHash<Vector<2, Bytes<32>>>([_x, _sk]));
}