├── ui ├── index.html └── hc.js ├── test ├── testGame1 │ ├── _config.json │ ├── playerB.json │ ├── playerA.json │ └── sequenceDiagram.svg ├── publicFunctionTests.json └── validationFunctionTests.json ├── .gitignore ├── dna ├── battleship │ ├── resultSchema.json │ ├── guessSchema.json │ ├── invitationSchema.json │ ├── gameSchema.json │ ├── boardSchema.json │ └── battleship.js ├── properties_schema.json └── dna.json ├── LICENSE.md └── README.md /ui/index.html: -------------------------------------------------------------------------------- 1 |
Your UI here! -------------------------------------------------------------------------------- /ui/hc.js: -------------------------------------------------------------------------------- 1 | function yourApp(){alert('your UI code here!')} -------------------------------------------------------------------------------- /test/testGame1/_config.json: -------------------------------------------------------------------------------- 1 | { 2 | "GossipInterval": 100, 3 | "Duration": 4 4 | } 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | priv.key 3 | agent.txt 4 | 5 | # OS generated files # 6 | ###################### 7 | .DS_Store 8 | .DS_Store? 9 | ._* 10 | .Spotlight-V100 11 | .Trashes 12 | ehthumbs.db 13 | Thumbs.db -------------------------------------------------------------------------------- /dna/battleship/resultSchema.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Game Result Schema", 3 | "type": "object", 4 | "properties": { 5 | "gameHash": { "type": "string" }, 6 | "winner": { "type": "string"} 7 | }, 8 | "required": ["gameHash", "winner"] 9 | } -------------------------------------------------------------------------------- /dna/properties_schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Properties Schema", 3 | "type": "object", 4 | "properties": { 5 | "description": { 6 | "type": "string" 7 | }, 8 | "language": { 9 | "type": "string" 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /dna/battleship/guessSchema.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Guess Schema", 3 | "type": "object", 4 | "properties": { 5 | "x": { "type": "integer"}, 6 | "y": { "type": "integer"}, 7 | "gameHash": {"type": "string"}, 8 | "playerHash": {"type": "string"} 9 | }, 10 | "required": ["x", "y", "gameHash", "playerHash"] 11 | } -------------------------------------------------------------------------------- /dna/battleship/invitationSchema.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Game Invitation Schema", 3 | "type": "object", 4 | "properties": { 5 | "creator": { "type": "string" }, 6 | "creatorBoardHash": { "type": "string"}, 7 | "invitee": { "type": "string" } 8 | }, 9 | "required": ["creator", "invitee", "creatorBoardHash"] 10 | } -------------------------------------------------------------------------------- /dna/battleship/gameSchema.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Game Schema", 3 | "type": "object", 4 | "properties": { 5 | "creator": { "type": "string" }, 6 | "creatorBoardHash": { "type": "string"}, 7 | "invitee": { "type": "string" }, 8 | "inviteeBoardHash": { "type": "string"} 9 | }, 10 | "required": ["creator", "creatorBoardHash", "invitee", "inviteeBoardHash"] 11 | } -------------------------------------------------------------------------------- /dna/battleship/boardSchema.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Board Schema", 3 | 4 | "definitions": { 5 | "pieceLocation": { 6 | "type": "object", 7 | "properties": { 8 | "x": { "type": "integer"}, 9 | "y": { "type": "integer"}, 10 | "orientation": { "enum": ["h", "v"]} 11 | }, 12 | "required": ["x","y","orientation"] 13 | } 14 | }, 15 | 16 | "type": "object", 17 | "properties": { 18 | "pieces": { 19 | "type": "array", 20 | "items": {"$ref": "#/definitions/pieceLocation"} 21 | }, 22 | "salt": { "type": "string"} 23 | }, 24 | "required": ["pieces"] 25 | } -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Willem Olding 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /test/testGame1/playerB.json: -------------------------------------------------------------------------------- 1 | { 2 | "Tests": [ 3 | { 4 | "Time": 2000, 5 | "Convey": "That the send invitation is visible", 6 | "Zome": "battleship", 7 | "FnName": "getReceivedInvitations", 8 | "Raw": true, 9 | "Input": "getReceivedInvitations({playerHash: '%playerB_key%'})[0].Hash", 10 | "Regexp": ".*" 11 | }, 12 | { 13 | "Time": 2400, 14 | "Convey": "That the invitation can be accepted", 15 | "Zome": "battleship", 16 | "FnName": "acceptInvitation", 17 | "Input": { 18 | "board": { 19 | "pieces": [ 20 | {"x": 0, "y": 0, "orientation": "h"}, 21 | {"x": 2, "y": 6, "orientation": "h"}, 22 | {"x": 7, "y": 6, "orientation": "v"}, 23 | {"x": 9, "y": 2, "orientation": "v"}, 24 | {"x": 4, "y": 3, "orientation": "h"} 25 | ] 26 | }, 27 | "inviteHash": "%result0%" 28 | }, 29 | "Regexp": ".*" 30 | }, 31 | { 32 | "Time": 2600, 33 | "Convey": "Can make the first guess", 34 | "Zome": "battleship", 35 | "FnName": "makeGuess", 36 | "Input": { 37 | "gameHash": {"%result%": 1}, 38 | "guess": {"x": 0, "y": 0} 39 | }, 40 | "Output": "true" 41 | } 42 | ] 43 | } 44 | -------------------------------------------------------------------------------- /test/testGame1/playerA.json: -------------------------------------------------------------------------------- 1 | { 2 | "Tests": [ 3 | { 4 | "Time": 1000, 5 | "Convey": "Create a new game invitation", 6 | "Zome": "battleship", 7 | "FnName": "newInvitation", 8 | "Input": { 9 | "board": { 10 | "pieces": [ 11 | {"x": 0, "y": 0, "orientation": "h"}, 12 | {"x": 2, "y": 6, "orientation": "h"}, 13 | {"x": 7, "y": 6, "orientation": "v"}, 14 | {"x": 9, "y": 2, "orientation": "v"}, 15 | {"x": 4, "y": 3, "orientation": "h"} 16 | ] 17 | }, 18 | "invitee": "%playerB_key%" 19 | }, 20 | "Output": "%h1%" 21 | }, 22 | { 23 | "Time": 2200, 24 | "Convey": "The invitation was linked to this nodes hash", 25 | "Zome": "battleship", 26 | "FnName": "getSentInvitations", 27 | "Raw": true, 28 | "Input": "getSentInvitations({playerHash: '%playerA_key%'}).length", 29 | "Output": 1 30 | }, 31 | { 32 | "Time": 2400, 33 | "Convey": "The invitation was linked to other player", 34 | "Zome": "battleship", 35 | "FnName": "getReceivedInvitations", 36 | "Raw": true, 37 | "Input": "getReceivedInvitations({playerHash: '%playerB_key%'}).length", 38 | "Output": 1 39 | }, 40 | { 41 | "Time": 2600, 42 | "Convey": "The other player accepted the invitation and created a game which is visible", 43 | "Zome": "battleship", 44 | "FnName": "getCurrentGames", 45 | "Raw": true, 46 | "Input": "getCurrentGames({playerHash: '%playerA_key%'})[0].Hash", 47 | "Regexp": ".*" 48 | }, 49 | { 50 | "Time": 2800, 51 | "Convey": "Can make the second guess", 52 | "Zome": "battleship", 53 | "FnName": "makeGuess", 54 | "Input": { 55 | "gameHash": "%result3%", 56 | "guess": {"x": 1, "y": 1} 57 | }, 58 | "Output": "false" 59 | } 60 | ] 61 | } 62 | -------------------------------------------------------------------------------- /test/publicFunctionTests.json: -------------------------------------------------------------------------------- 1 | 2 | { 3 | "Tests": [ 4 | { 5 | "Convey": "Can call newInvitation", 6 | "Zome": "battleship", 7 | "FnName": "newInvitation", 8 | "Input": { 9 | "board": { 10 | "pieces": [ 11 | {"x": 0, "y": 0, "orientation": "h"}, 12 | {"x": 2, "y": 6, "orientation": "h"}, 13 | {"x": 7, "y": 6, "orientation": "v"}, 14 | {"x": 9, "y": 2, "orientation": "v"}, 15 | {"x": 4, "y": 3, "orientation": "h"} 16 | ] 17 | }, 18 | "invitee": "%key%" 19 | }, 20 | "Output": "%h1%" 21 | }, 22 | 23 | { 24 | "Convey": "Can call acceptInvitation", 25 | "Zome": "battleship", 26 | "FnName": "acceptInvitation", 27 | "Input": { 28 | "board": { 29 | "pieces": [ 30 | {"x": 0, "y": 0, "orientation": "h"}, 31 | {"x": 2, "y": 6, "orientation": "h"}, 32 | {"x": 7, "y": 6, "orientation": "v"}, 33 | {"x": 9, "y": 2, "orientation": "v"}, 34 | {"x": 4, "y": 3, "orientation": "h"} 35 | ] 36 | }, 37 | "inviteHash": "%r1%" 38 | }, 39 | "Output": "%h1%" 40 | }, 41 | 42 | { 43 | "Convey": "Can call makeGuess", 44 | "Zome": "battleship", 45 | "FnName": "makeGuess", 46 | "Input": { 47 | "gameHash": "%h1%", 48 | "guess": {"x": 0, "y": 0} 49 | }, 50 | "Regexp": ".*" 51 | }, 52 | 53 | { 54 | "Convey": "Can call getCurrentGames", 55 | "Zome": "battleship", 56 | "FnName": "getCurrentGames", 57 | "Input": {"playerHash": "%key%"}, 58 | "Regexp": ".*" 59 | }, 60 | 61 | { 62 | "Convey": "Can call getSentInvitations", 63 | "Zome": "battleship", 64 | "FnName": "getSentInvitations", 65 | "Input": {"playerHash": "%key%"}, 66 | "Regexp": ".*" 67 | }, 68 | 69 | { 70 | "Convey": "Can call getReceivedInvitations", 71 | "Zome": "battleship", 72 | "FnName": "getReceivedInvitations", 73 | "Input": {"playerHash": "%key%"}, 74 | "Regexp": ".*" 75 | }, 76 | { 77 | "Convey": "Can call getGuesses", 78 | "Zome": "battleship", 79 | "FnName": "getGuesses", 80 | "Input": {"gameHash": {"%result%":1}}, 81 | "Output": [{"gameHash":"%h3%","playerHash":"%key%","x":0,"y":0}] 82 | } 83 | ] 84 | } 85 | -------------------------------------------------------------------------------- /test/validationFunctionTests.json: -------------------------------------------------------------------------------- 1 | { 2 | "Tests": [ 3 | { 4 | "Convey": "Can identify too few pieces in a board", 5 | "Zome": "battleship", 6 | "Raw": true, 7 | "Input": "hasCorrectNumberOfPieces({pieces: [{x: 0, y: 0, orientation: 'h'},{x: 3, y: 2, orientation: 'v'},{x: 6, y: 3, orientation: 'h'}]})", 8 | "Output": false 9 | }, 10 | { 11 | "Convey": "Can identify too many pieces in a board", 12 | "Zome": "battleship", 13 | "Raw": true, 14 | "Input": "hasCorrectNumberOfPieces({pieces: [{x: 0, y: 0, orientation: 'h'},{x: 3, y: 2, orientation: 'v'},{x: 6, y: 3, orientation: 'h'},{x: 6, y: 3, orientation: 'h'},{x: 6, y: 3, orientation: 'h'},{x: 6, y: 3, orientation: 'h'}]})", 15 | "Output": false 16 | }, 17 | { 18 | "Convey": "Can identify correct number of pieces", 19 | "Zome": "battleship", 20 | "Raw": true, 21 | "Input": "hasCorrectNumberOfPieces({pieces: [{x: 0, y: 0, orientation: 'h'},{x: 3, y: 2, orientation: 'v'},{x: 6, y: 3, orientation: 'h'},{x: 6, y: 3, orientation: 'h'},{x: 6, y: 3, orientation: 'h'}]})", 22 | "Output": true 23 | }, 24 | 25 | { 26 | "Convey": "Can identify a piece out of bounds", 27 | "Zome": "battleship", 28 | "Raw": true, 29 | "Input": "piecesInBounds({pieces: [{x: 7, y: 7, orientation: 'h'}]})", 30 | "Output": false 31 | }, 32 | { 33 | "Convey": "Can identify a piece in bounds", 34 | "Zome": "battleship", 35 | "Raw": true, 36 | "Input": "piecesInBounds({pieces: [{x: 2, y: 2, orientation: 'h'}]})", 37 | "Output": true 38 | }, 39 | 40 | { 41 | "Convey": "Can evaulate a missing guess", 42 | "Zome": "battleship", 43 | "Raw": true, 44 | "Input": "evaluateGuess({pieces: [{x: 5, y: 0, orientation: 'v'}]}, {x: 0, y: 0})", 45 | "Output": false 46 | }, 47 | { 48 | "Convey": "Can evaulate a hitting guess", 49 | "Zome": "battleship", 50 | "Raw": true, 51 | "Input": "evaluateGuess({pieces: [{x: 5, y: 0, orientation: 'v'}]}, {x: 5, y: 2})", 52 | "Output": true 53 | }, 54 | 55 | { 56 | "Convey": "Can identify overlapping pieces", 57 | "Zome": "battleship", 58 | "Raw": true, 59 | "Input": "noPiecesOverlapping({pieces: [{x: 5, y: 0, orientation: 'v'},{x: 2, y: 2, orientation: 'h'}]})", 60 | "Output": false 61 | }, 62 | { 63 | "Convey": "Can identify that no pieces overlap", 64 | "Zome": "battleship", 65 | "Raw": true, 66 | "Input": "noPiecesOverlapping({pieces: [{x: 5, y: 0, orientation: 'v'},{x: 4, y: 0, orientation: 'v'}]})", 67 | "Output": true 68 | }, 69 | { 70 | "Convey": "Can validate a board", 71 | "Zome": "battleship", 72 | "Raw": true, 73 | "Input": "boardIsValid({pieces: [{x: 0, y: 0, orientation: 'h'},{x: 2, y: 6, orientation: 'h'},{x: 7, y: 6, orientation: 'v'},{x: 9, y: 2, orientation: 'v'},{x: 4, y: 3, orientation: 'h'}]})", 74 | "Output": true 75 | } 76 | 77 | ] 78 | } 79 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Holochain-Battleship 2 | 3 | The classic two player game battleship implemented in Holochain 4 | 5 | While much of the functionality is working this is still under active development. No validation or UI at this stage. 6 | 7 | ## Overview 8 | 9 | This is an educational example of peer-to-peer interactions usign Holochain. Battleship makes a great example as it requires some interesting mechanics including: 10 | 11 | - Making use of the local chain to commit a board layout while keeping it secret 12 | - Sending and responding to guesses using messaging 13 | - Validation of game rules to prevent cheating 14 | - Keeping a validated record of all game actions in the shared DHT 15 | 16 | ## Design 17 | 18 | If an agent (PlayerA) wishes to play a game with another agent (PlayerB) they first need create a board which defines where they want their ships located in the 10x10 grid which committed to their local chain. The board entry also has some salt stored with it so the layout cannot be inferred from the hash. 19 | 20 | They then post an **invitation entry** to the DHT. The invitation contains the address of the agent with wish to play with as well as the hash of the board entry. This public sharing of the board hash prevents the agent from changing it later. This invitation is linked to the other agents permanent address. 21 | 22 | PlayerB can check their pending invitations by querying the links on their address. To take action an invitation PlayerB must also commit a board to their local chain and then posts a **game entry**. The game entry contains the addresses of both players along with their board hashes. PlayerB gets to make the first guess. 23 | 24 | A **guess entry** represents the equivalent of a player taking their turn to guess out loud a particular location (e.g. D5). Guess entries are linked to their game. After posting their guess public the player then uses the messaging protocol to inform the other agent. Upon checking that the guess is committed to the DHT the other player will respond if that location is a hit or miss. Their response is also stored in the public DHT. 25 | 26 | Players take turns guessing and validation ensures that the correct ordering is followed. There will reach a point where one player will guess the final remaining ship location of the other player. This will trigger the end of the game. On this trigger both agents will publish the previously private boards to the public DHT. The winning player will then publish a **results entry**. Pushing this entry to the DHT triggers a validation process which ensures every guess and response was in accordance with the respective boards. Passing validation the results entry is published and linked to both players address. 27 | 28 | The revealing of the board at the end of the game allows any party to audit a game and ensure that guesses and responses made during the game were correct. 29 | 30 | ## Test Scenarios 31 | 32 | Sequence diagram of test scenario 1. In order the players: 33 | - Both register their names 34 | - Player A commits a private board and sends an invite 35 | - Player B commits a private board and accepts the invite 36 | - Player B makes a guess to which player A responds 37 | - Player A makes a guess to which player B responds 38 | 39 |  40 | 41 | ## Getting Started 42 | 43 | Assuming you have a working installation of Holochain simply clone the repo run in development mode using: 44 | ``` 45 | hcdev web 46 | ``` 47 | from the project directory. To run the tests or test scenarios use 48 | ``` 49 | hcdev test 50 | ``` 51 | or 52 | ``` 53 | hcdev --mdns=true scenario testGame1 54 | ``` 55 | 56 | To produce the mermaid sequence diagram code use 57 | ``` 58 | hcdev --mdns=true scenario testGame1 | sed -n 's:.*