├── .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 | [](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 |
64 |
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 |
--------------------------------------------------------------------------------