├── program.json ├── imports ├── move.aleo ├── board.aleo └── verify.aleo ├── main.aleo └── README.md /program.json: -------------------------------------------------------------------------------- 1 | { 2 | "program": "battleship.aleo", 3 | "version": "0.0.0", 4 | "description": "Play ZK Battleship", 5 | "development": { 6 | "private_key": "APrivateKey1zkp86FNGdKxjgAdgQZ967bqBanjuHkAaoRe19RK24ZCGsHH", 7 | "view_key": "AViewKey1hh6dvSEgeMdfseP4hfdbNYjX4grETwCuTbKnCftkpMwE", 8 | "address": "aleo1wyvu96dvv0auq9e4qme54kjuhzglyfcf576h0g3nrrmrmr0505pqd6wnry" 9 | }, 10 | "license": "MIT" 11 | } -------------------------------------------------------------------------------- /imports/move.aleo: -------------------------------------------------------------------------------- 1 | program move.aleo; 2 | 3 | record move: 4 | owner as address.private; 5 | gates as u64.private; 6 | incoming_fire_coordinate as u64.private; 7 | player_1 as address.private; 8 | player_2 as address.private; 9 | prev_hit_or_miss as u64.private; 10 | 11 | // input r0 (move.record): the move record created by the opponent. 12 | // input r1 (u64): the u64 representation of incoming_fire_coordinate, the bitstring fire coordinate 13 | // to send to the opponent. 14 | // input r2 (u64): the u64 representation of prev_hit_or_miss, this player's previous fire coordinate as a hit or miss. 15 | // One flipped bit indicates a hit. No flipped bits indicates a miss. 16 | // returns new move record owned by the opponent. 17 | function create_move: 18 | input r0 as move.record; 19 | input r1 as u64.private; 20 | input r2 as u64.private; 21 | 22 | // a new move record should be created and owned by the opponent. 23 | is.eq r0.owner r0.player_1 into r3; 24 | ternary r3 r0.player_2 r0.player_1 into r4; 25 | 26 | cast r4 r0.gates r1 r0.player_2 r0.player_1 r2 into r5 as move.record; 27 | 28 | output r5 as move.record; 29 | 30 | // input r0 (address): the address of the second player 31 | // returns move record owned by the opponent. Note, this move record contains 32 | // dummy fire coordinates and previous hit or miss. 33 | function start_game: 34 | // self.caller will be player 1 35 | input r0 as address.private; // player 2 36 | 37 | cast r0 0u64 0u64 self.caller r0 0u64 into r1 as move.record; 38 | 39 | output r1 as move.record; -------------------------------------------------------------------------------- /imports/board.aleo: -------------------------------------------------------------------------------- 1 | program board.aleo; 2 | 3 | // Battleship boards are represented by 8x8 squares. 4 | // A u64 is all that is required to represent a hit or a miss on a single board. 5 | // Starting from the top row, left to right, a hit is 1 and a miss is 0. 6 | // A first move resulting in a hit in row 1, column 3 would be: 7 | // 00100000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 8 | // A second u64 is needed to represent which squares have been played, with 1s being played squares and 0s being 9 | // unplayed squares. 10 | record board_state: 11 | owner as address.private; 12 | gates as u64.private; 13 | // the hits and misses registered on the opponent's board 14 | hits_and_misses as u64.private; 15 | // the squares that have been played on the opponent's board 16 | played_tiles as u64.private; 17 | ships as u64.private; // the ship bitstring representing all ship positions on your own board 18 | player_1 as address.private; 19 | player_2 as address.private; 20 | game_started as boolean.private; 21 | 22 | // input r0 (u64): u64 equivalent of the bitstring representation of a game board, 8x8. 23 | // input r1 (address): the address of the opponent. 24 | function initialize_board: 25 | input r0 as u64.private; 26 | input r1 as address.private; 27 | 28 | cast self.caller 0u64 0u64 0u64 r0 self.caller r1 false into r2 as board_state.record; 29 | 30 | output r2 as board_state.record; 31 | 32 | // input r0 (board_state.record): the record of the board to start. A board can only be started once. 33 | // returns a new board state record that has been started. Will fail if this board has been started before. 34 | function start_board: 35 | input r0 as board_state.record; 36 | 37 | // ensure this board hasn't been used to start a game before 38 | assert.eq false r0.game_started; 39 | 40 | cast r0.owner r0.gates r0.hits_and_misses r0.played_tiles r0.ships r0.player_1 r0.player_2 true into r1 as board_state.record; 41 | 42 | output r1 as board_state.record; 43 | 44 | // input r0 (board_state.record): the record of the board to update 45 | // input r1 (u64): the u64 equivalent of a bitstring fire coordinate to send to the opponent. 46 | // returns a new board state record that includes all the played tiles. Fails if r1 has been played before. 47 | function update_played_tiles: 48 | input r0 as board_state.record; 49 | input r1 as u64.private; // your next move 50 | 51 | // need to make sure r1 is a valid move. Only one bit of r1 should be flipped. 52 | sub r1 1u64 into r2; 53 | // bitwise and operation 54 | and r1 r2 into r3; 55 | assert.eq 0u64 r3; 56 | 57 | // need to make sure r1 is a valid move given the played_tiles. no bits should overlap. 58 | and r0.played_tiles r1 into r4; 59 | assert.eq 0u64 r4; 60 | 61 | or r0.played_tiles r1 into r5; // new played tiles 62 | 63 | cast r0.owner r0.gates r0.hits_and_misses r5 r0.ships r0.player_1 r0.player_2 r0.game_started into r6 as board_state.record; 64 | 65 | output r6 as board_state.record; 66 | 67 | // input r0 (board_state.record): the record of the board to update 68 | // input r1 (u64): the u64 equivalent of a bitstring of whether this player's previous move was a hit or miss. 69 | // returns a new board state record that includes all the hits and misses. 70 | function update_hits_and_misses: 71 | input r0 as board_state.record; 72 | input r1 as u64.private; 73 | 74 | or r0.hits_and_misses r1 into r2; // updated hits and misses 75 | 76 | cast r0.owner r0.gates r2 r0.played_tiles r0.ships r0.player_1 r0.player_2 r0.game_started into r3 as board_state.record; 77 | 78 | output r3 as board_state.record; 79 | -------------------------------------------------------------------------------- /main.aleo: -------------------------------------------------------------------------------- 1 | import board.aleo; 2 | import move.aleo; 3 | import verify.aleo; 4 | 5 | // The 'battleship.aleo' program. 6 | program battleship.aleo; 7 | 8 | // input r0 (u64): the u64 representation of a carrier's placement in an 8x8 grid. Length = 5. 9 | // input r1 (u64): the u64 representation of a battleship's placement in an 8x8 grid. Length = 4. 10 | // input r2 (u64): the u64 representation of a cruiser's placement in an 8x8 grid. Length = 3. 11 | // input r3 (u64): the u64 representation of a destroyer's placement in an 8x8 grid. Length = 2. 12 | // input r4 (address): the address of the opponent. 13 | function initialize_board: 14 | input r0 as u64.private; // Carrier, length 5 15 | input r1 as u64.private; // Battleship, length 4 16 | input r2 as u64.private; // Cruiser, length 3 17 | input r3 as u64.private; // Destroyer, length 2 18 | input r4 as address.private; 19 | 20 | // verify that each individual ship placement bitstring is valid 21 | call verify.aleo/validate_ship_placement r0 5u64 31u64 4311810305u64 into r5; 22 | assert.eq r5 true; 23 | call verify.aleo/validate_ship_placement r1 4u64 15u64 16843009u64 into r6; 24 | assert.eq r6 true; 25 | call verify.aleo/validate_ship_placement r2 3u64 7u64 65793u64 into r7; 26 | assert.eq r7 true; 27 | call verify.aleo/validate_ship_placement r3 2u64 3u64 257u64 into r8; 28 | assert.eq r8 true; 29 | 30 | // create the board with all the ship placements combined 31 | call verify.aleo/create_board r0 r1 r2 r3 into r9; 32 | 33 | // initialize the board state record 34 | call board.aleo/initialize_board r9 r4 into r10; 35 | 36 | output r10 as board.aleo/board_state.record; 37 | 38 | // input r0 (board_state.record): the board record to start a game with. 39 | // returns an updated board state record that has been started. This board cannot be used to start any other games. 40 | // returns a dummy move record owned by the opponent. 41 | // This function commits a given board to a game with an opponent and creates the initial dummy move. 42 | function offer_battleship: 43 | input r0 as board.aleo/board_state.record; 44 | 45 | call board.aleo/start_board r0 into r1; 46 | call move.aleo/start_game r0.player_2 into r2; 47 | 48 | output r1 as board.aleo/board_state.record; 49 | output r2 as move.aleo/move.record; 50 | 51 | // input r0 (board_state.record): the board record to play the game with. 52 | // input r1 (move.record): move record to play to begin the game. This should be the dummy move record created 53 | // from offer_battleship. 54 | // returns updated board_state.record that has been started and can no longer be used to join or start new games. 55 | // returns dummy move record owned by the opponent. 56 | function start_battleship: 57 | input r0 as board.aleo/board_state.record; 58 | input r1 as move.aleo/move.record; 59 | 60 | // validate that the move players and board players match each other. 61 | assert.eq r0.player_1 r1.player_2; 62 | assert.eq r0.player_2 r1.player_1; 63 | 64 | call board.aleo/start_board r0 into r2; 65 | call move.aleo/start_game r0.player_2 into r3; 66 | 67 | output r2 as board.aleo/board_state.record; 68 | output r3 as move.aleo/move.record; 69 | 70 | // input r0 (board_state.record): the board record to update 71 | // input r1 (move.record): the incoming move from the opponent 72 | // input r2 (u64): the u64 equivalent of the bitwise representation of the next coordinate to play on 73 | // the opponent's board. 74 | // returns updated board record. 75 | // returns new move record owned by opponent. 76 | function play: 77 | input r0 as board.aleo/board_state.record; 78 | input r1 as move.aleo/move.record; 79 | input r2 as u64.private; 80 | 81 | // verify the board has been started. This prevents players from starting a game and then creating 82 | // a brand new board to play with. 83 | assert.eq r0.game_started true; 84 | 85 | // validate that the move players and board players match each other. 86 | assert.eq r0.player_1 r1.player_2; 87 | assert.eq r0.player_2 r1.player_1; 88 | 89 | // play coordinate on own board. will fail if not a valid move 90 | call board.aleo/update_played_tiles r0 r2 into r3; 91 | 92 | // update own board with result of last shot 93 | call board.aleo/update_hits_and_misses r3 r1.prev_hit_or_miss into r4; 94 | 95 | // assess whether incoming move firing coordinate is a hit 96 | and r1.incoming_fire_coordinate r0.ships into r5; 97 | 98 | call move.aleo/create_move r1 r2 r5 into r6; 99 | 100 | output r4 as board.aleo/board_state.record; 101 | output r6 as move.aleo/move.record; -------------------------------------------------------------------------------- /imports/verify.aleo: -------------------------------------------------------------------------------- 1 | program verify.aleo; 2 | 3 | // We're using the c_ prefix to denote the special closure function type. 4 | // input r0 (u64): an input that corresponds to a u64 bitstring 5 | // returns the number of "flipped" bits. 6 | // E.g. 17870283321406128128u64, in binary 11111000 00000000 00000000 00000000 00000000 00000000 00000000 00000000, 7 | // returns 5u64; 8 | closure c_bitcount: 9 | input r0 as u64; 10 | 11 | div r0 2u64 into r1; 12 | div r0 4u64 into r2; 13 | div r0 8u64 into r3; 14 | 15 | and r1 8608480567731124087u64 into r4; 16 | and r2 3689348814741910323u64 into r5; 17 | and r3 1229782938247303441u64 into r6; 18 | 19 | sub r0 r4 into r7; 20 | sub r7 r5 into r8; 21 | sub r8 r6 into r9; 22 | 23 | div r9 16u64 into r10; 24 | add r9 r10 into r11; 25 | and r11 1085102592571150095u64 into r12; 26 | rem r12 255u64 into r13; 27 | 28 | output r13 as u64; 29 | 30 | // input r0 (u64): the u64 representation of a ship's placement in an 8x8 grid. 31 | // input r1 (u64): the u64 representation of a ship's bitstring, either horizontally or vertically. 32 | // E.g. a ship of length 3's bit string horizontally would be: 000111 = 7u64. Vertically, the bit string would be: 33 | // 10000000100000001 = 65793u64. 34 | // returns boolean of whether all the flipped bits in r0 are "adjacent". Horizontally, this means all flipped bits are 35 | // directly next to each other (111). Vertically, this means all flipped bits are separated by 7 unflipped bits 36 | // (10000000100000001). 37 | closure c_adjacency_check: 38 | input r0 as u64; 39 | input r1 as u64; 40 | 41 | // this may result in 0 42 | div r0 r1 into r2; 43 | // subtracting 1 from 0 will cause an underflow, so we should check for this edge case. 44 | is.eq r2 0u64 into r3; 45 | // if the above division resulted in 0, we know the adjacency check should return false. 46 | // Setting to r4 to 3 (11) will guarantee failure here. 47 | ternary r3 3u64 r2 into r4; 48 | 49 | sub r4 1u64 into r5; 50 | and r4 r5 into r6; 51 | 52 | is.eq r6 0u64 into r7; 53 | 54 | output r7 as boolean; 55 | 56 | // input r0 (u64): the u64 representation of a ship's placement in an 8x8 grid. 57 | // input r1 (u64): the u64 representation of a ship's bitstring horizontally 58 | // returns boolean of whether adjacent flipped bits don't split a row of size 8. 59 | // E.g. 111000000 has adjacent flipped bits but splits a row: 00000001 11000000 60 | closure c_horizontal_check: 61 | input r0 as u64; 62 | input r1 as u64; 63 | 64 | rem r0 255u64 into r2; 65 | // this may result in 0 66 | div r2 r1 into r3; 67 | 68 | // subtracting 1 from 0 will cause an underflow 69 | is.eq r3 0u64 into r4; 70 | // setting to 3 will guarantee failure 71 | ternary r4 3u64 r3 into r5; 72 | sub r5 1u64 into r6; 73 | and r5 r6 into r7; 74 | 75 | is.eq r7 0u64 into r8; 76 | 77 | output r8 as boolean; 78 | 79 | // input r0 (u64): the u64 representation of a ship's placement in an 8x8 grid. 80 | // input r1 (u64): the length of the placed ship 81 | // input r2 (u64): the u64 equivalent of a ship's horizontal bitstring representation 82 | // input r3 (u64): the u64 equivalent of a ship's vertical bitstring representation 83 | // returns boolean whether the ship placement is valid or not 84 | function validate_ship_placement: 85 | input r0 as u64.private; // Ship placement on board 86 | input r1 as u64.private; // Ship length 87 | input r2 as u64.private; // Horizontal ship bitstring 88 | input r3 as u64.private; // Vertical ship bitstring 89 | 90 | // check bitcount -- all other validations depend on the bitcount being correct 91 | call c_bitcount r0 into r4; // how many bits in the ship placement 92 | assert.eq r4 r1; 93 | 94 | // if horizontal: 95 | call c_adjacency_check r0 r2 into r5; // true if bits are adjacent horizontally 96 | call c_horizontal_check r0 r2 into r6; // true if those horizontal bits are not split across rows 97 | and r5 r6 into r7; // true if bits are adjacent horizontally and not split across rows 98 | 99 | // if vertical: 100 | call c_adjacency_check r0 r3 into r8; // true if bits are adjacent vertically 101 | 102 | or r7 r8 into r9; // ship is valid if it is vertically or horizontally valid 103 | 104 | output r9 as boolean.private; 105 | 106 | // input r0 (u64): the u64 representation of a carrier's placement in an 8x8 grid. Length = 5. 107 | // input r1 (u64): the u64 representation of a battleship's placement in an 8x8 grid. Length = 4. 108 | // input r2 (u64): the u64 representation of a cruiser's placement in an 8x8 grid. Length = 3. 109 | // input r3 (u64): the u64 representation of a destroyer's placement in an 8x8 grid. Length = 2. 110 | // returns the u64 representation of all the ships' placements in an 8x8 grid. This function will fail 111 | // if any of the ship placements overlap each other. 112 | function create_board: 113 | input r0 as u64.private; // Carrier, length 5 114 | input r1 as u64.private; // Battleship, length 4 115 | input r2 as u64.private; // Cruiser, length 3 116 | input r3 as u64.private; // Destroyer, length 2 117 | 118 | // bitwise combine the ship placements together 119 | or r0 r1 into r4; 120 | or r2 r3 into r5; 121 | or r4 r5 into r6; // full bitwise combination of ships 122 | 123 | call c_bitcount r6 into r7; 124 | assert.eq r7 14u64; // given 4 individually-valid ships, a valid combination should yield exactly 14 flipped bits. 125 | 126 | output r6 as u64.private; -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ZK Battleship 2 | 3 | ## QuickStart 4 | 5 | ### Build Guide 6 | 7 | To compile this Aleo program, run: 8 | ```bash 9 | aleo build 10 | ``` 11 | 12 | ### Usage Guide 13 |
Commands and Playing the Game 14 | 15 | In order to play battleship, there must be two players with two boards. Navigate to the zk-battleship aleo project. Then create two new aleo accounts: 16 | ```bash 17 | aleo account new 18 | >>> Private Key APrivateKey1zkpGKaJY47BXb6knSqmT3JZnBUEGBDFAWz2nMVSsjwYpJmm 19 | >>> View Key AViewKey1fSyEPXxfPFVgjL6qcM9izWRGrhSHKXyN3c64BNsAjnA6 20 | >>> Address aleo15g9c69urtdhvfml0vjl8px07txmxsy454urhgzk57szmcuttpqgq5cvcdy 21 | 22 | aleo account new 23 | >>> Private Key APrivateKey1zkp86FNGdKxjgAdgQZ967bqBanjuHkAaoRe19RK24ZCGsHH 24 | >>> View Key AViewKey1hh6dvSEgeMdfseP4hfdbNYjX4grETwCuTbKnCftkpMwE 25 | >>> Address aleo1wyvu96dvv0auq9e4qme54kjuhzglyfcf576h0g3nrrmrmr0505pqd6wnry 26 | ``` 27 | 28 | Save the keys and addresses. Set the `program.json` private_key and address to one of the newly created aleo accounts. We'll refer to this address as Player 1, and the remaining address as Player 2. 29 | 30 | ```json 31 | { 32 | "program": "battleship.aleo", 33 | "version": "0.0.0", 34 | "description": "Play ZK Battleship", 35 | "development": { 36 | "private_key": "APrivateKey1zkpGKaJY47BXb6knSqmT3JZnBUEGBDFAWz2nMVSsjwYpJmm", 37 | "view_key": "AViewKey1fSyEPXxfPFVgjL6qcM9izWRGrhSHKXyN3c64BNsAjnA6", 38 | "address": "aleo15g9c69urtdhvfml0vjl8px07txmxsy454urhgzk57szmcuttpqgq5cvcdy" 39 | }, 40 | "license": "MIT" 41 | } 42 | ``` 43 | 44 | Now, we need to make a board as Player 1. See the [modeling the boards and ships](https://github.com/demox-labs/zk-battleship/#modeling-the-board-and-ships) section for information on valid ship bitstrings and placements on the board. For this example, we will be using sample valid inputs. Initialize a new board as Player 1 with valid ship inputs and Player 2's address: `aleo run initialize_board ship_5_bitstring ship_4_bitstring ship_3_bitstring ship_2_bitstring player_2_address` 45 | ```bash 46 | aleo run initialize_board 34084860461056u64 551911718912u64 7u64 1157425104234217472u64 aleo1wyvu96dvv0auq9e4qme54kjuhzglyfcf576h0g3nrrmrmr0505pqd6wnry 47 | 48 | >>> ➡️ Output 49 | 50 | • { 51 | owner: aleo15g9c69urtdhvfml0vjl8px07txmxsy454urhgzk57szmcuttpqgq5cvcdy.private, 52 | gates: 0u64.private, 53 | hits_and_misses: 0u64.private, 54 | played_tiles: 0u64.private, 55 | ships: 1157459741006397447u64.private, 56 | player_1: aleo15g9c69urtdhvfml0vjl8px07txmxsy454urhgzk57szmcuttpqgq5cvcdy.private, 57 | player_2: aleo1wyvu96dvv0auq9e4qme54kjuhzglyfcf576h0g3nrrmrmr0505pqd6wnry.private, 58 | game_started: false.private, 59 | _nonce: 3887646704618532506963887075433683846689834495661101507703164090915348189037group.public 60 | } 61 | 62 | ✅ Executed 'battleship.aleo/initialize_board' 63 | ``` 64 | 65 | The output is a board_state record owned by Player 1. Notice that the `game_started` flag is false, as well as the composite ship configuration `ships`. 1157459741006397447u64 to a binary bitstring becomes `0001000000010000000111111000000010000000100000001000000000000111`, or laid out in columns and rows: 66 | ``` 67 | 0 0 0 1 0 0 0 0 68 | 0 0 0 1 0 0 0 0 69 | 0 0 0 1 1 1 1 1 70 | 1 0 0 0 0 0 0 0 71 | 1 0 0 0 0 0 0 0 72 | 1 0 0 0 0 0 0 0 73 | 1 0 0 0 0 0 0 0 74 | 0 0 0 0 0 1 1 1 75 | ``` 76 | 77 | Now, we can offer a battleship game to player 2. Run `aleo run offer_battleship 'board_state.record'` with the record you just created: 78 | ```bash 79 | aleo run offer_battleship '{ 80 | owner: aleo15g9c69urtdhvfml0vjl8px07txmxsy454urhgzk57szmcuttpqgq5cvcdy.private, 81 | gates: 0u64.private, 82 | hits_and_misses: 0u64.private, 83 | played_tiles: 0u64.private, 84 | ships: 1157459741006397447u64.private, 85 | player_1: aleo15g9c69urtdhvfml0vjl8px07txmxsy454urhgzk57szmcuttpqgq5cvcdy.private, 86 | player_2: aleo1wyvu96dvv0auq9e4qme54kjuhzglyfcf576h0g3nrrmrmr0505pqd6wnry.private, 87 | game_started: false.private, 88 | _nonce: 3887646704618532506963887075433683846689834495661101507703164090915348189037group.public 89 | }' 90 | 91 | >>> ➡️ Outputs 92 | 93 | • { 94 | owner: aleo15g9c69urtdhvfml0vjl8px07txmxsy454urhgzk57szmcuttpqgq5cvcdy.private, 95 | gates: 0u64.private, 96 | hits_and_misses: 0u64.private, 97 | played_tiles: 0u64.private, 98 | ships: 1157459741006397447u64.private, 99 | player_1: aleo15g9c69urtdhvfml0vjl8px07txmxsy454urhgzk57szmcuttpqgq5cvcdy.private, 100 | player_2: aleo1wyvu96dvv0auq9e4qme54kjuhzglyfcf576h0g3nrrmrmr0505pqd6wnry.private, 101 | game_started: true.private, 102 | _nonce: 6563064852163330630334088854834332804417910882908622526775624018226782316843group.public 103 | } 104 | • { 105 | owner: aleo1wyvu96dvv0auq9e4qme54kjuhzglyfcf576h0g3nrrmrmr0505pqd6wnry.private, 106 | gates: 0u64.private, 107 | incoming_fire_coordinate: 0u64.private, 108 | player_1: aleo15g9c69urtdhvfml0vjl8px07txmxsy454urhgzk57szmcuttpqgq5cvcdy.private, 109 | player_2: aleo1wyvu96dvv0auq9e4qme54kjuhzglyfcf576h0g3nrrmrmr0505pqd6wnry.private, 110 | prev_hit_or_miss: 0u64.private, 111 | _nonce: 4374626042494973146987320062571809401151262172766172816829659487584978644457group.public 112 | } 113 | 114 | ✅ Executed 'battleship.aleo/offer_battleship' 115 | ``` 116 | 117 | The first output record is the udpated board_state.record. Notice the `game_started` flag is now true. This board cannot be used to offer any other battleship games or accept any battleship game offers. Player 1 would need to initialize a new board and use that instead. The second output record is a dummy move.record -- there are no fire coordinates included to play on Player 2's board, and no information about any previous Player 2 moves (Player 2 has not made any moves yet). This move.record is owned by Player 2, who must use that in combination with their own board_state.record to accept the game. Let's do that now. 118 | 119 | We must run the program as Player 2 now, so switch the `program.json` file to use Player 2's keys: 120 | ```json 121 | { 122 | "program": "battleship.aleo", 123 | "version": "0.0.0", 124 | "description": "Play ZK Battleship", 125 | "development": { 126 | "private_key": "APrivateKey1zkp86FNGdKxjgAdgQZ967bqBanjuHkAaoRe19RK24ZCGsHH", 127 | "view_key": "AViewKey1hh6dvSEgeMdfseP4hfdbNYjX4grETwCuTbKnCftkpMwE", 128 | "address": "aleo1wyvu96dvv0auq9e4qme54kjuhzglyfcf576h0g3nrrmrmr0505pqd6wnry" 129 | }, 130 | "license": "MIT" 131 | } 132 | ``` 133 | 134 | We'll create a new and different board for Player 2, and make sure to include Player 1's address as the opponent: 135 | ```bash 136 | aleo run initialize_board 31u64 2207646875648u64 224u64 9042383626829824u64 aleo15g9c69urtdhvfml0vjl8px07txmxsy454urhgzk57szmcuttpqgq5cvcdy 137 | 138 | >>> ➡️ Output 139 | 140 | • { 141 | owner: aleo1wyvu96dvv0auq9e4qme54kjuhzglyfcf576h0g3nrrmrmr0505pqd6wnry.private, 142 | gates: 0u64.private, 143 | hits_and_misses: 0u64.private, 144 | played_tiles: 0u64.private, 145 | ships: 9044591273705727u64.private, 146 | player_1: aleo1wyvu96dvv0auq9e4qme54kjuhzglyfcf576h0g3nrrmrmr0505pqd6wnry.private, 147 | player_2: aleo15g9c69urtdhvfml0vjl8px07txmxsy454urhgzk57szmcuttpqgq5cvcdy.private, 148 | game_started: false.private, 149 | _nonce: 1549419609469324182591325047490602235361156298832591378925133482196483208807group.public 150 | } 151 | 152 | ✅ Executed 'battleship.aleo/initialize_board' 153 | ``` 154 | 155 | Note, the output ships here is 9044591273705727u64, which in a bitstring is: 156 | ``` 157 | 0 0 1 0 0 0 0 0 158 | 0 0 1 0 0 0 1 0 159 | 0 0 0 0 0 0 1 0 160 | 0 0 0 0 0 0 1 0 161 | 0 0 0 0 0 0 1 0 162 | 0 0 0 0 0 0 0 0 163 | 1 1 1 1 1 1 1 1 164 | ``` 165 | 166 | Now, we can accept Player 1's offer. Run `aleo run start_battleship 'board_state.record' 'move.record'`: 167 | ```bash 168 | aleo run start_battleship '{ 169 | owner: aleo1wyvu96dvv0auq9e4qme54kjuhzglyfcf576h0g3nrrmrmr0505pqd6wnry.private, 170 | gates: 0u64.private, 171 | hits_and_misses: 0u64.private, 172 | played_tiles: 0u64.private, 173 | ships: 9044591273705727u64.private, 174 | player_1: aleo1wyvu96dvv0auq9e4qme54kjuhzglyfcf576h0g3nrrmrmr0505pqd6wnry.private, 175 | player_2: aleo15g9c69urtdhvfml0vjl8px07txmxsy454urhgzk57szmcuttpqgq5cvcdy.private, 176 | game_started: false.private, 177 | _nonce: 1549419609469324182591325047490602235361156298832591378925133482196483208807group.public 178 | }' '{ 179 | owner: aleo1wyvu96dvv0auq9e4qme54kjuhzglyfcf576h0g3nrrmrmr0505pqd6wnry.private, 180 | gates: 0u64.private, 181 | incoming_fire_coordinate: 0u64.private, 182 | player_1: aleo15g9c69urtdhvfml0vjl8px07txmxsy454urhgzk57szmcuttpqgq5cvcdy.private, 183 | player_2: aleo1wyvu96dvv0auq9e4qme54kjuhzglyfcf576h0g3nrrmrmr0505pqd6wnry.private, 184 | prev_hit_or_miss: 0u64.private, 185 | _nonce: 4374626042494973146987320062571809401151262172766172816829659487584978644457group.public 186 | }' 187 | 188 | >>> ➡️ Outputs 189 | 190 | • { 191 | owner: aleo1wyvu96dvv0auq9e4qme54kjuhzglyfcf576h0g3nrrmrmr0505pqd6wnry.private, 192 | gates: 0u64.private, 193 | hits_and_misses: 0u64.private, 194 | played_tiles: 0u64.private, 195 | ships: 9044591273705727u64.private, 196 | player_1: aleo1wyvu96dvv0auq9e4qme54kjuhzglyfcf576h0g3nrrmrmr0505pqd6wnry.private, 197 | player_2: aleo15g9c69urtdhvfml0vjl8px07txmxsy454urhgzk57szmcuttpqgq5cvcdy.private, 198 | game_started: true.private, 199 | _nonce: 6222383571142756260765569201308836492199048237638652378826141459336360362251group.public 200 | } 201 | • { 202 | owner: aleo15g9c69urtdhvfml0vjl8px07txmxsy454urhgzk57szmcuttpqgq5cvcdy.private, 203 | gates: 0u64.private, 204 | incoming_fire_coordinate: 0u64.private, 205 | player_1: aleo1wyvu96dvv0auq9e4qme54kjuhzglyfcf576h0g3nrrmrmr0505pqd6wnry.private, 206 | player_2: aleo15g9c69urtdhvfml0vjl8px07txmxsy454urhgzk57szmcuttpqgq5cvcdy.private, 207 | prev_hit_or_miss: 0u64.private, 208 | _nonce: 3742551407126138397717446975757978589064777004441277005584760115236217735495group.public 209 | } 210 | 211 | ✅ Executed 'battleship.aleo/start_battleship' 212 | ``` 213 | 214 | Notice the outputs here are similar to `offer_battleship`. A dummy move.record is owned by Player 1, and Player 2 gets a board_state.record with the `game_started` flag updated. However, now that Player 1 has a move.record and a started board, they can begin to play. Switch `program.json`'s keys back to Player 1's. Player 1 now makes the first real move: `aleo run play 'board_state.record' 'move.record' fire_coordinate` 215 | ```bash 216 | aleo run play '{ 217 | owner: aleo15g9c69urtdhvfml0vjl8px07txmxsy454urhgzk57szmcuttpqgq5cvcdy.private, 218 | gates: 0u64.private, 219 | hits_and_misses: 0u64.private, 220 | played_tiles: 0u64.private, 221 | ships: 1157459741006397447u64.private, 222 | player_1: aleo15g9c69urtdhvfml0vjl8px07txmxsy454urhgzk57szmcuttpqgq5cvcdy.private, 223 | player_2: aleo1wyvu96dvv0auq9e4qme54kjuhzglyfcf576h0g3nrrmrmr0505pqd6wnry.private, 224 | game_started: true.private, 225 | _nonce: 6563064852163330630334088854834332804417910882908622526775624018226782316843group.public 226 | }' '{ 227 | owner: aleo15g9c69urtdhvfml0vjl8px07txmxsy454urhgzk57szmcuttpqgq5cvcdy.private, 228 | gates: 0u64.private, 229 | incoming_fire_coordinate: 0u64.private, 230 | player_1: aleo1wyvu96dvv0auq9e4qme54kjuhzglyfcf576h0g3nrrmrmr0505pqd6wnry.private, 231 | player_2: aleo15g9c69urtdhvfml0vjl8px07txmxsy454urhgzk57szmcuttpqgq5cvcdy.private, 232 | prev_hit_or_miss: 0u64.private, 233 | _nonce: 3742551407126138397717446975757978589064777004441277005584760115236217735495group.public 234 | }' 1u64 235 | 236 | >>> ➡️ Outputs 237 | 238 | • { 239 | owner: aleo15g9c69urtdhvfml0vjl8px07txmxsy454urhgzk57szmcuttpqgq5cvcdy.private, 240 | gates: 0u64.private, 241 | hits_and_misses: 0u64.private, 242 | played_tiles: 1u64.private, 243 | ships: 1157459741006397447u64.private, 244 | player_1: aleo15g9c69urtdhvfml0vjl8px07txmxsy454urhgzk57szmcuttpqgq5cvcdy.private, 245 | player_2: aleo1wyvu96dvv0auq9e4qme54kjuhzglyfcf576h0g3nrrmrmr0505pqd6wnry.private, 246 | game_started: true.private, 247 | _nonce: 1474170213684980843727833284550698461565286563122422722760769547002894080093group.public 248 | } 249 | • { 250 | owner: aleo1wyvu96dvv0auq9e4qme54kjuhzglyfcf576h0g3nrrmrmr0505pqd6wnry.private, 251 | gates: 0u64.private, 252 | incoming_fire_coordinate: 1u64.private, 253 | player_1: aleo15g9c69urtdhvfml0vjl8px07txmxsy454urhgzk57szmcuttpqgq5cvcdy.private, 254 | player_2: aleo1wyvu96dvv0auq9e4qme54kjuhzglyfcf576h0g3nrrmrmr0505pqd6wnry.private, 255 | prev_hit_or_miss: 0u64.private, 256 | _nonce: 5481529266389297320813092061136936339861329677911328036818179854958874588416group.public 257 | } 258 | 259 | ✅ Executed 'battleship.aleo/play' 260 | ``` 261 | 262 | Player 1 has an updated board_state.record -- they have a new `played_tiles` bitstring, which corresponds to the fire coordinate they just sent to Player 2. You can see that the `incoming_fire_coordinate` in the move.record owned by Player 2 matches exactly the input given by Player 1. Player 2 can now play this move tile and respond with a fire coordinate of their own, and they will also let Player 1 know whether or not Player 1's fire coordinate hit or miss Player 2's ships. 263 | 264 | Switch `program.json` to Player 2's keys. Player 2 makes their move: 265 | ```bash 266 | aleo run play '{ 267 | owner: aleo1wyvu96dvv0auq9e4qme54kjuhzglyfcf576h0g3nrrmrmr0505pqd6wnry.private, 268 | gates: 0u64.private, 269 | hits_and_misses: 0u64.private, 270 | played_tiles: 0u64.private, 271 | ships: 9044591273705727u64.private, 272 | player_1: aleo1wyvu96dvv0auq9e4qme54kjuhzglyfcf576h0g3nrrmrmr0505pqd6wnry.private, 273 | player_2: aleo15g9c69urtdhvfml0vjl8px07txmxsy454urhgzk57szmcuttpqgq5cvcdy.private, 274 | game_started: true.private, 275 | _nonce: 6222383571142756260765569201308836492199048237638652378826141459336360362251group.public 276 | }' '{ 277 | owner: aleo1wyvu96dvv0auq9e4qme54kjuhzglyfcf576h0g3nrrmrmr0505pqd6wnry.private, 278 | gates: 0u64.private, 279 | incoming_fire_coordinate: 1u64.private, 280 | player_1: aleo15g9c69urtdhvfml0vjl8px07txmxsy454urhgzk57szmcuttpqgq5cvcdy.private, 281 | player_2: aleo1wyvu96dvv0auq9e4qme54kjuhzglyfcf576h0g3nrrmrmr0505pqd6wnry.private, 282 | prev_hit_or_miss: 0u64.private, 283 | _nonce: 5481529266389297320813092061136936339861329677911328036818179854958874588416group.public 284 | }' 2048u64 285 | 286 | >>> ➡️ Outputs 287 | 288 | • { 289 | owner: aleo1wyvu96dvv0auq9e4qme54kjuhzglyfcf576h0g3nrrmrmr0505pqd6wnry.private, 290 | gates: 0u64.private, 291 | hits_and_misses: 0u64.private, 292 | played_tiles: 2048u64.private, 293 | ships: 9044591273705727u64.private, 294 | player_1: aleo1wyvu96dvv0auq9e4qme54kjuhzglyfcf576h0g3nrrmrmr0505pqd6wnry.private, 295 | player_2: aleo15g9c69urtdhvfml0vjl8px07txmxsy454urhgzk57szmcuttpqgq5cvcdy.private, 296 | game_started: true.private, 297 | _nonce: 5254963165391133332409074172682159033621708071536429341861038147524454777097group.public 298 | } 299 | • { 300 | owner: aleo15g9c69urtdhvfml0vjl8px07txmxsy454urhgzk57szmcuttpqgq5cvcdy.private, 301 | gates: 0u64.private, 302 | incoming_fire_coordinate: 2048u64.private, 303 | player_1: aleo1wyvu96dvv0auq9e4qme54kjuhzglyfcf576h0g3nrrmrmr0505pqd6wnry.private, 304 | player_2: aleo15g9c69urtdhvfml0vjl8px07txmxsy454urhgzk57szmcuttpqgq5cvcdy.private, 305 | prev_hit_or_miss: 1u64.private, 306 | _nonce: 5851606198769770675504009323414373017067582072428989801313256693053765675198group.public 307 | } 308 | 309 | ✅ Executed 'battleship.aleo/play' 310 | ``` 311 | 312 | Player 2 now has an updated board_state.record which includes their newly updated `played_tiles`, only containing the fire coordinate they just sent to Player 1. Player 1 now owns a new move.record which includes the `hits_and_misses` field. This contains only the result of Player 1's previous fire coordinate they had sent to Player 2. It will always be a single coordinate on the 8x8 grid if it's a hit. A miss is 0u64 (8x8 grid of 0s), whereas a hit is the u64 equivalent of their previous fire coordinate in bitstring form. If you check Player 2's ships configuration, you'll note their entire bottom row is covered by two ships, so sample valid hits on the bottom row would be: 1u64, 2u64, 4u64, 8u64, 16u64, 32u64, 64u64, and 128u64. Since Player 1's first fire coordinate (1u64) was a hit, the `hits_and_misses` field is also 1u64. 313 | 314 | Player 1's next move will consume this move.record, which will update Player 1's board with the hit-or-miss, as well as figure out the result of Player 2's fire coordinate. Now that Player 1 has some `played_tiles`, they can no longer choose an alread-played fire coordinate. For example, running `aleo run play 'board_state.record' 'move.record' 1u64` will fail, because 1u64 has already been played. 315 | 316 | Switch `program.json` to use Player 1's keys. Run: 317 | ```bash 318 | aleo run play '{ 319 | owner: aleo15g9c69urtdhvfml0vjl8px07txmxsy454urhgzk57szmcuttpqgq5cvcdy.private, 320 | gates: 0u64.private, 321 | hits_and_misses: 0u64.private, 322 | played_tiles: 1u64.private, 323 | ships: 1157459741006397447u64.private, 324 | player_1: aleo15g9c69urtdhvfml0vjl8px07txmxsy454urhgzk57szmcuttpqgq5cvcdy.private, 325 | player_2: aleo1wyvu96dvv0auq9e4qme54kjuhzglyfcf576h0g3nrrmrmr0505pqd6wnry.private, 326 | game_started: true.private, 327 | _nonce: 1474170213684980843727833284550698461565286563122422722760769547002894080093group.public 328 | }' '{ 329 | owner: aleo15g9c69urtdhvfml0vjl8px07txmxsy454urhgzk57szmcuttpqgq5cvcdy.private, 330 | gates: 0u64.private, 331 | incoming_fire_coordinate: 2048u64.private, 332 | player_1: aleo1wyvu96dvv0auq9e4qme54kjuhzglyfcf576h0g3nrrmrmr0505pqd6wnry.private, 333 | player_2: aleo15g9c69urtdhvfml0vjl8px07txmxsy454urhgzk57szmcuttpqgq5cvcdy.private, 334 | prev_hit_or_miss: 1u64.private, 335 | _nonce: 5851606198769770675504009323414373017067582072428989801313256693053765675198group.public 336 | }' 2u64 337 | 338 | >>> ➡️ Outputs 339 | 340 | • { 341 | owner: aleo15g9c69urtdhvfml0vjl8px07txmxsy454urhgzk57szmcuttpqgq5cvcdy.private, 342 | gates: 0u64.private, 343 | hits_and_misses: 1u64.private, 344 | played_tiles: 3u64.private, 345 | ships: 1157459741006397447u64.private, 346 | player_1: aleo15g9c69urtdhvfml0vjl8px07txmxsy454urhgzk57szmcuttpqgq5cvcdy.private, 347 | player_2: aleo1wyvu96dvv0auq9e4qme54kjuhzglyfcf576h0g3nrrmrmr0505pqd6wnry.private, 348 | game_started: true.private, 349 | _nonce: 853278652528988609827041334083853520436225751739504321439524466875699631772group.public 350 | } 351 | • { 352 | owner: aleo1wyvu96dvv0auq9e4qme54kjuhzglyfcf576h0g3nrrmrmr0505pqd6wnry.private, 353 | gates: 0u64.private, 354 | incoming_fire_coordinate: 2u64.private, 355 | player_1: aleo15g9c69urtdhvfml0vjl8px07txmxsy454urhgzk57szmcuttpqgq5cvcdy.private, 356 | player_2: aleo1wyvu96dvv0auq9e4qme54kjuhzglyfcf576h0g3nrrmrmr0505pqd6wnry.private, 357 | prev_hit_or_miss: 0u64.private, 358 | _nonce: 710336412388939616658264778971886770861024495941253598683184288448156545822group.public 359 | } 360 | 361 | ✅ Executed 'battleship.aleo/play' 362 | ``` 363 | 364 | As before, both a board_state.record and move.record are created. The board_state.record now contains 3u64 as the `played_tiles`, which looks like this in bitstring form: 365 | ``` 366 | 0 0 0 0 0 0 0 0 367 | 0 0 0 0 0 0 0 0 368 | 0 0 0 0 0 0 0 0 369 | 0 0 0 0 0 0 0 0 370 | 0 0 0 0 0 0 0 0 371 | 0 0 0 0 0 0 0 0 372 | 0 0 0 0 0 0 0 0 373 | 0 0 0 0 0 0 1 1 374 | ``` 375 | 376 | The board_state.record `hits_and_misses` field has also been updated with the result of their previous move. The new move.record owned by Player 2 now contains information about whether Player 2's previous move was a hit or miss, as well as Player 1's new fire coordinate. 377 | 378 | Switch `program.json`'s keys to Player 2. Player 2 makes their next move: 379 | ```bash 380 | aleo run play '{ 381 | owner: aleo1wyvu96dvv0auq9e4qme54kjuhzglyfcf576h0g3nrrmrmr0505pqd6wnry.private, 382 | gates: 0u64.private, 383 | hits_and_misses: 0u64.private, 384 | played_tiles: 2048u64.private, 385 | ships: 9044591273705727u64.private, 386 | player_1: aleo1wyvu96dvv0auq9e4qme54kjuhzglyfcf576h0g3nrrmrmr0505pqd6wnry.private, 387 | player_2: aleo15g9c69urtdhvfml0vjl8px07txmxsy454urhgzk57szmcuttpqgq5cvcdy.private, 388 | game_started: true.private, 389 | _nonce: 5254963165391133332409074172682159033621708071536429341861038147524454777097group.public 390 | }' '{ 391 | owner: aleo1wyvu96dvv0auq9e4qme54kjuhzglyfcf576h0g3nrrmrmr0505pqd6wnry.private, 392 | gates: 0u64.private, 393 | incoming_fire_coordinate: 2u64.private, 394 | player_1: aleo15g9c69urtdhvfml0vjl8px07txmxsy454urhgzk57szmcuttpqgq5cvcdy.private, 395 | player_2: aleo1wyvu96dvv0auq9e4qme54kjuhzglyfcf576h0g3nrrmrmr0505pqd6wnry.private, 396 | prev_hit_or_miss: 0u64.private, 397 | _nonce: 710336412388939616658264778971886770861024495941253598683184288448156545822group.public 398 | }' 4u64 399 | 400 | >>> ➡️ Outputs 401 | 402 | • { 403 | owner: aleo1wyvu96dvv0auq9e4qme54kjuhzglyfcf576h0g3nrrmrmr0505pqd6wnry.private, 404 | gates: 0u64.private, 405 | hits_and_misses: 0u64.private, 406 | played_tiles: 2052u64.private, 407 | ships: 9044591273705727u64.private, 408 | player_1: aleo1wyvu96dvv0auq9e4qme54kjuhzglyfcf576h0g3nrrmrmr0505pqd6wnry.private, 409 | player_2: aleo15g9c69urtdhvfml0vjl8px07txmxsy454urhgzk57szmcuttpqgq5cvcdy.private, 410 | game_started: true.private, 411 | _nonce: 1145182747531998766752104305052328886102707397061849372000385383229513301534group.public 412 | } 413 | • { 414 | owner: aleo15g9c69urtdhvfml0vjl8px07txmxsy454urhgzk57szmcuttpqgq5cvcdy.private, 415 | gates: 0u64.private, 416 | incoming_fire_coordinate: 4u64.private, 417 | player_1: aleo1wyvu96dvv0auq9e4qme54kjuhzglyfcf576h0g3nrrmrmr0505pqd6wnry.private, 418 | player_2: aleo15g9c69urtdhvfml0vjl8px07txmxsy454urhgzk57szmcuttpqgq5cvcdy.private, 419 | prev_hit_or_miss: 2u64.private, 420 | _nonce: 5958326936461495382488152485080596366937963499216527548334225566230682598418group.public 421 | } 422 | 423 | ✅ Executed 'battleship.aleo/play' 424 | ``` 425 | 426 | Play continues back and forth between Player 1 and Player 2. When one player has a total of 14 flipped bits in their `hits_and_misses` field on their board_state.record, they have won the game. 427 |
428 | 429 | ## Strategy for Creating ZK Battleship 430 | 431 | Battleship is a game where two players lay their ships into secret configurations on their respective 8x8 grids, and then take turns firing upon each other's board. The game ends when one player has sunk all of the other player's ships. 432 | 433 | How can we ensure that the ship configurations of each player remains secret, while being able to trustlessly and fairly play with their opponent? ZK proofs on the Aleo chain can help us lay out rigid rules that must be followed in order to prevent cheating. 434 | 435 | Broadly speaking, we can follow this general strategy: 436 | 1. Create mathematical rules for placing the ships on the board, to ensure that neither player can cheat by stacking all their ships in one place, moving them off the board, or laying them across each other. 437 | 438 | 2. Ensure that the players and boards that begin a game cannot be swapped out. 439 | 440 | 3. Ensure that each player can only move once before the next player can move. 441 | 442 | 4. Enforce constraints on valid moves, and force the player to give their opponent information about their opponent's previous move in order to continue playing. 443 | 444 | See the github repo in order to follow along. 445 | 446 | ## Modeling the board and ships 447 | 448 | Most battleship representations in programs use a 64 character string or an array of arrays (8 arrays of 8 elements each) to model the board state. Unfortunately, Aleo instructions don't represent strings well yet, nor can we use for or while loops. Luckily for us, Aleo has the unsigned 64 bit integer type, or u64. To represent every space on a battleship board, from top left to bottom right, we can use each bit in a u64. For example, an empty board would be: 449 | 0u64 = 450 | ``` 451 | 0 0 0 0 0 0 0 0 452 | 0 0 0 0 0 0 0 0 453 | 0 0 0 0 0 0 0 0 454 | 0 0 0 0 0 0 0 0 455 | 0 0 0 0 0 0 0 0 456 | 0 0 0 0 0 0 0 0 457 | 0 0 0 0 0 0 0 0 458 | 0 0 0 0 0 0 0 0 459 | ``` 460 | 461 | Battleship is played with 4 different ship types -- a ship of length 5, length 4, length 3, and length 2. Some versions of battleship have an extra length 3 ship or another extra ship type, however, we will stick to the most basic version for this project. In order to be a valid ship placement, a ship must be placed vertically or horizontally (no diagonals). On a physical board, a ship cannot break across rows or intersect with another ship, but ships are allowed to touch one another. 462 | 463 | Similar to how we represent a board with a u64 bitstring, we can represent a ship horizontally as a bitstring. We "flip" the bits to represent a ship: 464 | | Length | Bitstring | u64 | 465 | | ------ | --------- | --- | 466 | | 5 | 11111 | 31u64| 467 | | 4 | 1111 | 15u64| 468 | | 3 | 111 | 7u64 | 469 | | 2 | 11 | 3u64 | 470 | 471 | We can also represent a ship vertically as a bitstring. To show this, we need 7 "unflipped" bits (zeroes) in between the flipped bits so that the bits are adjacent vertically. 472 | | Length | Bitstring | u64 | 473 | | --- | --- | --- | 474 | | 5 | 1 00000001 00000001 00000001 00000001 | 4311810305u64 | 475 | | 4 | 1 00000001 00000001 00000001 | 16843009u64 | 476 | | 3 | 1 00000001 00000001 | 65793u64 | 477 | | 2 | 1 00000001 | 257u64 | 478 | 479 | With a board model and ship bitstring models, we can now place ships on a board. 480 | 481 |
Examples of valid board configurations: 482 | 483 | 17870284429256033024u64 484 | ``` 485 | 1 1 1 1 1 0 0 0 486 | 0 0 0 0 0 0 0 0 487 | 0 0 0 0 0 0 0 1 488 | 0 0 0 0 0 0 0 1 489 | 1 1 1 1 0 0 0 1 490 | 0 0 0 0 0 0 0 0 491 | 0 0 0 0 0 0 1 1 492 | 0 0 0 0 0 0 0 0 493 | ``` 494 | 495 | 16383u64 496 | ``` 497 | 0 0 0 0 0 0 0 0 498 | 0 0 0 0 0 0 0 0 499 | 0 0 0 0 0 0 0 0 500 | 0 0 0 0 0 0 0 0 501 | 0 0 0 0 0 0 0 0 502 | 0 0 0 0 0 0 0 0 503 | 0 0 1 1 1 1 1 1 504 | 1 1 1 1 1 1 1 1 505 | ``` 506 | 507 | 2157505700798988545u64 508 | ``` 509 | 0 0 0 1 1 1 0 1 510 | 1 1 1 1 0 0 0 1 511 | 0 0 0 0 0 0 0 0 512 | 0 0 0 0 0 0 0 1 513 | 0 0 0 0 0 0 0 1 514 | 0 0 0 0 0 0 0 1 515 | 0 0 0 0 0 0 0 1 516 | 0 0 0 0 0 0 0 1 517 | ``` 518 | 519 |
520 | 521 |
Examples of invalid board configurations: 522 | 523 | Ships overlapping the bottom ship: 524 | 67503903u64 525 | ``` 526 | 0 0 0 0 0 0 0 0 527 | 0 0 0 0 0 0 0 0 528 | 0 0 0 0 0 0 0 0 529 | 0 0 0 0 0 0 0 0 530 | 0 0 0 0 0 1 0 0 531 | 0 0 0 0 0 1 1 0 532 | 0 0 0 0 0 1 1 1 533 | 0 0 0 1 1 1 1 1 534 | ``` 535 | 536 | Diagonal ships: 537 | 9242549787790754436u64 538 | ``` 539 | 1 0 0 0 0 0 0 0 540 | 0 1 0 0 0 1 0 0 541 | 0 0 1 0 0 0 1 0 542 | 0 0 0 1 0 0 0 0 543 | 0 0 0 1 1 0 0 0 544 | 0 0 1 0 0 0 0 1 545 | 0 1 0 0 0 0 1 0 546 | 1 0 0 0 0 1 0 0 547 | ``` 548 | 549 | Ships splitting across rows and columns: 550 | 1297811850814034450u64 551 | ``` 552 | 0 0 0 1 0 0 1 0 553 | 0 0 0 0 0 0 1 0 554 | 1 1 0 0 0 0 0 1 555 | 0 0 0 0 0 0 0 0 556 | 1 0 0 1 0 0 0 1 557 | 0 0 0 1 0 0 0 0 558 | 0 0 0 1 0 0 1 0 559 | 0 0 0 1 0 0 1 0 560 | ``` 561 |
562 | 563 | Given these rules, our strategy will be to validate each individaul ship bitstring placement on a board, and then, if all the ships are valid, compose all the positions onto a board and validate that the board with all ships are valid. If each individual ship's position is valid, then all the ships together should be valid unless any overlapping occurs. 564 | 565 | ## Validating a single ship at a time 566 | 567 | To follow along with the code, all verification of ship bitstrings is done in verify.aleo. We know a ship is valid if all these conditions are met: 568 | If horizontal: 569 | 1. The correct number of bits is flipped (a ship of length 5 should not have 6 flipped bits) 570 | 2. All the bits are adjacent to each other. 571 | 3. The bits do not split a row. 572 | 573 | If vertical: 574 | 1. The correct number of bits is flipped. 575 | 2. All the bits are adjacent to each other, vertically. This means that each flipped bit should be separated by exactly 7 unflipped bits. 576 | 3. The bits do not split a column. 577 | 578 | If a ship is valid vertically or horizontally, then we know the ship is valid. We just need to check for the bit count, the adjacency of those bits, and make sure those bits do not split a row/column. However, we can't loop through the bit string to count bits, or to make sure those bits don't break across columns. We'll need to turn to special bitwise operations and hacks. 579 | 580 |
Bit Counting 581 | 582 | See the "c_bitcount" closure to follow along with the code. 50 years ago, MIT AI Laboratory published HAKMEM, which was a series of tricks and hacks to speed up processing for bitwise operations. https://w3.pppl.gov/~hammett/work/2009/AIM-239-ocr.pdf We turned to HAKMEM 169 for bitcounting inspiration, although we've tweaked our implementation to be (hopefully) easier to understand. Before diving into details, let's build some intuition. 583 | 584 | Let a,b,c,d be either 0 or 1. Given a polynomial 8a + 4b + 2c + d, how do we find the summation of a + b + c + d? 585 | If we subtract subsets of this polynomial, we'll be left with the summation. 586 | Step 1: 8a + 4b + 2c + d 587 | Step 2: -4a - 2b - c 588 | Step 3: -2a - b 589 | Step 4: - a 590 | Step 5: = a + b + c + d 591 | This polynomial is basically a bitwise representation of a number, so given a 4 bit number, e.g. 1011 or 13u64, we can follow these instructions to get the bit count. Step 2 is just subtracting the starting number but bit shifted to the right (equivalent to dividing by 2). Step 3 bit shifts the starting number to the right twice and is subtracted, and Step 4 bit shifts thrice and is subtracted. Put another way: Start with a 4-digit binary number A. A - (A >> 1) - (A >> 2) - (A >> 3) = B. 592 | Step 1: 1101 = 13u64 593 | Step 2: -0110 = 6u64 594 | Step 3: -0011 = 3u64 595 | Step 4: -0001 = 1u64 596 | Step 5: =0011 = 3u64 597 | 598 | To make this process work for any bit-length number, where the sum of the bits is left in groups of 4 bits, we'll need to use some bit-masking, so that the sum of one group of 4 does not interfere with the next group of 4. 599 | With a larger starting number, like 1111 0001 0111 0110, we will need the following bit maskings: 600 | ``` 601 | For A >> 1, we'll use 0111 0111 0111 .... (in u64, this is 8608480567731124087u64) 602 | For A >> 2, we'll use 0011 0011 0011 .... (in u64, this is 3689348814741910323u64) 603 | For A >> 3, we'll use 0001 0001 0001 .... (in u64, this is 1229782938247303441u64) 604 | ``` 605 | 606 | For example, finding the sums of groups of 4 with a 16-bit number we'll call A to yield the bit sum number B: 607 | ``` 608 | A: 1111 0001 0111 0110 609 | A>>1: 0111 1000 1011 1011 610 | A>>2: 0011 1100 0101 1101 611 | A>>3: 0001 1110 0010 1110 612 | 613 | A>>1: 0111 1000 1011 1011 614 | & 0111 0111 0111 0111: 615 | 0111 0000 0011 0011 616 | 617 | A>>2: 0011 1100 0101 1101 618 | & 0011 0011 0011 0011: 619 | 0011 0000 0001 0001 620 | 621 | A>>3: 0001 1110 0010 1110 622 | & 0001 0001 0001 0001: 623 | 0001 0000 0000 0000 624 | 625 | A - (A>>1 & 0111....) - (A>>2 & 0011....) - (A>>3 & 0001....): 626 | B: 0100 0001 0011 0010 627 | 4 1 3 2 628 | ``` 629 | 630 | The next step is to combine the summation of each of those 4-bit groups into sums of 8-bit groups. To do this, we'll use another bit trick. We will shift this number B to the right by 4 (B >> 4), and add that back to B. Then, we'll apply a bit masking of 0000 1111 0000 1111 .... (in u64, this is 1085102592571150095u64) to yield the sums of bits in groups of 8, a number we'll call C. 631 | ``` 632 | B: 0100 0001 0011 0010 633 | B>>4: 0000 0100 0001 0011 634 | 0100 0101 0100 0101 635 | 4 5 4 5 636 | 637 | apply the bit mask 638 | 0000 1111 0000 1111 639 | 640 | C: 0000 0101 0000 0101 641 | 0 5 0 5 642 | ``` 643 | 644 | At this point, we've gone from a bit sum in groups of 4 to bit sums in groups of 8. That's great, but ultimately we want the total sum of bits in the original binary number. The final bit trick is to modulo C by 255. This is 2^8 - 1. For a bit of intuition, consider the number 1 0000 0001. If we take 1 0000 0001 mod 256, we're left with 1. If we take 1 0000 0001 mod 255, we're left with 2. Modding by 255 gives us the amount of bits _beyond_ the first 255 numbers, as 255 is the largest number that can be represented with 8 bits. 645 | 646 | A full summary of abbreviated steps to get the bit count, starting with a 64 bit integer A (closely following the c_bitcount closure in the verify.aleo code): 647 | let A = 64 unsigned bit integer 648 | let B = A - (A>>1 & 8608480567731124087u64) - (A>>2 & 3689348814741910323u64) - (A>>3 & 1229782938247303441u64) 649 | let C = (B - B>>4) & 1085102592571150095u64 650 | bit count = C mod 255u64 651 | 652 |
653 | 654 |
Adjacency Check 655 | 656 | Given a ship's placement on the board and its bitstring representation (horizontally or vertically), we can determine if the bits are adjacent. Follow the c_adjacency_check closure in verify.aleo. Given the ship of length 2, we know it's horizontal bitstring is 11 (3u64) and it's vertical bitstring is 100000001 (257u64). If on the board, the ship starts at the bottom right corner, its horizontal ship placement string would be: 657 | 3u64 658 | ``` 659 | 0 0 0 0 0 0 0 0 660 | 0 0 0 0 0 0 0 0 661 | 0 0 0 0 0 0 0 0 662 | 0 0 0 0 0 0 0 0 663 | 0 0 0 0 0 0 0 0 664 | 0 0 0 0 0 0 0 0 665 | 0 0 0 0 0 0 0 0 666 | 0 0 0 0 0 0 1 1 667 | ``` 668 | 669 | Vertical ship placement: 670 | 257u64 671 | ``` 672 | 0 0 0 0 0 0 0 0 673 | 0 0 0 0 0 0 0 0 674 | 0 0 0 0 0 0 0 0 675 | 0 0 0 0 0 0 0 0 676 | 0 0 0 0 0 0 0 0 677 | 0 0 0 0 0 0 0 0 678 | 0 0 0 0 0 0 0 1 679 | 0 0 0 0 0 0 0 1 680 | ``` 681 | 682 | If we move the ship to the left one column: 683 | Horizontal 6u64 684 | ``` 685 | 0 0 0 0 0 0 0 0 686 | 0 0 0 0 0 0 0 0 687 | 0 0 0 0 0 0 0 0 688 | 0 0 0 0 0 0 0 0 689 | 0 0 0 0 0 0 0 0 690 | 0 0 0 0 0 0 0 0 691 | 0 0 0 0 0 0 0 0 692 | 0 0 0 0 0 1 1 0 693 | ``` 694 | 695 | Vertical 514u64 696 | ``` 697 | 0 0 0 0 0 0 0 0 698 | 0 0 0 0 0 0 0 0 699 | 0 0 0 0 0 0 0 0 700 | 0 0 0 0 0 0 0 0 701 | 0 0 0 0 0 0 0 0 702 | 0 0 0 0 0 0 0 0 703 | 0 0 0 0 0 0 1 0 704 | 0 0 0 0 0 0 1 0 705 | ``` 706 | 707 | If we move the ship up one row: 708 | Horizontal 768u64 709 | ``` 710 | 0 0 0 0 0 0 0 0 711 | 0 0 0 0 0 0 0 0 712 | 0 0 0 0 0 0 0 0 713 | 0 0 0 0 0 0 0 0 714 | 0 0 0 0 0 0 0 0 715 | 0 0 0 0 0 0 0 0 716 | 0 0 0 0 0 0 1 1 717 | 0 0 0 0 0 0 0 0 718 | ``` 719 | 720 | Vertical 65792u64 721 | ``` 722 | 0 0 0 0 0 0 0 0 723 | 0 0 0 0 0 0 0 0 724 | 0 0 0 0 0 0 0 0 725 | 0 0 0 0 0 0 0 0 726 | 0 0 0 0 0 0 0 0 727 | 0 0 0 0 0 0 0 1 728 | 0 0 0 0 0 0 0 1 729 | 0 0 0 0 0 0 0 0 730 | ``` 731 | 732 | We can make the observation that the original bitstring is always shifted by a power of 2 to get to a new valid position on the board. Therefore, if we take the ship placement bitstring and divide by the ship bitstring (either horizontal or vertical), as long as the remaining number is a power of 2 (2^0, 2^1, 2^2, 2^3...), we know the ship's bits are adjacent. 733 | 734 | To ensure that the remaining number is a power of 2, we can use a bit trick. See the bit trick for ensuring a bitstring is a power of 2 section. 735 | 736 | In the code, you'll notice one extra step. Dividing a ship placement bitstring by a ship bitstring representation could result in 0, and then subtracting by 1 will result in an underflow. In that case, we know the ship placement is not valid, so we can set a number which is gauranteed to not be a power of 2. 737 |
738 | 739 |
Splitting a row or column 740 | Follow the c_horizontal_check closure in verify.aleo to follow the code. Assume all the bits are adjacent (see the adjacency check section). The column case is trivial. We can be certain that if a ship bitstring splits columns, the division of that ship placement bitstring by its ship bitstring representation will not yield a power of 2, and it would have failed the adjacency check. 741 | 742 | The horizontal case must be checked because a split row bitstring could still contain a ship with adjacent bits. To make this check easier, we will condense the 64 bitstring into an 8 bitstring by taking it modulo 255. If we assume that a bitstring is not splitting a row, then taking the ship placement bitstring modulo 255 will yield an 8 bit valid bitstring. If the original ship placement bitstring is not valid, then we will have an invalid 8 bit bitstring. E.g.: 743 | ``` 744 | 1 1 1 0 0 0 0 0 745 | 0 0 0 0 0 0 0 0 746 | 0 0 0 0 0 0 0 0 747 | 0 0 0 0 0 0 0 0 748 | 0 0 0 0 0 0 0 0 749 | 0 0 0 0 0 0 0 0 750 | 0 0 0 0 0 0 0 0 751 | 0 0 0 0 0 0 0 0 752 | ``` 753 | mod 255 = 11100000 (valid) 754 | 755 | ``` 756 | 0 0 0 0 0 0 0 1 757 | 1 1 0 0 0 0 0 0 758 | 0 0 0 0 0 0 0 0 759 | 0 0 0 0 0 0 0 0 760 | 0 0 0 0 0 0 0 0 761 | 0 0 0 0 0 0 0 0 762 | 0 0 0 0 0 0 0 0 763 | 0 0 0 0 0 0 0 0 764 | ``` 765 | mod 255 = 11000001 (invalid) 766 | 767 | How do we know the 8 bit bitstring is valid or not? We can simply do an adjacency check, as before. 768 | 769 |
770 | 771 |
Ensuring a bitstring is a power of 2 772 | 773 | Any power of 2 will have a single bit flipped. If we subtract 1 from that number, it will result in a complementary bitstring that, bitwise-anded with the original, will always result in 0. 774 | 775 | E.g. 776 | ``` 777 | 8: 1000 778 | 8-1: 0111 779 | 8&7: 0000 == 0 780 | 781 | 7: 0111 782 | 7-1: 0110 783 | 7&6: 0110 != 0 784 | ``` 785 | 786 |
787 | 788 | ## Validating all ships together in a single board 789 | 790 | Give individual valid ship position bitstrings, we can combine all these together into a single board using bitwise or operators. See the create_board function in verify.aleo to follow the code. Once all ships are on the board, we can count the total number of bits, which should be 14 exactly for a ship of length 5, 4, 3, and 2. 791 | 792 | ## Ensure that players and boards cannot swap mid-game 793 | 794 | Board states are represented with the board_state record. Each board has a flag indicating whether a game has been started with the board. This flag is set when offering a battleship game to an opponent, or accepting a battleship game from an opponent. Move records are created only in 3 ways: 795 | 1. Offering a battleship game creates a dummy move record that sets the two players to the addresses set in the board state record. 796 | 2. Accepting a battleship game consumes the first dummy move record and checks that the move record contains the same two players as the board of the player accepting the game. Then, a new dummy move record is created and keeps the same two players. 797 | 2. A move record must be consumed in order to play and create the next move record. There's a check to ensure the players in the move record matches the players in the board, and the players in the next move record are automatically set. 798 | 799 | The only way moves _not_ matching a board can be combined is if the players begin multiple games with each other. As long as one player is honest and only accepts a single game with a particular opponent, only one set of moves can be played on one board between them. 800 | 801 | ## Ensure that each player can only move once before the next player can move 802 | 803 | A move record must be consumed in order to create the next move record. The owner of the move record changes with each play. Player A must spend a move record in order to create a move record containing their fire coordinate, and that move record will be owned by Player B. Player B must spend that move record in order to create the next move record, which will belong to Player A. 804 | 805 | ## Enforce constraints on valid moves, and force the player to give their opponent information about their opponent's previous move in order to continue playing 806 | 807 | A valid move for a player is a fire coordinate that has only one flipped bit in a u64. We can make sure only one bit is flipped with the powers of 2 bit trick. That single bit must be a coordinate that has not been played by that player before, which we check in board.aleo/update_played_tiles. 808 | 809 | In order to give their next move to their opponent, a player must call the main.aleo/play function, which checks the opponent's fire coordinate on the current player's board. The move record being created is updated with whether that fire coordinate was a hit or a miss for the opponent. 810 | 811 | ## Winning the game 812 | 813 | Right now, the way to check when a game has been won is to count the number of hits on your hits_and_misses field on your board_state record. Once you have 14 hits, you've won the game. 814 | --------------------------------------------------------------------------------