├── 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 | ![Alt text](./test/testGame1/sequenceDiagram.svg) 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:.*\(.*\).*:\1:p' 59 | ``` 60 | ## Authors 61 | 62 | [Willem Olding](https://github.com/willemolding) 63 | 64 | ## License 65 | 66 | This project is licensed under the MIT License - see the [LICENSE.md](LICENSE.md) file for details 67 | -------------------------------------------------------------------------------- /dna/dna.json: -------------------------------------------------------------------------------- 1 | { 2 | "Version": 1, 3 | "UUID": "c58d29e8-5116-11e8-9026-685b35836414", 4 | "Name": "holochain-battleship", 5 | "Properties": { 6 | "description": "provides an application template", 7 | "language": "en" 8 | }, 9 | "PropertiesSchemaFile": "properties_schema.json", 10 | "RequiresVersion": 23, 11 | "DHTConfig": { 12 | "HashType": "sha2-256", 13 | "NeighborhoodSize": 0 14 | }, 15 | "Progenitor": { 16 | "Identity": "", 17 | "PubKey": null 18 | }, 19 | "Zomes": [ 20 | { 21 | "Name": "battleship", 22 | "Description": "Provides the rules of the game battleship as played on Holochain", 23 | "CodeFile": "battleship.js", 24 | "RibosomeType": "js", 25 | "BridgeFuncs": null, 26 | "Config": { 27 | "ErrorHandling": "throwErrors" 28 | }, 29 | "Entries": [ 30 | { 31 | "Name": "agentString", 32 | "DataFormat": "string", 33 | "Sharing": "public" 34 | }, 35 | { 36 | "Name": "game", 37 | "DataFormat": "json", 38 | "Schema": "", 39 | "SchemaFile": "gameSchema.json", 40 | "Sharing": "public" 41 | }, 42 | { 43 | "Name": "privateBoard", 44 | "DataFormat": "json", 45 | "Schema": "", 46 | "SchemaFile": "boardSchema.json", 47 | "Sharing": "private" 48 | }, 49 | { 50 | "Name": "publicBoard", 51 | "DataFormat": "json", 52 | "Schema": "", 53 | "SchemaFile": "boardSchema.json", 54 | "Sharing": "public" 55 | }, 56 | { 57 | "Name": "guess", 58 | "DataFormat": "json", 59 | "Schema": "", 60 | "SchemaFile": "guessSchema.json", 61 | "Sharing": "public" 62 | }, 63 | { 64 | "Name": "invitation", 65 | "DataFormat": "json", 66 | "Schema": "", 67 | "SchemaFile": "invitationSchema.json", 68 | "Sharing": "public" 69 | }, 70 | { 71 | "Name": "result", 72 | "DataFormat": "json", 73 | "Schema": "", 74 | "SchemaFile": "resultSchema.json", 75 | "Sharing": "public" 76 | }, 77 | 78 | { 79 | "Name": "agentStringLink", 80 | "DataFormat": "links", 81 | "Sharing": "public" 82 | }, 83 | { 84 | "Name": "inviteLinks", 85 | "DataFormat": "links", 86 | "Sharing": "public" 87 | }, 88 | { 89 | "Name": "resultLinks", 90 | "DataFormat": "links", 91 | "Sharing": "public" 92 | }, 93 | { 94 | "Name": "gameLinks", 95 | "DataFormat": "links", 96 | "Sharing": "public" 97 | }, 98 | { 99 | "Name": "guessLinks", 100 | "DataFormat": "links", 101 | "Sharing": "public" 102 | } 103 | ], 104 | "Functions": [ 105 | { 106 | "Name": "registerName", 107 | "CallingType": "json", 108 | "Exposure": "public" 109 | }, 110 | { 111 | "Name": "newInvitation", 112 | "CallingType": "json", 113 | "Exposure": "public" 114 | }, 115 | { 116 | "Name": "acceptInvitation", 117 | "CallingType": "json", 118 | "Exposure": "public" 119 | }, 120 | { 121 | "Name": "makeGuess", 122 | "CallingType": "json", 123 | "Exposure": "public" 124 | }, 125 | { 126 | "Name": "getSentInvitations", 127 | "CallingType": "json", 128 | "Exposure": "public" 129 | }, 130 | { 131 | "Name": "getReceivedInvitations", 132 | "CallingType": "json", 133 | "Exposure": "public" 134 | }, 135 | { 136 | "Name": "getCurrentGames", 137 | "CallingType": "json", 138 | "Exposure": "public" 139 | }, 140 | { 141 | "Name": "getGuesses", 142 | "CallingType": "json", 143 | "Exposure": "public" 144 | } 145 | ] 146 | } 147 | ] 148 | } 149 | -------------------------------------------------------------------------------- /dna/battleship/battleship.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var me = App.Key.Hash; 4 | 5 | var BOARD_SIZE = 10; 6 | var PIECE_SIZES = [5,4,3,3,2]; 7 | 8 | 9 | /*============================================= 10 | = public zome functions = 11 | =============================================*/ 12 | 13 | /** 14 | * Registers an agents name by linking an entry to their base hash 15 | * 16 | * @param {Object} payload 17 | * @param {string} payload.agentName - The name to register for this agent 18 | */ 19 | function registerName(payload) { 20 | // commit agent name and link to the key hash. 21 | var agentStringHash = commit("agentString", payload.agentName); 22 | commit("agentStringLink", { 23 | Links: [ { Base: me, Link: agentStringHash, Tag: "agentString" } ] 24 | }); 25 | } 26 | 27 | /** 28 | * Create a new game invitation 29 | * 30 | * @param {Object} payload 31 | * @param {Object} payload.board - Board object for the game 32 | * @param {string} payload.invitee - Hash of agent to invite 33 | * @return {string} inviteHash - The hash of this invitation entry if successful 34 | */ 35 | function newInvitation(payload) { 36 | debug(payload); 37 | var board = payload.board; 38 | var invitee = payload.invitee; 39 | 40 | var boardHash = commitPrivateBoard(board); 41 | 42 | // commit the game with the local hash of the board 43 | var invitation = { 44 | creator: me, 45 | creatorBoardHash: boardHash, 46 | invitee: invitee 47 | }; 48 | 49 | var inviteHash = commit("invitation", invitation); 50 | 51 | // link to both players 52 | commit("inviteLinks", { 53 | Links: [ { Base: me, Link: inviteHash, Tag: "creator" }, 54 | { Base: invitee, Link: inviteHash, Tag: "invitee" } ] 55 | }); 56 | 57 | return inviteHash; 58 | } 59 | 60 | 61 | /** 62 | * Accepts an invitation and posts a game entry 63 | * 64 | * @param {Object} payload 65 | * @param {Object} payload.board - Board object 66 | * @param {string} payload.inviteHash - Hash of an invite this is responding to 67 | * @return {string} Hash of the newly created game entry 68 | */ 69 | function acceptInvitation(payload) { 70 | debug(payload); 71 | var inviteHash = payload.inviteHash; 72 | var board = payload.board; 73 | 74 | var game = get(inviteHash); 75 | 76 | var boardHash = commitPrivateBoard(board); 77 | game.inviteeBoardHash = boardHash; 78 | 79 | var gameHash = commit("game", game); 80 | 81 | // link to both players 82 | commit("gameLinks", { 83 | Links: [ { Base: game.invitee, Link: gameHash, Tag: "game" }, 84 | { Base: game.creator, Link: gameHash, Tag: "game" } ] 85 | }); 86 | debug(gameHash); 87 | return gameHash; 88 | } 89 | 90 | /** 91 | * Makes a guess 92 | * 93 | * @param {Object} payload 94 | * @param {string} payload.gameHash - Hash of the game to make a guess 95 | * @param {Object} payload.guess - The guess to make 96 | * @return {string} guessHash - The hash of the created guess if successful 97 | */ 98 | function makeGuess(payload) { 99 | debug(payload); 100 | var gameHash = payload.gameHash; 101 | var guess = payload.guess; 102 | guess.playerHash = me; 103 | guess.gameHash = gameHash; 104 | 105 | // commit the guess to the global dht. This will trigger the validation 106 | var guessHash = commit("guess", guess); 107 | 108 | commit("guessLinks", { 109 | Links: [ { Base: gameHash, Link: guessHash, Tag: me } ] 110 | }); 111 | 112 | // message the guess hash to the other player 113 | // they will post a response if it is valid 114 | var game = get(gameHash); 115 | return send(getOtherPlayer(game), {guessHash: guessHash}); 116 | } 117 | 118 | /** 119 | * Get the invitations that have been sent by a player 120 | * 121 | * @param {Object} payload 122 | * @param {string} payload.playerHash 123 | * @return {Array} Array of invitation objects 124 | */ 125 | function getSentInvitations(payload) { 126 | return getLinks(payload.playerHash, "creator", { Load: true }).map(function(elem) { 127 | return {Entry: elem.Entry, Hash: elem.Hash}; 128 | }); 129 | } 130 | 131 | /** 132 | * Get the invitations received by a player 133 | * 134 | * @param {Object} payload 135 | * @param {string} payload.playerHash 136 | * @return {Array} Array of received invitations 137 | */ 138 | function getReceivedInvitations(payload) { 139 | return getLinks(payload.playerHash, "invitee", { Load: true }).map(function(elem) { 140 | return {Entry: elem.Entry, Hash: elem.Hash}; 141 | }); 142 | } 143 | 144 | /** 145 | * Get the games a player has been involved in 146 | * 147 | * @param {Object} payload 148 | * @param {string} payload.playerHash 149 | * @return {Array} Array of games 150 | */ 151 | function getCurrentGames(payload) { 152 | return getLinks(payload.playerHash, "game", { Load: true }).map(function(elem) { 153 | return {Entry: elem.Entry, Hash: elem.Hash}; 154 | }); 155 | } 156 | 157 | /** 158 | * Gets the guesses. 159 | * 160 | * @param {Object} payload 161 | * @param {string} payload.gameHash 162 | * @return {Array} The guesses 163 | */ 164 | function getGuesses(payload) { 165 | return getLinks(payload.gameHash, "", { Load : true }).map(function(elem) { 166 | return elem.Entry; 167 | }); 168 | } 169 | 170 | 171 | 172 | /*===== End of public zome functions ======*/ 173 | 174 | /*============================================ 175 | = local zome functions = 176 | ============================================*/ 177 | 178 | function generateSalt() { 179 | // this is not a true random generator but ok for a game 180 | // return "" + Math.random() + "" + Math.random(); 181 | return "000" // use this for testing to make it deterministic 182 | } 183 | 184 | function commitPrivateBoard(board) { 185 | // commit the board to the local chain. 186 | // This may fail if the board is not valid 187 | board.salt = generateSalt(); 188 | var boardHash = commit("privateBoard", board); 189 | return boardHash; 190 | } 191 | 192 | 193 | function hasCorrectNumberOfPieces(board) { 194 | return board.pieces.length === PIECE_SIZES.length; 195 | } 196 | 197 | 198 | function piecesInBounds(board) { 199 | // 'every' returns the logical AND of the function evaluated on each array element 200 | return board.pieces.every(function(piece, i) { 201 | if (piece.orientation === "h") { 202 | return piece.x >= 0 && piece.x + PIECE_SIZES[i] - 1 < BOARD_SIZE && piece.y >= 0 && piece.y < BOARD_SIZE 203 | } else { 204 | return piece.x >= 0 && piece.x < BOARD_SIZE && piece.y >= 0 && piece.y + PIECE_SIZES[i] - 1 < BOARD_SIZE 205 | } 206 | }); 207 | } 208 | 209 | function noPiecesOverlapping(board) { 210 | // compare every piece 211 | return board.pieces.every(function(piece, pieceIndex) { 212 | var noCollisions = true; 213 | for(var i = 0; i < PIECE_SIZES[i]; i++) { 214 | var guess; 215 | if(piece.orientation === 'h') { 216 | guess = { 217 | x: piece.x + i, 218 | y: piece.y 219 | }; 220 | } else { 221 | guess = { 222 | x: piece.x, 223 | y: piece.y + i 224 | }; 225 | } 226 | noCollisions = noCollisions && !evaluateGuess(board, guess, pieceIndex); 227 | } 228 | return noCollisions; 229 | }); 230 | } 231 | 232 | function boardIsValid(board) { 233 | return hasCorrectNumberOfPieces(board) 234 | && piecesInBounds(board) 235 | && noPiecesOverlapping(board); 236 | } 237 | 238 | function evaluateGuess(board, guess, indexToIgnore) { 239 | // return if a guess is a hit (true) or miss (false) on this board 240 | // indexToIgnore (optional) - dont inlude this piece in the evaluation 241 | // 'some' returns the logical OR of the function evaluated on each array element 242 | return board.pieces.some(function(piece, i) { 243 | if(i === indexToIgnore) return false; 244 | 245 | var xmin = piece.x; 246 | var xmax = piece.orientation === "h" ? xmin+PIECE_SIZES[i]-1 : xmin; 247 | var ymin = piece.y; 248 | var ymax = piece.orientation === "v" ? ymin+PIECE_SIZES[i]-1 : ymin; 249 | 250 | return ( guess.x >= xmin && guess.x <= xmax && guess.y >= ymin && guess.y <= ymax); 251 | }); 252 | } 253 | 254 | 255 | function getOtherPlayer(game) { 256 | if(game.creator === me) { 257 | return game.invitee; 258 | } else if (game.invitee === me) { 259 | return game.creator; 260 | } else { 261 | throw "Node is not a player in this game"; 262 | } 263 | } 264 | 265 | 266 | function isPlayersTurn(gameHash, playerHash) { 267 | // for it to be a players turn the most recent guess must not belong to this player 268 | var guesses = getGuesses(gameHash); 269 | 270 | if (guesses.length === 0) { 271 | // this is the first guess of the game 272 | // the invitee gets the first guess 273 | game = get(gameHash); 274 | return playerHash === game.invitee; 275 | } 276 | 277 | var lastGuess = guesses[guesses.length - 1].Entry; 278 | return lastGuess.playerHash !== playerHash; // this prevents a node playing with themselves 279 | } 280 | 281 | 282 | function guessWithinBounds(guess) { 283 | return guess.x > 0 && guess.x < BOARD_SIZE 284 | && guess.y > 0 && guess.y < BOARD_SIZE; 285 | } 286 | 287 | 288 | function guessIsValid(guess) { 289 | return isPlayersTurn(guess.gameHash, guess.playerHash) 290 | && guessWithinBounds(guess); 291 | } 292 | 293 | 294 | /*===== End of local zome functions ======*/ 295 | 296 | /*================================= 297 | = Callbacks = 298 | =================================*/ 299 | 300 | 301 | function receive(from, message) { 302 | // receiving a message means a player is requesting response to a guess. 303 | // If a guess is in the DHT it has already been validated by other nodes so 304 | // no additional verification is required 305 | var guessHash = message.guessHash; 306 | var guess = get(guessHash); 307 | var game = get(guess.gameHash); 308 | 309 | // next up find which board in the local chain to verify against 310 | var boardHash; 311 | if (game.creator === me) { 312 | boardHash = game.creatorBoardHash; 313 | } else if (game.invitee == me) { 314 | boardHash = game.inviteeBoardHash; 315 | } else { 316 | throw "Cannot resond to guess"; 317 | } 318 | 319 | var board = get(boardHash, {Local: true}); 320 | 321 | // respond if the guess is a hit or miss 322 | return evaluateGuess(board, guess); 323 | } 324 | 325 | 326 | function genesis() { 327 | registerName({agentName: App.Agent.String}); 328 | return true 329 | } 330 | 331 | 332 | function validateCommit (entryName, entry, header, pkg, sources) { 333 | switch(entryName) { 334 | case "game": 335 | // can only add a game when responding to an invitation 336 | return true; 337 | case "guess": 338 | // can only guess if it is your turn and you are playing the game 339 | return true;//guessIsValid(entry); 340 | case "result": 341 | // can only produce a result under strict winning conditions 342 | return true; 343 | case "privateBoard": 344 | case "publicBoard": 345 | return boardIsValid(entry); 346 | 347 | // all of these entries have no restrictions (links are validated seperatly) 348 | case "agentString": 349 | case "invitation": 350 | case "agentStringLink": 351 | case "inviteLinks": 352 | case "resultLinks": 353 | case "gameLinks": 354 | case "guessLinks": 355 | return true; 356 | default: 357 | return false; 358 | } 359 | } 360 | 361 | function validatePut (entryName, entry, header, pkg, sources) { 362 | return validateCommit(entryName, entry, header, pkg, sources); 363 | } 364 | 365 | function validateMod (entryName, entry, header, replaces, pkg, sources) { 366 | return false; // no modifications allowed 367 | } 368 | 369 | function validateDel (entryName, hash, pkg, sources) { 370 | return false; // no deletions are allowed 371 | } 372 | 373 | function validateLink(linkEntryType,baseHash,links,pkg,sources) { 374 | return true; 375 | } 376 | 377 | function validatePutPkg (entryName) { 378 | return null; 379 | } 380 | 381 | function validateModPkg (entryName) { 382 | return null; 383 | } 384 | 385 | function validateDelPkg (entryName) { 386 | return null; 387 | } 388 | 389 | function validateLinkPkg (entryName) { 390 | return null; 391 | } 392 | 393 | /*===== End of Callbacks ======*/ 394 | 395 | /*========================================== 396 | = function overrides = 397 | ==========================================*/ 398 | // used for creating sequence diagrams 399 | // delete these for production 400 | 401 | agentShortname = App.Agent.String.substr(0, App.Agent.String.indexOf('@')); 402 | 403 | var oldCommit = commit; 404 | commit = function(entryType, entryData) { 405 | if(entryType.indexOf("private") !== -1) { 406 | debug('' + agentShortname + '-->>' + agentShortname +': '+ entryType + ''); 407 | } else { 408 | debug('' + agentShortname + '-->>DHT: '+ entryType + ''); 409 | } 410 | return oldCommit(entryType, entryData); 411 | }; 412 | 413 | // don't override get for now to make the diagram easier to understand 414 | // var oldGet = get; 415 | // get = function(hash, options) { 416 | // result = oldGet(hash, options); 417 | // debug('' + 'DHT-->>' + agentShortname + ': '+ 'requested data' + ''); 418 | // return result; 419 | // }; 420 | 421 | var oldSend = send; 422 | send = function(to, message, options) { 423 | var toAgentName = getLinks(to, "agentString", { Load: true })[0].Entry; 424 | var toAgentShortname = toAgentName.substr(0, App.Agent.String.indexOf('@')); 425 | debug('' + agentShortname + '-->>' + toAgentShortname + ': '+ 'inform of guess' + ''); 426 | return oldSend(to, message, options); 427 | } 428 | 429 | /*===== End of function overrides ======*/ 430 | -------------------------------------------------------------------------------- /test/testGame1/sequenceDiagram.svg: -------------------------------------------------------------------------------- 1 | playerADHTplayerBagentStringagentStringagentStringLinkagentStringLinkprivateBoardinvitationinviteLinksprivateBoardgamegameLinksguessguessLinksinform of guessguessguessLinksinform of guessplayerADHTplayerB --------------------------------------------------------------------------------