├── .yarnrc ├── .gitignore ├── as-pect.config.js ├── assembly ├── __tests__ │ ├── as-pect.d.ts │ └── chess.spec.ts ├── tsconfig.json ├── model.ts └── main.ts ├── neardev ├── shared-test │ └── test.near.json └── shared-test-staging │ └── test.near.json ├── .gitpod.Dockerfile ├── src ├── loader.html ├── test.html ├── test.js ├── config.js ├── index.html └── main.js ├── asconfig.js ├── .travis.yml ├── README.md ├── .gitpod.yml ├── package.json └── setup.js /.yarnrc: -------------------------------------------------------------------------------- 1 | --install.freeze-lockfile true 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | out/ 3 | dist/ 4 | .DS_Store 5 | .cache/ -------------------------------------------------------------------------------- /as-pect.config.js: -------------------------------------------------------------------------------- 1 | module.exports = require("near-sdk-as/imports"); 2 | -------------------------------------------------------------------------------- /assembly/__tests__/as-pect.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /assembly/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "assemblyscript/std/assembly.json", 3 | "include": [ 4 | "./**/*.ts", 5 | "../**/*/as_types.d.ts" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /neardev/shared-test/test.near.json: -------------------------------------------------------------------------------- 1 | {"account_id":"test.near","private_key":"ed25519:2wyRcSwSuHtRVmkMCGjPwnzZmQLeXLzLLyED1NDMt4BjnKgQL6tF85yBx6Jr26D2dUNeC716RBoTxntVHsegogYw"} 2 | -------------------------------------------------------------------------------- /neardev/shared-test-staging/test.near.json: -------------------------------------------------------------------------------- 1 | {"account_id":"test.near","private_key":"ed25519:2wyRcSwSuHtRVmkMCGjPwnzZmQLeXLzLLyED1NDMt4BjnKgQL6tF85yBx6Jr26D2dUNeC716RBoTxntVHsegogYw"} 2 | -------------------------------------------------------------------------------- /.gitpod.Dockerfile: -------------------------------------------------------------------------------- 1 | FROM gitpod/workspace-full 2 | 3 | RUN bash -c ". .nvm/nvm.sh \ 4 | && nvm install v12 && nvm alias default v12 \ 5 | && nvm use default && npm i -g yarn && alias near='yarn near'" \ -------------------------------------------------------------------------------- /assembly/model.ts: -------------------------------------------------------------------------------- 1 | // @nearfile 2 | 3 | export class Game { 4 | player1: string; 5 | player2: string; 6 | fen: string = 'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1'; 7 | outcome: string; 8 | } 9 | 10 | export class GameWithId { 11 | id: u64; 12 | game: Game; 13 | } 14 | -------------------------------------------------------------------------------- /src/loader.html: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /asconfig.js: -------------------------------------------------------------------------------- 1 | 2 | const compile = require("near-sdk-as/compiler").compile; 3 | 4 | compile("assembly/main.ts", // input file 5 | "out/main.wasm", // output file 6 | [ 7 | // "-O1", // Optional arguments 8 | "--debug", 9 | "--measure", // Shows compiler runtime 10 | "--validate" // Validate the generated wasm module 11 | ], { 12 | verbose: true // Output the cli args passed to asc 13 | }); 14 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 12 4 | 5 | - env: 6 | - NODE_ENV=ci 7 | - NODE_ENV=ci-staging 8 | 9 | cache: yarn 10 | 11 | jobs: 12 | include: 13 | - name: yarn 14 | script: 15 | - yarn build 16 | - yarn test 17 | 18 | - name: fossa 19 | before_script: 20 | - "curl -H 'Cache-Control: no-cache' https://raw.githubusercontent.com/fossas/fossa-cli/fc60c6631a5d372d5a45fea35e31665b338f260d/install.sh | sudo bash" 21 | script: 22 | - fossa init 23 | - fossa analyze --server-scan 24 | - fossa test 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # NEAR Chess 2 | 3 | ## Description 4 | 5 | This example demonstrates how to create on-chain turn-based game (chess in this case) integrated with NEAR Wallet. 6 | 7 | [![Open in Gitpod](https://gitpod.io/button/open-in-gitpod.svg)](https://gitpod.io/#https://github.com/nearprotocol/near-chess) 8 | 9 | ## To Run 10 | 11 | ``` 12 | yarn 13 | yarn start 14 | ``` 15 | 16 | ## To Explore 17 | 18 | - `assembly/main.ts` for the contract code 19 | - `assembly/modelts` for the data model code 20 | - `src/main.html` for the front-end HTML 21 | - `src/main.js` for the JavaScript front-end code and how to integrate contracts 22 | - `src/test.js` for the JS tests for the contract 23 | -------------------------------------------------------------------------------- /src/test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /.gitpod.yml: -------------------------------------------------------------------------------- 1 | image: 2 | file: .gitpod.Dockerfile 3 | # Options to prebuild the image after github events and set notifications/badges 4 | # More here: https://www.gitpod.io/docs/prebuilds/ 5 | github: 6 | prebuilds: 7 | # enable for the master/default branch (defaults to true) 8 | master: true 9 | # enable for pull requests coming from this repo (defaults to true) 10 | pullRequests: true 11 | # enable for pull requests coming from forks (defaults to false) 12 | pullRequestsFromForks: true 13 | # add a check to pull requests (defaults to true) 14 | addCheck: true 15 | # add a "Review in Gitpod" button as a comment to pull requests (defaults to false) 16 | addComment: true 17 | 18 | # List the start up tasks. You can start them in parallel in multiple terminals. See https://www.gitpod.io/docs/config-start-tasks/ 19 | tasks: 20 | - before: nvm use default 21 | init: yarn && alias near=./node_modules/near-shell/bin/near 22 | command: yarn dev 23 | ports: 24 | - port: 1234 25 | onOpen: open-browser 26 | -------------------------------------------------------------------------------- /assembly/__tests__/chess.spec.ts: -------------------------------------------------------------------------------- 1 | import * as chess from "../main"; 2 | import { Context, context } from 'near-sdk-as'; 3 | import { Game } from "../model"; 4 | 5 | const PLAYER1 = "Bobby"; 6 | const PLAYER2 = "Garry" 7 | function getCurrentGame(player: string): Game { 8 | return chess.getGame(chess.getCurrentGame(player)); 9 | } 10 | 11 | describe("Game", () => { 12 | beforeAll(() => { 13 | Context.setSigner_account_id(PLAYER1); 14 | }); 15 | 16 | it("create a new game", () => { 17 | chess.createOrJoinGame(); 18 | const game = getCurrentGame(PLAYER1); 19 | expect(game.player1).toBe(PLAYER1, "Only one player."); 20 | expect(game.player2).toBeNull("No second player"); 21 | }); 22 | 23 | it("join a game", () => { 24 | Context.setSigner_account_id(PLAYER2); 25 | chess.createOrJoinGame(); 26 | const game = getCurrentGame(PLAYER1); 27 | expect(game.player1).toBe(PLAYER1, "Only one player."); 28 | expect(game.player2).not.toBeNull("Should be a second player"); 29 | expect(game.player2).toBe(PLAYER2); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "near-chess", 3 | "description": "Shows example of how to implement on-chain chess game and deploy to GitHub pages", 4 | "version": "0.0.1", 5 | "scripts": { 6 | "build": "npm run build:contract && npm run build:web", 7 | "build:contract": "node asconfig.js", 8 | "build:web": "parcel build src/index.html --public-url ./", 9 | "dev:deploy:contract": "near dev-deploy", 10 | "deploy:contract": "near deploy", 11 | "deploy:pages": "gh-pages -d dist/", 12 | "deploy": "npm run build && npm run deploy:contract && npm run deploy:pages", 13 | "prestart": "npm run build:contract && npm run dev:deploy:contract", 14 | "start": "CONTRACT_NAME=$(cat neardev/dev-account) parcel src/index.html", 15 | "dev": "nodemon --watch assembly -e ts --exec 'npm run start'", 16 | "test": "asp && npm run build:contract && jest test", 17 | "asp": "asp --verbose" 18 | }, 19 | "devDependencies": { 20 | "assemblyscript": "^0.9.4", 21 | "gh-pages": "^2.0.1", 22 | "gulp": "^4.0.2", 23 | "jest": "^22.4.4", 24 | "jest-environment-node": "^24.5.0", 25 | "near-sdk-as": "^0.1.2", 26 | "near-shell": "^0.20.1", 27 | "nodemon": "^2.0.2", 28 | "parcel-bundler": "^1.12.4" 29 | }, 30 | "dependencies": { 31 | "nearlib": "^0.20.0", 32 | "regenerator-runtime": "^0.13.3" 33 | }, 34 | "jest": { 35 | "testEnvironment": "near-shell/test_environment", 36 | "testPathIgnorePatterns": [ 37 | "/assembly/", 38 | "/node_modules/" 39 | ] 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/test.js: -------------------------------------------------------------------------------- 1 | describe("Authorizer", function() { 2 | let near; 3 | let contract; 4 | let alice; 5 | 6 | jasmine.DEFAULT_TIMEOUT_INTERVAL = 10000; 7 | 8 | // Common setup below 9 | beforeAll(async function() { 10 | near = await nearlib.connect(nearConfig); 11 | alice = nearConfig.contractName; 12 | contract = await near.loadContract(nearConfig.contractName, { 13 | // NOTE: This configuration only needed while NEAR is still in development 14 | // View methods are read only. They don't modify the state, but usually return some value. 15 | viewMethods: ["getCurrentGame", "getGame", "getRecentGames"], 16 | // Change methods can modify the state. But you don't receive the returned value when called. 17 | changeMethods: ["createOrJoinGame", "makeMove", "giveUpCurrentGame"], 18 | sender: alice 19 | }); 20 | }); 21 | 22 | // Multiple tests can be described below. Search Jasmine JS for documentation. 23 | describe("simple", function() { 24 | beforeAll(async function() { 25 | // There can be some common setup for each test. 26 | }); 27 | 28 | it("creates a game that shows up as expected in recent games", async function() { 29 | await contract.createOrJoinGame(); 30 | const recentGames = await contract.getRecentGames(); 31 | console.log("aloha recentGames", recentGames); 32 | expect(recentGames.length).toBe(1); 33 | expect(recentGames[0]['game']['player1']).toBe(nearConfig.contractName); 34 | }); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /src/config.js: -------------------------------------------------------------------------------- 1 | const CONTRACT_NAME = process.env.CONTRACT_NAME || 'near-chess-devnet'; 2 | 3 | function getConfig(env) { 4 | switch (env) { 5 | 6 | case 'production': 7 | case 'development': 8 | return { 9 | networkId: 'default', 10 | nodeUrl: 'https://rpc.nearprotocol.com', 11 | contractName: CONTRACT_NAME, 12 | walletUrl: 'https://wallet.nearprotocol.com', 13 | helperUrl: 'https://near-contract-helper.onrender.com', 14 | }; 15 | case 'staging': 16 | return { 17 | networkId: 'staging', 18 | nodeUrl: 'https://staging-rpc.nearprotocol.com/', 19 | contractName: CONTRACT_NAME, 20 | walletUrl: 'https://near-wallet-staging.onrender.com', 21 | helperUrl: 'https://near-contract-helper-staging.onrender.com', 22 | }; 23 | case 'local': 24 | return { 25 | networkId: 'local', 26 | nodeUrl: 'http://localhost:3030', 27 | keyPath: `${process.env.HOME}/.near/validator_key.json`, 28 | walletUrl: 'http://localhost:4000/wallet', 29 | contractName: CONTRACT_NAME, 30 | }; 31 | case 'test': 32 | case 'test-remote': 33 | case 'ci': 34 | return { 35 | networkId: 'shared-test', 36 | nodeUrl: 'http://shared-test.nearprotocol.com:3030', 37 | contractName: CONTRACT_NAME, 38 | masterAccount: 'test.near', 39 | }; 40 | case 'ci-staging': 41 | return { 42 | networkId: 'shared-test-staging', 43 | nodeUrl: 'http://staging-shared-test.nearprotocol.com:3030', 44 | contractName: CONTRACT_NAME, 45 | masterAccount: 'test.near', 46 | }; 47 | case 'tatooine': 48 | return { 49 | networkId: 'tatooine', 50 | nodeUrl: 'https://rpc.tatooine.nearprotocol.com', 51 | contractName: CONTRACT_NAME, 52 | walletUrl: 'https://wallet.tatooine.nearprotocol.com', 53 | }; 54 | default: 55 | throw Error(`Unconfigured environment '${env}'. Can be configured in src/config.js.`); 56 | } 57 | } 58 | 59 | module.exports = getConfig; 60 | -------------------------------------------------------------------------------- /assembly/main.ts: -------------------------------------------------------------------------------- 1 | // @nearfile 2 | 3 | import { context, storage, logging } from "near-sdk-as"; 4 | import { Game, GameWithId } from "./model"; 5 | 6 | // --- contract code goes below 7 | 8 | export function getRecentGames(): Array { 9 | let lastId = storage.getSome('lastId'); 10 | let games = new Array(); 11 | for (let id = lastId; id + 10 > lastId && id > 0; --id) { 12 | let game = new GameWithId(); 13 | game.id = id; 14 | game.game = getGame(id); 15 | games.push(game); 16 | } 17 | return games; 18 | } 19 | 20 | export function giveUpCurrentGame(): void { 21 | let gameId = getCurrentGame(context.sender); 22 | if (gameId == 0) { 23 | return; 24 | } 25 | let game = getGame(gameId); 26 | if (game.outcome != null || game.player2 == null) { 27 | return; 28 | } 29 | game.outcome = "Player " + context.sender + " gave up"; 30 | setGame(gameId, game); 31 | } 32 | 33 | export function createOrJoinGame(): void { 34 | giveUpCurrentGame(); 35 | let lastId = storage.getPrimitive('lastId', 0); 36 | let gameKey: string; 37 | let game: Game | null = null; 38 | if (lastId > 0) { 39 | game = getGame(lastId); 40 | if (game.player2) { 41 | game = null; 42 | } else { 43 | if (game.player1 == context.sender) { 44 | return; 45 | } 46 | game.player2 = context.sender; 47 | } 48 | } 49 | if (game == null) { 50 | game = new Game(); 51 | lastId++; 52 | storage.set('lastId', lastId); 53 | gameKey = getGameKey(lastId); 54 | game.player1 = context.sender; 55 | } 56 | setGame(lastId, game); 57 | // TODO: Make it possible to return result from method to avoid this 58 | logging.log("sender: " + context.sender); 59 | storage.set("gameId:" + context.sender, lastId); 60 | } 61 | 62 | export function getCurrentGame(player: string): u64 { 63 | return storage.getPrimitive("gameId:" + player, 0); 64 | } 65 | 66 | export function getGame(gameId: u64): Game { 67 | return storage.getSome(getGameKey(gameId)); 68 | } 69 | 70 | function setGame(gameId: u64, game: Game): void { 71 | storage.set(getGameKey(gameId), game); 72 | } 73 | 74 | export function makeMove(gameId: u64, fen: string): void { 75 | let game = getGame(gameId); 76 | assert(game.outcome == null, "Game over"); 77 | let turn = getCurrentTurn(game.fen); 78 | let nextTurn = getCurrentTurn(fen); 79 | let validTurn = 80 | nextTurn != turn && ( 81 | (context.sender == game.player1 && turn == 'w') || 82 | (context.sender == game.player2 && turn == 'b')); 83 | logging.log("turn " + turn); 84 | logging.log("nextTurn " + nextTurn); 85 | logging.log("sender " + context.sender); 86 | 87 | assert(validTurn, 'Wrong side to make move'); 88 | // TODO: Validate chess rules 89 | game.fen = fen; 90 | setGame(gameId, game); 91 | } 92 | 93 | function getGameKey(gameId: u64): string { 94 | return 'game:' + gameId.toString(); 95 | } 96 | 97 | function getCurrentTurn(fen: string): string { 98 | // TODO: Pull all of chess.js working with fen 99 | var tokens = fen.split(' '); 100 | var position = tokens[0]; 101 | let turn = tokens[1]; 102 | return turn; 103 | } 104 | -------------------------------------------------------------------------------- /setup.js: -------------------------------------------------------------------------------- 1 | // This file is not required when running the project locally. Its purpose is to set up the 2 | // AssemblyScript compiler when a new project has been loaded in WebAssembly Studio. 3 | 4 | // Path manipulation lifted from https://gist.github.com/creationix/7435851 5 | 6 | // Joins path segments. Preserves initial "/" and resolves ".." and "." 7 | // Does not support using ".." to go above/outside the root. 8 | // This means that join("foo", "../../bar") will not resolve to "../bar" 9 | function join(/* path segments */) { 10 | // Split the inputs into a list of path commands. 11 | var parts = []; 12 | for (var i = 0, l = arguments.length; i < l; i++) { 13 | parts = parts.concat(arguments[i].split("/")); 14 | } 15 | // Interpret the path commands to get the new resolved path. 16 | var newParts = []; 17 | for (i = 0, l = parts.length; i < l; i++) { 18 | var part = parts[i]; 19 | // Remove leading and trailing slashes 20 | // Also remove "." segments 21 | if (!part || part === ".") continue; 22 | // Interpret ".." to pop the last segment 23 | if (part === "..") newParts.pop(); 24 | // Push new path segments. 25 | else newParts.push(part); 26 | } 27 | // Preserve the initial slash if there was one. 28 | if (parts[0] === "") newParts.unshift(""); 29 | // Turn back into a single string path. 30 | return newParts.join("/") || (newParts.length ? "/" : "."); 31 | } 32 | 33 | // A simple function to get the dirname of a path 34 | // Trailing slashes are ignored. Leading slash is preserved. 35 | function dirname(path) { 36 | return join(path, ".."); 37 | } 38 | 39 | require.config({ 40 | paths: { 41 | "binaryen": "https://cdn.jsdelivr.net/gh/AssemblyScript/binaryen.js@e41ec5c177e3d2cacccd4ccb1877ae29a7352dc1/index", 42 | "assemblyscript": "https://cdn.jsdelivr.net/gh/nearprotocol/assemblyscript@a4aa1a5/dist/assemblyscript", 43 | "assemblyscript/bin/asc": "https://cdn.jsdelivr.net/gh/nearprotocol/assemblyscript@a4aa1a5/dist/asc", 44 | } 45 | }); 46 | logLn("Loading AssemblyScript compiler ..."); 47 | require(["assemblyscript/bin/asc"], asc => { 48 | monaco.languages.typescript.typescriptDefaults.addExtraLib(asc.definitionFiles.assembly); 49 | asc.runningInStudio = true; 50 | asc.main = (main => (args, options, fn) => { 51 | if (typeof options === "function") { 52 | fn = options; 53 | options = undefined; 54 | } 55 | 56 | return main(args, options || { 57 | stdout: asc.createMemoryStream(), 58 | stderr: asc.createMemoryStream(logLn), 59 | readFile: (filename, baseDir) => { 60 | let path = join(baseDir, filename); 61 | console.log("readFile", path); 62 | if (path.startsWith("out/") && path.indexOf(".near.ts") == -1) { 63 | path = path.replace(/^out/, baseDir ); 64 | console.log("path", path); 65 | } else if (path.startsWith(baseDir) && path.indexOf(".near.ts") != -1) { 66 | path = path.replace(new RegExp("^" + baseDir), "out"); 67 | console.log("path", path); 68 | } 69 | const file = project.getFile(path); 70 | return file ? file.data : null; 71 | }, 72 | writeFile: (filename, contents) => { 73 | const name = filename.startsWith("../") ? filename.substring(3) : filename; 74 | const type = fileTypeForExtension(name.substring(name.lastIndexOf(".") + 1)); 75 | project.newFile(name, type, true).setData(contents); 76 | }, 77 | listFiles: () => [] 78 | }, fn); 79 | })(asc.main); 80 | logLn("AssemblyScript compiler is ready!"); 81 | }); 82 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 22 | 23 | 24 | 45 |
46 |
47 |
48 |
49 |

Show us your moves

50 |

Pease sign-in to start playing. Don't worry – it's just few clicks in a browser, you don't have to install anything.

51 | 52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |

61 |

62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 | 72 | 73 |
74 |
75 |
76 |
77 |

Recent games

78 |
79 |
80 |
81 |
82 | 83 |
84 |
85 |
86 |
87 |
88 |
89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | import "regenerator-runtime/runtime"; 2 | 3 | import * as nearlib from "nearlib" 4 | import getConfig from "./config" 5 | 6 | let nearConfig = getConfig(process.env.NODE_ENV || "development") 7 | 8 | async function doInitContract() { 9 | window.near = await nearlib.connect(Object.assign(nearConfig, { deps: { keyStore: new nearlib.keyStores.BrowserLocalStorageKeyStore() }})); 10 | window.walletAccount = new nearlib.WalletAccount(window.near); 11 | 12 | // Getting the Account ID. If unauthorized yet, it's just empty string. 13 | window.accountId = window.walletAccount.getAccountId(); 14 | 15 | // Initializing our contract APIs by contract name and configuration. 16 | // NOTE: This configuration only needed while NEAR is still in development 17 | window.contract = await near.loadContract(nearConfig.contractName, { 18 | // View methods are read only. They don't modify the state, but usually return some value. 19 | viewMethods: ["getCurrentGame", "getGame", "getRecentGames"], 20 | // Change methods can modify the state. But you don't receive the returned value when called. 21 | changeMethods: ["createOrJoinGame", "makeMove", "giveUpCurrentGame"], 22 | // Sender is the account ID to initialize transactions. 23 | sender: window.accountId, 24 | }); 25 | 26 | // Once everything is ready, we can start using contract 27 | return doWork(); 28 | } 29 | 30 | // Using initialized contract 31 | async function doWork() { 32 | // Based on whether you've authorized, checking which flow we should go. 33 | if (!window.walletAccount.isSignedIn()) { 34 | signedOutFlow(); 35 | } else { 36 | signedInFlow(); 37 | } 38 | } 39 | 40 | // Function that initializes the signIn button using WalletAccount 41 | function signedOutFlow() { 42 | // Displaying the signed out flow elements. 43 | $('.signed-out-flow').removeClass('d-none'); 44 | // Adding an event to a sing-in button. 45 | $('#sign-in-button').click(() => { 46 | window.walletAccount.requestSignIn( 47 | // The contract name that would be authorized to be called by the user's account. 48 | nearConfig.contractName, 49 | // This is the app name. It can be anything. 50 | 'NEAR Chess', 51 | // We can also provide URLs to redirect on success and failure. 52 | // The current URL is used by default. 53 | ); 54 | }); 55 | } 56 | 57 | // Main function for the signed-in flow (already authorized by the wallet). 58 | function signedInFlow() { 59 | // Displaying the signed in flow elements. 60 | $('.signed-in-flow').removeClass('d-none'); 61 | 62 | // Displaying current account name. 63 | document.getElementById('account-id').innerText = window.accountId; 64 | 65 | document.querySelector('.new-game').addEventListener('click', () => { 66 | newGame().catch(console.error); 67 | }); 68 | 69 | document.querySelector('.give-up').addEventListener('click', () => { 70 | giveUp().catch(console.error); 71 | }); 72 | 73 | document.querySelector('.get-recent-games').addEventListener('click', () => { 74 | loadRecentGames().catch(console.error); 75 | }); 76 | 77 | document.getElementById('sign-out-button').addEventListener('click', () => { 78 | walletAccount.signOut(); 79 | // Forcing redirect. 80 | window.location.replace(window.location.origin + window.location.pathname); 81 | }); 82 | 83 | loadGame().catch(console.error); 84 | loadRecentGames().catch(console.error); 85 | 86 | } 87 | 88 | async function loadRecentGames() { 89 | let recentGames = await window.contract.getRecentGames(); 90 | $("#recent-games").empty(); 91 | recentGames.forEach(game => { 92 | let gameEl = $(`
93 |
94 |
95 |

${game.game.player1}

96 |

${game.game.player2 || "Waiting for player to join..."}

97 |
98 |
`); 99 | $("#recent-games").append(gameEl); 100 | let board = ChessBoard(gameEl.find(".board")[0], { 101 | pieceTheme: 'http://chessboardjs.com/img/chesspieces/alpha/{piece}.png', 102 | showNotation: false 103 | }); 104 | board.position(game.game.fen, false); 105 | // TODO: Is this detached when element removed? 106 | $(window).resize(board.resize); 107 | }); 108 | } 109 | 110 | let serverGame; 111 | let currentGameId; 112 | let playerSide; 113 | async function loadGame(gameId) { 114 | if (gameId) { 115 | currentGameId = gameId; 116 | } else if (!currentGameId) { 117 | currentGameId = await window.contract.getCurrentGame({player: window.accountId}); 118 | } 119 | if (!currentGameId) { 120 | return; 121 | } 122 | 123 | console.log("currentGameId", currentGameId); 124 | serverGame = await window.contract.getGame({gameId: currentGameId}); 125 | console.log("game", serverGame); 126 | playerSide = null; 127 | if (serverGame.player1 == window.accountId) { 128 | playerSide = "w"; 129 | } 130 | if (serverGame.player2 == window.accountId) { 131 | playerSide = "b"; 132 | } 133 | updateServerStatus(); 134 | 135 | if (game.fen() != serverGame.fen) { 136 | game.load(serverGame.fen); 137 | updateBoard(); 138 | } 139 | 140 | if ((game.turn() != playerSide || !serverGame.player2) && serverGame.outcome == null) { 141 | setTimeout(() => loadGame().catch(console.error), 3000); 142 | } 143 | } 144 | 145 | async function newGame() { 146 | await window.contract.createOrJoinGame(); 147 | loadRecentGames().catch(console.error); 148 | currentGameId = 0; 149 | await loadGame(); 150 | } 151 | 152 | async function giveUp() { 153 | await window.contract.giveUpCurrentGame(); 154 | await loadGame(); 155 | } 156 | 157 | let board; 158 | let game = new Chess(); 159 | game.clear(); 160 | 161 | // do not pick up pieces if the game is over 162 | // only pick up pieces for the side to move 163 | var onDragStart = function(source, piece, position, orientation) { 164 | if (game.game_over() === true || 165 | (game.turn() === 'w' && piece.search(/^b/) !== -1) || 166 | (game.turn() === 'b' && piece.search(/^w/) !== -1) || 167 | !playerSide || playerSide != game.turn()) { 168 | return false; 169 | } 170 | }; 171 | 172 | var onDrop = function(source, target) { 173 | if (!serverGame || !serverGame.player2 || serverGame.outcome != null) { 174 | return "snapback"; 175 | } 176 | // see if the move is legal 177 | var move = game.move({ 178 | from: source, 179 | to: target, 180 | promotion: 'q' // NOTE: always promote to a queen for example simplicity 181 | }); 182 | 183 | // illegal move 184 | if (move === null) return 'snapback'; 185 | 186 | updateStatus(); 187 | 188 | // Make move on chain 189 | window.contract.makeMove({gameId: currentGameId, fen: game.fen()}).finally(loadGame); 190 | }; 191 | 192 | // update the board position after the piece snap 193 | // for castling, en passant, pawn promotion 194 | var onSnapEnd = function() { 195 | board.position(game.fen()); 196 | }; 197 | 198 | function updateBoard() { 199 | board.position(game.fen()); 200 | updateStatus(); 201 | } 202 | 203 | function getStatusText() { 204 | let moveColor = game.turn() === 'b' ? 'Black' : 'White'; 205 | 206 | // checkmate? 207 | if (game.in_checkmate() === true) { 208 | return 'Game over, ' + moveColor + ' is in checkmate.'; 209 | } 210 | 211 | // draw? 212 | else if (game.in_draw() === true) { 213 | return 'Game over, drawn position'; 214 | } 215 | 216 | // game still on 217 | else { 218 | let status = moveColor + ' to move'; 219 | 220 | // check? 221 | if (game.in_check() === true) { 222 | return status + ', ' + moveColor + ' is in check'; 223 | } 224 | 225 | return status; 226 | } 227 | 228 | return ''; 229 | } 230 | 231 | function updateStatus() { 232 | $('.status').removeClass('d-none'); 233 | $('.status').text(getStatusText()); 234 | updateServerStatus(); 235 | } 236 | 237 | function getServerStatus() { 238 | if (!serverGame || !serverGame.player2) { 239 | return 'Waiting for player to join...'; 240 | } 241 | if (serverGame.outcome != null) { 242 | return serverGame.outcome; 243 | } 244 | if (!(playerSide == "w" || playerSide == "b")) { 245 | return `Watching ${serverGame.player1} vs ${serverGame.player2}`; 246 | } 247 | if (playerSide == "w") { 248 | return `Playing as white against ${serverGame.player2}`; 249 | } else { 250 | return `Playing as black against ${serverGame.player1}`; 251 | } 252 | } 253 | 254 | function updateServerStatus() { 255 | $('.server-status').removeClass('d-none'); 256 | $('.server-status').html(getServerStatus()); 257 | } 258 | 259 | var cfg = { 260 | pieceTheme: 'http://chessboardjs.com/img/chesspieces/alpha/{piece}.png', 261 | draggable: true, 262 | onDragStart: onDragStart, 263 | onDrop: onDrop, 264 | onSnapEnd: onSnapEnd 265 | }; 266 | board = ChessBoard('board', cfg); 267 | 268 | updateStatus(); 269 | 270 | 271 | // COMMON CODE BELOW: 272 | // Loads nearlib and this contract into window scope. 273 | 274 | window.nearInitPromise = doInitContract().catch(console.error); 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | --------------------------------------------------------------------------------