├── .gitignore ├── css ├── chessground-examples │ ├── .gitignore │ ├── assets │ │ ├── images │ │ │ ├── board │ │ │ │ ├── 3d │ │ │ │ │ └── woodi.1024.png │ │ │ │ └── blue.svg │ │ │ └── pieces │ │ │ │ ├── staunton │ │ │ │ └── basic │ │ │ │ │ ├── Black-King.png │ │ │ │ │ ├── Black-Pawn.png │ │ │ │ │ ├── Black-Queen.png │ │ │ │ │ ├── Black-Rook.png │ │ │ │ │ ├── White-King.png │ │ │ │ │ ├── White-Pawn.png │ │ │ │ │ ├── White-Queen.png │ │ │ │ │ ├── White-Rook.png │ │ │ │ │ ├── Black-Bishop.png │ │ │ │ │ ├── Black-Knight.png │ │ │ │ │ ├── White-Bishop.png │ │ │ │ │ ├── White-Knight.png │ │ │ │ │ ├── Black-Bishop-Flipped.png │ │ │ │ │ ├── Black-Knight-Flipped.png │ │ │ │ │ ├── White-Bishop-Flipped.png │ │ │ │ │ └── White-Knight-Flipped.png │ │ │ │ └── merida │ │ │ │ ├── wR.svg │ │ │ │ ├── bP.svg │ │ │ │ ├── bR.svg │ │ │ │ ├── wP.svg │ │ │ │ ├── bQ.svg │ │ │ │ ├── bN.svg │ │ │ │ ├── bB.svg │ │ │ │ ├── wK.svg │ │ │ │ ├── wN.svg │ │ │ │ ├── wQ.svg │ │ │ │ ├── wB.svg │ │ │ │ └── bK.svg │ │ ├── examples.css │ │ ├── theme.css │ │ ├── 3d.css │ │ └── chessground.css │ ├── README.md │ ├── tsconfig.json │ ├── index.html │ ├── index.standalone.html │ ├── package.json │ ├── src │ │ ├── units │ │ │ ├── basics.ts │ │ │ ├── viewOnly.ts │ │ │ ├── fen.ts │ │ │ ├── unit.ts │ │ │ ├── perf.ts │ │ │ ├── zh.ts │ │ │ ├── in3d.ts │ │ │ ├── anim.ts │ │ │ ├── play.ts │ │ │ └── svg.ts │ │ ├── util.ts │ │ └── main.ts │ └── gulpfile.js ├── board │ └── blue3.jpg ├── largeBoard.css ├── board-theme.css ├── playerProfiles.css ├── nextPrevious.css ├── activity.css ├── invites.css ├── pgnExport.css ├── actionButtons.css ├── loading.css ├── historyArea.css ├── global.css ├── assets │ └── mono │ │ ├── P.svg │ │ ├── B.svg │ │ ├── N.svg │ │ ├── K.svg │ │ ├── R.svg │ │ └── Q.svg ├── miniboards.css ├── promote.css └── game.css ├── assets └── sounds │ ├── Move.mp3 │ └── Capture.mp3 ├── ui ├── notify │ ├── notify.js │ └── notifier.js ├── miniboard │ ├── timeAgo.js │ ├── miniboard_list.js │ └── miniboard.js ├── player │ ├── player_profile.js │ └── player_games.js ├── viewer_perspective │ └── user_location.js ├── export │ └── pgnExport.js ├── game │ ├── promote.js │ ├── PieceGraveyard.js │ ├── nextGameControl.js │ ├── gameHistory.js │ ├── gameActions.js │ └── gameView.js ├── settings │ └── settings-dialog.js ├── recent_activity │ ├── recent.js │ └── gameEnd.js ├── challenge │ └── challenge_control.js ├── pageLayout │ └── navigation.js └── invitations │ └── invitations.js ├── package.json ├── README.md ├── index.js └── LICENSE /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | -------------------------------------------------------------------------------- /css/chessground-examples/.gitignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | /*.d.ts 3 | node_modules 4 | npm-debug.log 5 | -------------------------------------------------------------------------------- /css/board/blue3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Happy0/ssb-chess-mithril/HEAD/css/board/blue3.jpg -------------------------------------------------------------------------------- /assets/sounds/Move.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Happy0/ssb-chess-mithril/HEAD/assets/sounds/Move.mp3 -------------------------------------------------------------------------------- /assets/sounds/Capture.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Happy0/ssb-chess-mithril/HEAD/assets/sounds/Capture.mp3 -------------------------------------------------------------------------------- /css/largeBoard.css: -------------------------------------------------------------------------------- 1 | .ssb-chess-board-large { 2 | width: 600px; 3 | height: 600px; 4 | margin: auto; 5 | } 6 | -------------------------------------------------------------------------------- /css/board-theme.css: -------------------------------------------------------------------------------- 1 | .ssb-chess-board-background-blue3 cg-board { 2 | background: url("./board/blue3.jpg"); 3 | background-size: cover; 4 | } 5 | -------------------------------------------------------------------------------- /css/chessground-examples/assets/images/board/3d/woodi.1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Happy0/ssb-chess-mithril/HEAD/css/chessground-examples/assets/images/board/3d/woodi.1024.png -------------------------------------------------------------------------------- /css/chessground-examples/assets/images/pieces/staunton/basic/Black-King.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Happy0/ssb-chess-mithril/HEAD/css/chessground-examples/assets/images/pieces/staunton/basic/Black-King.png -------------------------------------------------------------------------------- /css/chessground-examples/assets/images/pieces/staunton/basic/Black-Pawn.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Happy0/ssb-chess-mithril/HEAD/css/chessground-examples/assets/images/pieces/staunton/basic/Black-Pawn.png -------------------------------------------------------------------------------- /css/chessground-examples/assets/images/pieces/staunton/basic/Black-Queen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Happy0/ssb-chess-mithril/HEAD/css/chessground-examples/assets/images/pieces/staunton/basic/Black-Queen.png -------------------------------------------------------------------------------- /css/chessground-examples/assets/images/pieces/staunton/basic/Black-Rook.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Happy0/ssb-chess-mithril/HEAD/css/chessground-examples/assets/images/pieces/staunton/basic/Black-Rook.png -------------------------------------------------------------------------------- /css/chessground-examples/assets/images/pieces/staunton/basic/White-King.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Happy0/ssb-chess-mithril/HEAD/css/chessground-examples/assets/images/pieces/staunton/basic/White-King.png -------------------------------------------------------------------------------- /css/chessground-examples/assets/images/pieces/staunton/basic/White-Pawn.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Happy0/ssb-chess-mithril/HEAD/css/chessground-examples/assets/images/pieces/staunton/basic/White-Pawn.png -------------------------------------------------------------------------------- /css/chessground-examples/assets/images/pieces/staunton/basic/White-Queen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Happy0/ssb-chess-mithril/HEAD/css/chessground-examples/assets/images/pieces/staunton/basic/White-Queen.png -------------------------------------------------------------------------------- /css/chessground-examples/assets/images/pieces/staunton/basic/White-Rook.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Happy0/ssb-chess-mithril/HEAD/css/chessground-examples/assets/images/pieces/staunton/basic/White-Rook.png -------------------------------------------------------------------------------- /css/chessground-examples/assets/images/pieces/staunton/basic/Black-Bishop.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Happy0/ssb-chess-mithril/HEAD/css/chessground-examples/assets/images/pieces/staunton/basic/Black-Bishop.png -------------------------------------------------------------------------------- /css/chessground-examples/assets/images/pieces/staunton/basic/Black-Knight.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Happy0/ssb-chess-mithril/HEAD/css/chessground-examples/assets/images/pieces/staunton/basic/Black-Knight.png -------------------------------------------------------------------------------- /css/chessground-examples/assets/images/pieces/staunton/basic/White-Bishop.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Happy0/ssb-chess-mithril/HEAD/css/chessground-examples/assets/images/pieces/staunton/basic/White-Bishop.png -------------------------------------------------------------------------------- /css/chessground-examples/assets/images/pieces/staunton/basic/White-Knight.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Happy0/ssb-chess-mithril/HEAD/css/chessground-examples/assets/images/pieces/staunton/basic/White-Knight.png -------------------------------------------------------------------------------- /css/chessground-examples/assets/images/pieces/staunton/basic/Black-Bishop-Flipped.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Happy0/ssb-chess-mithril/HEAD/css/chessground-examples/assets/images/pieces/staunton/basic/Black-Bishop-Flipped.png -------------------------------------------------------------------------------- /css/chessground-examples/assets/images/pieces/staunton/basic/Black-Knight-Flipped.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Happy0/ssb-chess-mithril/HEAD/css/chessground-examples/assets/images/pieces/staunton/basic/Black-Knight-Flipped.png -------------------------------------------------------------------------------- /css/chessground-examples/assets/images/pieces/staunton/basic/White-Bishop-Flipped.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Happy0/ssb-chess-mithril/HEAD/css/chessground-examples/assets/images/pieces/staunton/basic/White-Bishop-Flipped.png -------------------------------------------------------------------------------- /css/chessground-examples/assets/images/pieces/staunton/basic/White-Knight-Flipped.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Happy0/ssb-chess-mithril/HEAD/css/chessground-examples/assets/images/pieces/staunton/basic/White-Knight-Flipped.png -------------------------------------------------------------------------------- /css/chessground-examples/README.md: -------------------------------------------------------------------------------- 1 | Usage examples for [lichess' chessground](https://github.com/ornicar/chessground). 2 | 3 | ``` 4 | yarn install 5 | gulp dev 6 | http-server (or any other local http server of your choice) 7 | ``` 8 | 9 | Then browse http://127.0.0.1:8080 10 | -------------------------------------------------------------------------------- /css/playerProfiles.css: -------------------------------------------------------------------------------- 1 | .ssb-chess-player-profile { 2 | 3 | } 4 | 5 | .ssb-chess-profile-game-summary { 6 | 7 | } 8 | 9 | .ssb-chess-player-finished-games { 10 | width: 100%; 11 | } 12 | 13 | .ssb-chess-player-finished-games-scroller { 14 | width: 100%; 15 | display: flex; 16 | flex-direction: column; 17 | } 18 | -------------------------------------------------------------------------------- /css/nextPrevious.css: -------------------------------------------------------------------------------- 1 | .ssb-chess-next-previous-buttons-container { 2 | display: flex; 3 | margin: auto; 4 | width: 200px; 5 | padding-top: 10px; 6 | } 7 | 8 | .ssb-chess-next-previous-button { 9 | margin: 5px; 10 | font-size: 20px; 11 | } 12 | 13 | .ssb-chess-next-previous-button-hidden { 14 | visibility: hidden; 15 | } -------------------------------------------------------------------------------- /css/activity.css: -------------------------------------------------------------------------------- 1 | .ssb-chess-game-notifications { 2 | width: 600px; 3 | margin: auto; 4 | } 5 | 6 | .ssb-chess-game-end-notification { 7 | display: flex; 8 | } 9 | 10 | .ssb-chess-game-activity-notification-text { 11 | margin-top: 10%; 12 | flex: 2; 13 | text-align: center; 14 | vertical-align: middle; 15 | line-height: 90px; /* the same as your div height */ 16 | } 17 | -------------------------------------------------------------------------------- /css/invites.css: -------------------------------------------------------------------------------- 1 | .ssb-chess-invites-section-title { 2 | font-size: 20px; 3 | margin: 5px; 4 | text-align: center; 5 | } 6 | 7 | .ssb-chess-challenge-control { 8 | margin: 20px; 9 | text-align: center; 10 | } 11 | 12 | #ssb-chess-challenge-control-select { 13 | width: 150px; 14 | } 15 | 16 | .ssb-chess-challenge-send-button { 17 | margin: auto; 18 | margin-top: 10px; 19 | padding: 5px; 20 | }; 21 | -------------------------------------------------------------------------------- /css/chessground-examples/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": [ 3 | "src/*.ts" 4 | ], 5 | "compilerOptions": { 6 | "outDir": "./dist", 7 | "noImplicitAny": false, 8 | "strictNullChecks": true, 9 | "noUnusedLocals": true, 10 | "noEmitOnError": false, 11 | "alwaysStrict": true, 12 | "noImplicitReturns": true, 13 | "noImplicitThis": true, 14 | "noUnusedParameters": true, 15 | "target": "es6" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /css/pgnExport.css: -------------------------------------------------------------------------------- 1 | .ssb-chess-import-pgn-advice { 2 | width: 600px; 3 | margin-left: auto; 4 | margin-right: auto; 5 | padding: 10px; 6 | } 7 | 8 | .ssb-chess-pgn-text-box-container { 9 | margin-left: auto; 10 | margin-right: auto; 11 | width: 400px; 12 | height: 400px; 13 | padding: 10px; 14 | } 15 | 16 | .ssb-chess-pgn-text-box { 17 | width: 400px; 18 | height: 400px; 19 | } 20 | 21 | .ssb-chess-pgn-export-back-button { 22 | margin-left: auto; 23 | margin-right: auto; 24 | padding: 10px; 25 | margin-top: 20px; 26 | } 27 | -------------------------------------------------------------------------------- /css/actionButtons.css: -------------------------------------------------------------------------------- 1 | .ssb-game-actions { 2 | margin-left: auto; 3 | margin-right: auto; 4 | } 5 | 6 | .ssb-chess-resign-confirmation-prompt { 7 | text-align: center; 8 | } 9 | 10 | .ssb-chess-resign-confirmation-buttons { 11 | display: flex; 12 | flex-direction: row; 13 | } 14 | 15 | .ssb-chess-resign-confirmation-button { 16 | margin: 2px; 17 | } 18 | 19 | .ssb-game-action-button { 20 | width: 100%; 21 | margin: 2px; 22 | } 23 | 24 | .ssb-game-rematch-invite-text { 25 | text-align: center 26 | } 27 | 28 | .ssb-game-rematch-accept-button { 29 | margin-left: auto; 30 | margin-right: auto; 31 | } -------------------------------------------------------------------------------- /css/chessground-examples/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
11 | 12 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /css/chessground-examples/index.standalone.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 |
11 |
12 | 13 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /css/loading.css: -------------------------------------------------------------------------------- 1 | .ssb-chess-loading { 2 | background: #ff4d4d; 3 | display: flex; 4 | justify-content: center; 5 | margin-bottom: 10px; 6 | } 7 | 8 | .ssb-chess-loading-text { 9 | margin-right: 10px; 10 | font-size: 15px; 11 | } 12 | 13 | .ssb-chess-loading-hide { 14 | display: none; 15 | } 16 | 17 | .ssb-chess-loader { 18 | border: 2px solid #f3f3f3; /* Light grey */ 19 | border-top: 2px solid #3498db; /* Blue */ 20 | border-radius: 50%; 21 | width: 12px; 22 | height: 12px; 23 | animation: ssb-chess-spin 2s linear infinite; 24 | } 25 | 26 | @keyframes ssb-chess-spin { 27 | 0% { transform: rotate(0deg); } 28 | 100% { transform: rotate(360deg); } 29 | } 30 | -------------------------------------------------------------------------------- /css/chessground-examples/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "chessground-examples", 3 | "version": "6.0.1", 4 | "description": "chessground examples", 5 | "main": "dist/chessground-examples.js", 6 | "author": "Thibault Duplessis", 7 | "license": "GPL-3.0", 8 | "dependencies": { 9 | "chess.js": "^0.10", 10 | "chessground": "^7.6", 11 | "page": "^1.7", 12 | "snabbdom": "^0.7" 13 | }, 14 | "devDependencies": { 15 | "browserify": "^16", 16 | "gulp": "^4", 17 | "gulp-sourcemaps": "^2", 18 | "gulp-uglify": "^3", 19 | "tsify": "^3", 20 | "typescript": "^3", 21 | "vinyl-buffer": "^1", 22 | "vinyl-source-stream": "^2", 23 | "watchify": "^3" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /css/historyArea.css: -------------------------------------------------------------------------------- 1 | .ssb-chess-pgn-cell { 2 | 3 | } 4 | 5 | .ssb-chess-history-player-container { 6 | display: flex; 7 | justify-content: space-between; 8 | margin-bottom: 10px; 9 | } 10 | 11 | .ssb-chess-pgn-moves-list { 12 | height: 300px; 13 | overflow-y: scroll; 14 | } 15 | 16 | .ssb-chess-pgn-move { 17 | display: flex; 18 | justify-content: space-between; 19 | font-weight: bold; 20 | } 21 | 22 | .ssb-chess-history-player { 23 | text-align: center; 24 | } 25 | 26 | .ssb-chess-pgn-move-selected { 27 | color: #d85000!important; 28 | } 29 | 30 | .ssb-chess-status-text { 31 | font-style: italic; 32 | text-align: center; 33 | padding-left: 5px; 34 | padding-right: 5px; 35 | margin-top: 10px; 36 | } 37 | -------------------------------------------------------------------------------- /css/global.css: -------------------------------------------------------------------------------- 1 | .ssb-chess-nav-item { 2 | margin: 10px; 3 | text-transform: uppercase; 4 | } 5 | 6 | .ssb-chess-nav-count { 7 | margin-left: 2px; 8 | } 9 | 10 | .ssb-chess-container { 11 | height: 100%; 12 | width: 100%; 13 | overflow: scroll; 14 | } 15 | 16 | #ssb-chess-settings-dialog { 17 | background-color: white; 18 | position: absolute; 19 | z-index: 500; 20 | top: 30%; 21 | left: 50%; 22 | height: 100px; 23 | width: 300px; 24 | } 25 | 26 | #ssb-chess-dialog-checkbox-container { 27 | margin: 5px; 28 | text-align: center; 29 | } 30 | 31 | #ssb-chess-settings-dialog-close { 32 | margin: auto; 33 | display: block; 34 | } 35 | 36 | #ssb-chess-settings-nav-item { 37 | float: right; 38 | margin-right: 10px; 39 | } 40 | -------------------------------------------------------------------------------- /css/chessground-examples/assets/images/board/blue.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /css/assets/mono/P.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | -------------------------------------------------------------------------------- /ui/notify/notify.js: -------------------------------------------------------------------------------- 1 | module.exports = () => { 2 | function displayNotification(text, onclick) { 3 | const options = { 4 | body: text, 5 | requireInteraction: true, 6 | }; 7 | 8 | const n = new Notification('Scuttlebutt Chess', options); 9 | 10 | if (onclick) { 11 | n.onclick = onclick; 12 | } 13 | } 14 | 15 | function showNotification(text, onclick) { 16 | const { permission } = Notification; 17 | if (permission === 'default') { 18 | Notification.requestPermission().then((result) => { 19 | if (result === 'granted') { 20 | displayNotification(text); 21 | } 22 | }); 23 | } else if (permission === 'granted') { 24 | displayNotification(text, onclick); 25 | } 26 | } 27 | 28 | return { 29 | showNotification, 30 | }; 31 | }; 32 | -------------------------------------------------------------------------------- /ui/miniboard/timeAgo.js: -------------------------------------------------------------------------------- 1 | const Value = require('mutant/value') 2 | const computed = require('mutant/computed') 3 | const human = require('human-time') 4 | 5 | // Taken from https://github.com/ssbc/patchcore/blob/7dc9300dea6a9c3b6b2fcf98b60b464b08b34624/lib/timeAgo.js 6 | module.exports = function timeAgo (timestamp) { 7 | var timer 8 | var value = Value(Time(timestamp)) 9 | return computed([value], (a) => a, { 10 | onListen: () => { 11 | timer = setInterval(refresh, 30e3) 12 | refresh() 13 | }, 14 | onUnlisten: () => { 15 | clearInterval(timer) 16 | } 17 | }, { 18 | idle: true 19 | }) 20 | 21 | function refresh () { 22 | value.set(Time(timestamp)) 23 | } 24 | } 25 | 26 | function Time (timestamp) { 27 | return human(new Date(timestamp)) 28 | .replace(/minute/, 'min') 29 | .replace(/second/, 'sec') 30 | } -------------------------------------------------------------------------------- /css/assets/mono/B.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 7 | 9 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /css/chessground-examples/src/units/basics.ts: -------------------------------------------------------------------------------- 1 | import { Chessground } from 'chessground'; 2 | import { Unit } from './unit'; 3 | 4 | export const defaults: Unit = { 5 | name: 'Default configuration', 6 | run(el) { 7 | return Chessground(el); 8 | } 9 | }; 10 | 11 | export const fromFen: Unit = { 12 | name: 'From FEN, from black POV', 13 | run(el) { 14 | return Chessground(el, { 15 | fen:'2r3k1/pp2Qpbp/4b1p1/3p4/3n1PP1/2N4P/Pq6/R2K1B1R w -', 16 | orientation: 'black' 17 | }); 18 | } 19 | }; 20 | 21 | export const lastMoveCrazyhouse: Unit = { 22 | name: 'Last move: crazyhouse', 23 | run(el) { 24 | const cg = Chessground(el); 25 | setTimeout(() => { 26 | cg.set({lastMove:['e2', 'e4']}); 27 | setTimeout(() => cg.set({lastMove:['g6']}), 1000); 28 | setTimeout(() => cg.set({lastMove:['e1']}), 2000); 29 | }); 30 | return cg; 31 | } 32 | }; 33 | -------------------------------------------------------------------------------- /css/miniboards.css: -------------------------------------------------------------------------------- 1 | .ssb-chess-miniboards { 2 | display: flex; 3 | flex-wrap: wrap; 4 | justify-content: center; 5 | } 6 | 7 | .ssb-chess-miniboard { 8 | margin: 2%; 9 | justify-content: center; 10 | } 11 | 12 | .cg-wrap.ssb-chess-board-small { 13 | width: 220px; 14 | height: 220px; 15 | } 16 | 17 | .cg-wrap.ssb-chess-board-medium { 18 | width: 320px; 19 | height: 320px; 20 | } 21 | 22 | .ssb-chess-miniboard-name { 23 | padding-left: 10px; 24 | padding-right: 10px; 25 | padding-top: 5px; 26 | } 27 | 28 | .ssb-chess-miniboard-time-ago { 29 | padding-top: 5px; 30 | font-style: italic; 31 | } 32 | 33 | .ssb-chess-miniboard-controls { 34 | display: flex; 35 | justify-content: center; 36 | } 37 | 38 | .ssb-chess-miniboard-control { 39 | margin: 5px; 40 | } 41 | 42 | .ssb-chess-miniboard-bottom { 43 | display: flex; 44 | justify-content: space-between; 45 | border: 1px solid grey; 46 | width: 320px; 47 | } 48 | -------------------------------------------------------------------------------- /css/assets/mono/N.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 8 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /css/chessground-examples/src/units/viewOnly.ts: -------------------------------------------------------------------------------- 1 | import { Chess } from 'chess.js'; 2 | import { Chessground } from 'chessground'; 3 | import { Unit } from './unit'; 4 | 5 | export const fullRandom: Unit = { 6 | name: 'View only: 2 random AIs', 7 | run(el) { 8 | const chess = new Chess(); 9 | const cg = Chessground(el, { 10 | viewOnly: true, 11 | animation: { 12 | duration: 1000 13 | }, 14 | movable: { 15 | free: false 16 | }, 17 | drawable: { 18 | visible: false 19 | } 20 | }); 21 | function makeMove() { 22 | if (!cg.state.dom.elements.board.offsetParent) return; 23 | const moves = chess.moves({verbose:true}); 24 | const move = moves[Math.floor(Math.random() * moves.length)]; 25 | chess.move(move.san); 26 | cg.move(move.from, move.to); 27 | setTimeout(makeMove, 700); 28 | } 29 | setTimeout(makeMove, 700); 30 | return cg; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /css/chessground-examples/assets/images/pieces/merida/wR.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /css/chessground-examples/assets/images/pieces/merida/bP.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /css/chessground-examples/assets/images/pieces/merida/bR.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ssb-chess-mithril", 3 | "version": "2.0.5", 4 | "description": "An ssb-chess client", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no tests\"", 8 | "release-notes": "node scripts/release-notes.js" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "https://www.github.com/happy0/ssb-chess-mithril" 13 | }, 14 | "author": "Gordon Martin", 15 | "license": "GPL-3.0", 16 | "dependencies": { 17 | "chessground": "7.6.8", 18 | "howler": "^2.0.15", 19 | "human-time": "0.0.2", 20 | "hyperscript": "^2.0.2", 21 | "lodash": "^4.17.11", 22 | "mithril": "1.1.7", 23 | "mutant": "3.29.0", 24 | "pubsub-js": "^1.7.0", 25 | "pull-scroll": "^1.0.9", 26 | "pull-stream": "^3.6.14", 27 | "ramda": "^0.25.0", 28 | "ssb-chess": "4.0.2", 29 | "ssb-embedded-chat": "2.0.1" 30 | }, 31 | "devDependencies": { 32 | "electron-builder": "^22.10.5", 33 | "electron": "^13.0.1" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /css/chessground-examples/src/units/fen.ts: -------------------------------------------------------------------------------- 1 | import { Unit } from './unit'; 2 | import { Chessground } from 'chessground'; 3 | import { Key } from 'chessground/types.d'; 4 | 5 | export const autoSwitch: Unit = { 6 | name: 'FEN: switch (puzzle bug)', 7 | run(cont) { 8 | const configs: Array<() => {fen: string; lastMove: Key[]}> = [() => { 9 | return { 10 | orientation: 'black', 11 | fen: 'rnbqkb1r/pp1ppppp/5n2/8/3N1B2/8/PPP1PPPP/RN1QKB1R b KQkq - 0 4', 12 | lastMove: ['f3', 'd4'] 13 | }; 14 | }, () => { 15 | return { 16 | orientation: 'white', 17 | fen: '2r2rk1/4bp1p/pp2p1p1/4P3/4bP2/PqN1B2Q/1P3RPP/2R3K1 w - - 1 23', 18 | lastMove: ['b4', 'b3'] 19 | }; 20 | }]; 21 | const cg = Chessground(cont, configs[0]()); 22 | const delay = 2000; 23 | let it = 0; 24 | function run() { 25 | if (!cg.state.dom.elements.board.offsetParent) return; 26 | cg.set(configs[++it % configs.length]()); 27 | setTimeout(run, delay); 28 | } 29 | setTimeout(run, delay); 30 | return cg; 31 | } 32 | }; 33 | -------------------------------------------------------------------------------- /css/chessground-examples/src/units/unit.ts: -------------------------------------------------------------------------------- 1 | import { Api } from 'chessground/api'; 2 | 3 | import * as basics from './basics' 4 | import * as play from './play' 5 | import * as perf from './perf' 6 | import * as zh from './zh' 7 | import * as anim from './anim' 8 | import * as svg from './svg' 9 | import * as in3d from './in3d' 10 | import * as fen from './fen' 11 | import * as viewOnly from './viewOnly' 12 | 13 | export interface Unit { 14 | name: string; 15 | run: (el: HTMLElement) => Api 16 | } 17 | 18 | export const list: Unit[] = [ 19 | basics.defaults, basics.fromFen, basics.lastMoveCrazyhouse, 20 | play.initial, play.castling, play.vsRandom, play.fullRandom, play.slowAnim, play.conflictingHold, 21 | perf.move, perf.select, 22 | anim.conflictingAnim, anim.withSameRole, anim.notSameRole, anim.whileHolding, 23 | zh.lastMoveDrop, 24 | svg.presetUserShapes, svg.changingShapesHigh, svg.changingShapesLow, svg.brushModifiers, svg.autoShapes, svg.visibleFalse, svg.enabledFalse, 25 | in3d.defaults, in3d.vsRandom, in3d.fullRandom, 26 | fen.autoSwitch, 27 | viewOnly.fullRandom 28 | ]; 29 | -------------------------------------------------------------------------------- /css/assets/mono/K.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 12 | 15 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /ui/player/player_profile.js: -------------------------------------------------------------------------------- 1 | const m = require('mithril'); 2 | const PlayerGames = require('./player_games'); 3 | 4 | module.exports = gameCtrl => ({ 5 | view: () => m('div'), 6 | oncreate: (vNode) => { 7 | if (vNode.attrs.playerId === this.playerId) { 8 | return; 9 | } 10 | 11 | this.playerId = atob(vNode.attrs.playerId); 12 | const playerGames = PlayerGames(gameCtrl).getScrollingFinishedGamesDom(this.playerId); 13 | 14 | vNode.dom.appendChild(playerGames); 15 | }, 16 | onupdate: (vNode) => { 17 | /* When we arrive at this route again we need to replace the contents of 18 | * the game history page if it's a different player since we last loaded 19 | * this route. Maybe there's a nicer way of doing this... */ 20 | if (vNode.attrs.playerId !== this.playerId) { 21 | this.playerId = atob(vNode.attrs.playerId); 22 | 23 | while (vNode.dom.firstChild) { 24 | vNode.dom.removeChild(vNode.dom.firstChild); 25 | } 26 | 27 | const playerGames = PlayerGames(gameCtrl).getScrollingFinishedGamesDom(this.playerId); 28 | vNode.dom.appendChild(playerGames); 29 | } 30 | }, 31 | }); 32 | -------------------------------------------------------------------------------- /css/assets/mono/R.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 8 | 11 | 14 | 17 | 20 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /ui/viewer_perspective/user_location.js: -------------------------------------------------------------------------------- 1 | module.exports = () => { 2 | function isVisible(elm) { 3 | // Thanks https://stackoverflow.com/a/33026481 =] 4 | if (!elm.offsetHeight && !elm.offsetWidth) { return false; } 5 | if (getComputedStyle(elm).visibility === 'hidden') { return false; } 6 | return true; 7 | } 8 | 9 | /** 10 | * Returns true if the user can currently see the chess app, and false otherwise. 11 | * The user might be in a different tab in the containing application, for example. 12 | */ 13 | function chessAppIsVisible() { 14 | const topLevelElementArr = document.getElementsByClassName('ssb-chess-container'); 15 | 16 | if (!topLevelElementArr || topLevelElementArr.length <= 0) { 17 | return false; 18 | } 19 | const element = topLevelElementArr[0]; 20 | 21 | return document.hasFocus() && isVisible(element); 22 | } 23 | 24 | /** 25 | * Applies the function argument if the chess app is not currently visible 26 | * to the user. 27 | */ 28 | function ifChessAppNotVisible(fn) { 29 | if (!chessAppIsVisible()) { 30 | fn(); 31 | } 32 | } 33 | 34 | return { 35 | chessAppIsVisible, 36 | ifChessAppNotVisible, 37 | }; 38 | }; 39 | -------------------------------------------------------------------------------- /css/chessground-examples/src/units/perf.ts: -------------------------------------------------------------------------------- 1 | import { Unit } from './unit'; 2 | import { Chessground } from 'chessground'; 3 | 4 | export const move: Unit = { 5 | name: 'Perf: piece move', 6 | run(cont) { 7 | const cg = Chessground(cont, { 8 | animation: { duration: 500 } 9 | }); 10 | const delay = 400; 11 | function run() { 12 | if (!cg.state.dom.elements.board.offsetParent) return; 13 | cg.move('e2', 'a8'); 14 | setTimeout(() => { 15 | cg.move('a8', 'e2'); 16 | setTimeout(run, delay); 17 | }, delay); 18 | } 19 | setTimeout(run, delay); 20 | return cg; 21 | } 22 | }; 23 | export const select: Unit = { 24 | name: 'Perf: square select', 25 | run(cont) { 26 | const cg = Chessground(cont, { 27 | movable: { 28 | free: false, 29 | dests: { 30 | e2: ['e3', 'e4', 'd3', 'f3'] 31 | } 32 | } 33 | }); 34 | const delay = 500; 35 | function run() { 36 | if (!cg.state.dom.elements.board.offsetParent) return; 37 | cg.selectSquare('e2'); 38 | setTimeout(() => { 39 | cg.selectSquare('d4'); 40 | setTimeout(run, delay); 41 | }, delay); 42 | } 43 | setTimeout(run, delay); 44 | return cg; 45 | } 46 | }; 47 | -------------------------------------------------------------------------------- /css/chessground-examples/src/units/zh.ts: -------------------------------------------------------------------------------- 1 | import { Unit } from './unit'; 2 | import { Chessground } from 'chessground'; 3 | import { Key } from 'chessground/types.d'; 4 | 5 | export const lastMoveDrop: Unit = { 6 | name: 'Crazyhouse: lastMove = drop', 7 | run(cont) { 8 | const configs: Array<() => {fen: string; lastMove: Key[]}> = [() => { 9 | return { 10 | fen: 'Bn2kb1r/p1p2ppp/4q3/2Pp4/3p1NP1/2B2n2/PPP2P1P/R2KqB1R/RNpp w k - 42 22', 11 | lastMove: ['e5', 'd4'] 12 | }; 13 | }, () => { 14 | return { 15 | fen: 'Bn2kb1r/p1p2ppp/4q3/2Pp4/3p1NP1/2B2n2/PPP2P1P/R2KqB1R/RNpp w k - 42 22', 16 | lastMove: ['f4'] 17 | }; 18 | }, () => { 19 | return { 20 | fen: 'Bn2kb1r/p1p2ppp/4q3/2Pp4/3p1NP1/2B2n2/PPP2P1P/R2KqB1R/RNpp w k - 42 22', 21 | lastMove: ['e1'] 22 | }; 23 | }]; 24 | const cg = Chessground(cont, configs[0]()); 25 | const delay = 2000; 26 | let it = 0; 27 | function run() { 28 | if (!cg.state.dom.elements.board.offsetParent) return; 29 | const config = configs[++it % configs.length]; 30 | console.log(config); 31 | cg.set(config()); 32 | setTimeout(run, delay); 33 | } 34 | setTimeout(run, delay); 35 | return cg; 36 | } 37 | }; 38 | 39 | -------------------------------------------------------------------------------- /css/chessground-examples/assets/examples.css: -------------------------------------------------------------------------------- 1 | /* Unrelated to chessground; only for the examples page */ 2 | body { 3 | background-image: linear-gradient(to bottom, #2c2c2c, #1a1a1a 116px); 4 | color: #b0b0b0; 5 | font-family: "Helvetica Neue",Helvetica,Arial,sans-serif; 6 | font-size: 11px; 7 | } 8 | #chessground-examples { 9 | margin: 30px 0 0 0; 10 | display: flex; 11 | flex-flow: row; 12 | } 13 | #chessground-examples menu { 14 | flex: 0 0 300px; 15 | margin: 0 15px 0 0; 16 | } 17 | #chessground-examples menu a { 18 | display: block; 19 | cursor: pointer; 20 | padding: 5px 15px; 21 | } 22 | #chessground-examples menu a:hover { 23 | background: #333; 24 | } 25 | #chessground-examples menu a.active { 26 | border-left: 5px solid #b0b0b0; 27 | } 28 | #chessground-examples section { 29 | display: inline-block; 30 | background: #404040; 31 | padding: 10px 12px 10px 12px; 32 | border-radius: 2px; 33 | } 34 | #chessground-examples section p { 35 | text-align: center; 36 | margin: 20px 0 0 0; 37 | } 38 | #chessground-examples control { 39 | margin-left: 20px; 40 | } 41 | #chessground-examples control .zoom { 42 | margin-top: 1em; 43 | display: block; 44 | } 45 | #chessground-examples control .zoom input { 46 | margin-left: 1em; 47 | width: 4em; 48 | } 49 | -------------------------------------------------------------------------------- /ui/export/pgnExport.js: -------------------------------------------------------------------------------- 1 | const m = require('mithril'); 2 | 3 | module.exports = (gameId, pgnString) => { 4 | function informationText() { 5 | return m('div', { class: 'ssb-chess-import-pgn-advice' }, [ 6 | m('div', 'A chess PGN can be exported to another program chess for computer assisted game analysis. '), 7 | m('span', "If you are online, I recommend lichess.org's analysis tools which can recommend stronger moves and show you your mistakes. "), 8 | m('span', 'You can import your PGN here: '), 9 | m('a', { href: 'https://lichess.org/paste' }, 'https://lichess.org/paste'), 10 | m('span', ". There are offline tools available but I haven't used any."), 11 | ]); 12 | } 13 | 14 | function pgnBox() { 15 | return m('div', { class: 'ssb-chess-pgn-text-box-container' }, 16 | m('textarea', { class: 'ssb-chess-pgn-text-box' }, pgnString)); 17 | } 18 | 19 | function goBackButton() { 20 | const goBackToGame = () => m.route.set(`/games/${btoa(gameId)}`); 21 | 22 | return m('button', { class: 'ssb-chess-pgn-export-back-button', title: 'Go back to game', onclick: goBackToGame }, 'Go back to game'); 23 | } 24 | 25 | return { 26 | view: () => m('div', { class: 'ssb-chess-pgn-export' }, [ 27 | informationText(), 28 | pgnBox(), 29 | goBackButton(), 30 | ]), 31 | }; 32 | }; 33 | -------------------------------------------------------------------------------- /css/assets/mono/Q.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 15 | 18 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /css/chessground-examples/src/util.ts: -------------------------------------------------------------------------------- 1 | import { Api } from 'chessground/api'; 2 | 3 | export function toDests(chess: any) { 4 | const dests = {}; 5 | chess.SQUARES.forEach(s => { 6 | const ms = chess.moves({square: s, verbose: true}); 7 | if (ms.length) dests[s] = ms.map(m => m.to); 8 | }); 9 | return dests; 10 | } 11 | 12 | export function toColor(chess: any) { 13 | return (chess.turn() === 'w') ? 'white' : 'black'; 14 | 15 | } 16 | 17 | export function playOtherSide(cg: Api, chess) { 18 | return (orig, dest) => { 19 | chess.move({from: orig, to: dest}); 20 | cg.set({ 21 | turnColor: toColor(chess), 22 | movable: { 23 | color: toColor(chess), 24 | dests: toDests(chess) 25 | } 26 | }); 27 | }; 28 | } 29 | 30 | export function aiPlay(cg: Api, chess, delay: number, firstMove: boolean) { 31 | return (orig, dest) => { 32 | chess.move({from: orig, to: dest}); 33 | setTimeout(() => { 34 | const moves = chess.moves({verbose:true}); 35 | const move = firstMove ? moves[0] : moves[Math.floor(Math.random() * moves.length)]; 36 | chess.move(move.san); 37 | cg.move(move.from, move.to); 38 | cg.set({ 39 | turnColor: toColor(chess), 40 | movable: { 41 | color: toColor(chess), 42 | dests: toDests(chess) 43 | } 44 | }); 45 | cg.playPremove(); 46 | }, delay); 47 | }; 48 | } 49 | -------------------------------------------------------------------------------- /css/chessground-examples/assets/theme.css: -------------------------------------------------------------------------------- 1 | /* 2 | * Board 3 | */ 4 | .blue .cg-wrap { 5 | background-image: url('images/board/blue.svg'); 6 | } 7 | 8 | .merida .cg-wrap piece.pawn.white { 9 | background-image: url('images/pieces/merida/wP.svg'); 10 | } 11 | .merida .cg-wrap piece.bishop.white { 12 | background-image: url('images/pieces/merida/wB.svg'); 13 | } 14 | .merida .cg-wrap piece.knight.white { 15 | background-image: url('images/pieces/merida/wN.svg'); 16 | } 17 | .merida .cg-wrap piece.rook.white { 18 | background-image: url('images/pieces/merida/wR.svg'); 19 | } 20 | .merida .cg-wrap piece.queen.white { 21 | background-image: url('images/pieces/merida/wQ.svg'); 22 | } 23 | .merida .cg-wrap piece.king.white { 24 | background-image: url('images/pieces/merida/wK.svg'); 25 | } 26 | .merida .cg-wrap piece.pawn.black { 27 | background-image: url('images/pieces/merida/bP.svg'); 28 | } 29 | .merida .cg-wrap piece.bishop.black { 30 | background-image: url('images/pieces/merida/bB.svg'); 31 | } 32 | .merida .cg-wrap piece.knight.black { 33 | background-image: url('images/pieces/merida/bN.svg'); 34 | } 35 | .merida .cg-wrap piece.rook.black { 36 | background-image: url('images/pieces/merida/bR.svg'); 37 | } 38 | .merida .cg-wrap piece.queen.black { 39 | background-image: url('images/pieces/merida/bQ.svg'); 40 | } 41 | .merida .cg-wrap piece.king.black { 42 | background-image: url('images/pieces/merida/bK.svg'); 43 | } 44 | -------------------------------------------------------------------------------- /css/promote.css: -------------------------------------------------------------------------------- 1 | #ssb-promotion-box-pieces { 2 | display: flex; 3 | flex-direction: row; 4 | background-color: white; 5 | } 6 | 7 | #ssb-promotion-box piece { 8 | background-size: 75px 75px; 9 | width: 75px; 10 | height: 75px; 11 | display: block; 12 | position: relative; 13 | } 14 | 15 | #ssb-promotion-box piece:hover { 16 | background-color: lightcyan; 17 | } 18 | 19 | #ssb-promotion-box piece.white.queen { 20 | background-image: url('chessground-examples/assets/images/pieces/merida/wQ.svg'); 21 | } 22 | 23 | #ssb-promotion-box piece.white.knight { 24 | background-image: url('chessground-examples/assets/images/pieces/merida/wN.svg'); 25 | } 26 | 27 | #ssb-promotion-box piece.white.bishop { 28 | background-image: url('chessground-examples/assets/images/pieces/merida/wB.svg'); 29 | } 30 | 31 | #ssb-promotion-box piece.white.rook { 32 | background-image: url('chessground-examples/assets/images/pieces/merida/wR.svg'); 33 | } 34 | 35 | 36 | #ssb-promotion-box piece.black.queen { 37 | background-image: url('chessground-examples/assets/images/pieces/merida/bQ.svg'); 38 | } 39 | 40 | #ssb-promotion-box piece.black.knight { 41 | background-image: url('chessground-examples/assets/images/pieces/merida/bN.svg'); 42 | } 43 | 44 | #ssb-promotion-box piece.black.bishop { 45 | background-image: url('chessground-examples/assets/images/pieces/merida/bB.svg'); 46 | } 47 | 48 | #ssb-promotion-box piece.black.rook { 49 | background-image: url('chessground-examples/assets/images/pieces/merida/bR.svg'); 50 | } 51 | -------------------------------------------------------------------------------- /css/game.css: -------------------------------------------------------------------------------- 1 | .ssb-chess-game-layout { 2 | margin-top: 20px; 3 | display: flex; 4 | } 5 | 6 | .ssb-chess-history-area { 7 | display: flex; 8 | flex-direction: column; 9 | width: 200px; 10 | margin: auto; 11 | justify-content: center; 12 | } 13 | 14 | .ssb-chess-chat { 15 | width: 300px; 16 | margin: auto; 17 | } 18 | 19 | .ssb-embedded-chat-input-box { 20 | width: 98%; 21 | border: 1px solid black; 22 | padding-left: 1px; 23 | padding-right: 1px; 24 | height: 15px; 25 | } 26 | 27 | .ssb-embedded-chat-messages { 28 | width: 99%; 29 | height: 430px; 30 | border: 1px black solid; 31 | } 32 | 33 | .ssb-embedded-chat-message { 34 | margin-left: 5px; 35 | margin-right: 5px; 36 | margin-bottom: 2px; 37 | } 38 | 39 | .ssb-embedded-chat-message-old { 40 | color: grey 41 | } 42 | 43 | .ssb-chess-graveyard { 44 | width: 200px; 45 | height: 50px; 46 | margin-top: 5px; 47 | margin-bottom: 5px; 48 | } 49 | 50 | .ssb-chess-graveyard-piece { 51 | margin: 2px; 52 | } 53 | 54 | mono-piece { 55 | width: 32px; 56 | height: 32px; 57 | display: inline-block; 58 | background-size: cover; 59 | } 60 | 61 | mono-piece.bishop { 62 | background-image: url("assets/mono/B.svg"); 63 | } 64 | 65 | mono-piece.pawn { 66 | background-image: url("assets/mono/P.svg"); 67 | } 68 | 69 | mono-piece.queen { 70 | background-image: url("assets/mono/Q.svg"); 71 | } 72 | 73 | mono-piece.knight { 74 | background-image: url("assets/mono/N.svg"); 75 | } 76 | 77 | mono-piece.rook { 78 | background-image: url("assets/mono/R.svg"); 79 | } 80 | -------------------------------------------------------------------------------- /css/chessground-examples/gulpfile.js: -------------------------------------------------------------------------------- 1 | const gulp = require('gulp'); 2 | const source = require('vinyl-source-stream'); 3 | const buffer = require('vinyl-buffer'); 4 | const colors = require('ansi-colors'); 5 | const logger = require('fancy-log'); 6 | const watchify = require('watchify'); 7 | const browserify = require('browserify'); 8 | const uglify = require('gulp-uglify'); 9 | const tsify = require('tsify'); 10 | 11 | const destination = gulp.dest('./dist'); 12 | const fileBaseName = 'chessground-examples'; 13 | 14 | const browserifyOpts = (debug) => ({ 15 | entries: ['src/main.ts'], 16 | standalone: 'ChessgroundExamples', 17 | debug: debug 18 | }); 19 | 20 | const prod = () => browserify(browserifyOpts(false)) 21 | .plugin(tsify) 22 | .bundle() 23 | .pipe(source(`${fileBaseName}.min.js`)) 24 | .pipe(buffer()) 25 | .pipe(uglify()) 26 | .pipe(destination); 27 | 28 | const dev = () => browserify(browserifyOpts(true)) 29 | .plugin(tsify) 30 | .bundle() 31 | .pipe(source(`${fileBaseName}.js`)) 32 | .pipe(destination); 33 | 34 | const watch = () => { 35 | 36 | const bundle = () => bundler 37 | .bundle() 38 | .on('error', error => logger.error(colors.red(error.message))) 39 | .pipe(source(`${fileBaseName}.js`)) 40 | .pipe(destination); 41 | 42 | const bundler = watchify( 43 | browserify(Object.assign({}, watchify.args, browserifyOpts(true))) 44 | .plugin(tsify) 45 | ).on('update', bundle).on('log', logger.info); 46 | 47 | return bundle(); 48 | }; 49 | 50 | gulp.task('prod', prod); 51 | gulp.task('dev', dev); 52 | gulp.task('default', watch); 53 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![A screenshot of ssb-chess](http://i.imgur.com/Xz9ovwX.png) 2 | 3 | Correspondence chess built on top of the scuttlebutt platform. More information about scuttlebutt here: https://staltz.com/an-off-grid-social-network.html and [https://www.scuttlebutt.nz/](https://www.scuttlebutt.nz/) 4 | 5 | It is built to allow it to be integrated into scuttlebutt viewers (such as [patchbay](https://www.github.com/ssbc/patchbay), [patchwork](https://www.github.com/ssbc/patchbay) using [depject](https://github.com/depject/depject) so that they can take care of things like discovering friends to play with, etc. 6 | 7 | ### Installation 8 | 9 | You can find a desktop app releases to run ssb-chess at [https://www.github.com/happy0/ssb-chess-electron](https://www.github.com/happy0/ssb-chess-electron). 10 | 11 | ### Libraries used 12 | * [ssb-chess](https://www.github.com/happy0/ssb-chess) is used for all the ssb-chess protocol logic (querying games, making moves, sending invites, etc) 13 | * [Mithriljs](https://mithril.js.org/) is used for rendering the pages. 14 | * [Chessground](https://github.com/ornicar/chessground) is used for the board and pieces widget and animating the moves. 15 | * [Embedded Chat](https://github.com/happy0/ssb-embedded-chat) is used for the chatroom to allow the players to chat during their game. 16 | 17 | ## Required ssb-server plugins 18 | 19 | SEe [https://github.com/Happy0/ssb-chess/blob/master/README.md#required-ssb-server-plugins](https://github.com/Happy0/ssb-chess/blob/master/README.md#required-ssb-server-plugins) for information about ssb-server plugins required to run this. 20 | -------------------------------------------------------------------------------- /ui/player/player_games.js: -------------------------------------------------------------------------------- 1 | const Scroller = require('pull-scroll'); 2 | const h = require('hyperscript'); 3 | const m = require('mithril'); 4 | const pull = require('pull-stream'); 5 | 6 | const Miniboard = require('../miniboard/miniboard'); 7 | 8 | module.exports = (gameCtrl) => { 9 | function renderFinishedGameSummary(gameSummary, playerId) { 10 | const dom = document.createElement('div'); 11 | dom.className = 'ssb-chess-profile-game-summary'; 12 | 13 | const gameSummaryObservable = gameCtrl.getGameCtrl().getSituationSummaryObservable(gameSummary.gameId); 14 | 15 | const miniboard = Miniboard(gameSummaryObservable, gameSummary, playerId); 16 | 17 | const board = m(miniboard); 18 | 19 | m.render(dom, board); 20 | 21 | return dom; 22 | } 23 | 24 | function getScrollingFinishedGamesDom(playerId) { 25 | const finishedGamesSource = gameCtrl.getPlayerCtrl().endedGamesSummariesSource(playerId); 26 | 27 | const content = h('div', { 28 | className: 'ssb-chess-player-finished-games-scroller', 29 | }); 30 | 31 | const scroller = h('div', { 32 | style: { 33 | 'overflow-y': 'scroll', 34 | position: 'fixed', 35 | bottom: '0px', 36 | top: '200px', 37 | width: '100%', 38 | }, 39 | }, content); 40 | 41 | pull(finishedGamesSource, 42 | Scroller(scroller, content, current => renderFinishedGameSummary(current, playerId))); 43 | 44 | return h('div', { 45 | className: 'ssb-chess-player-finished-games', 46 | }, scroller); 47 | } 48 | 49 | 50 | return { 51 | getScrollingFinishedGamesDom, 52 | }; 53 | }; 54 | -------------------------------------------------------------------------------- /ui/game/promote.js: -------------------------------------------------------------------------------- 1 | const m = require('mithril'); 2 | 3 | module.exports = (chessBoardDomElement, colour, column, onChoice) => { 4 | const roles = ['queen', 'rook', 'knight', 'bishop']; 5 | 6 | function renderPiece(role, cb) { 7 | return m('piece', { 8 | class: `${colour} ${role}`, 9 | onclick: () => cb(role), 10 | }); 11 | } 12 | 13 | const PromotionBox = (cb) => { 14 | const component = { 15 | view: () => m('div', { 16 | id: 'ssb-promotion-box', 17 | }, [ 18 | m('div', { 19 | id: 'ssb-promotion-box-pieces', 20 | }, roles.map(role => renderPiece(role, cb))), 21 | ]), 22 | }; 23 | 24 | return component; 25 | }; 26 | 27 | 28 | function columnLetterToNumberFromZero(columnLetter) { 29 | return columnLetter.codePointAt(0) - 97; 30 | } 31 | 32 | function renderPromotionOptionsOverlay() { 33 | const c = document.getElementsByClassName('ssb-chess-container')[0]; 34 | 35 | const prom = document.createElement('div'); 36 | 37 | const cb = (piece) => { 38 | c.removeChild(prom); 39 | onChoice(piece); 40 | }; 41 | 42 | const box = PromotionBox(cb); 43 | 44 | const left = document.getElementsByTagName("cg-board")[0].getBoundingClientRect().x + (2 * 75); 45 | const top = document.getElementsByTagName("cg-board")[0].getBoundingClientRect().y + (3 * 75) + 35; 46 | 47 | const promotionBox = m('div', { 48 | style: `z-index: 1000; position: absolute; left: ${left}px; top: ${top}px;`, 49 | }, m(box)); 50 | 51 | c.appendChild(prom); 52 | 53 | m.render(prom, promotionBox); 54 | } 55 | 56 | return { 57 | renderPromotionOptionsOverlay, 58 | }; 59 | }; 60 | -------------------------------------------------------------------------------- /ui/settings/settings-dialog.js: -------------------------------------------------------------------------------- 1 | const m = require('mithril'); 2 | 3 | module.exports = (settingsCtrl, onCloseDialog) => { 4 | function labelCheckbox(label, getSetting, onSelect) { 5 | const checkboxSettings = { 6 | type: 'checkbox', 7 | class: 'ssb-chess-dialog-checkbox', 8 | checked: getSetting(), 9 | onchange: (cb) => { 10 | onSelect(cb.srcElement.checked); 11 | }, 12 | }; 13 | 14 | if (getSetting() === true) { 15 | checkboxSettings.checked = true; 16 | } 17 | 18 | return m('div', { 19 | id: 'ssb-chess-dialog-checkbox-container', 20 | }, [ 21 | m('span', { class: 'ssb-chess-dialog-label' }, label), 22 | m('input', checkboxSettings), 23 | ]); 24 | } 25 | 26 | function closeDialog() { 27 | document.removeEventListener('click', windowClickListener, true); 28 | onCloseDialog(); 29 | } 30 | 31 | function windowClickListener(event) { 32 | const dialogElement = document.getElementById('ssb-chess-settings-dialog'); 33 | if (!dialogElement) { 34 | return; 35 | } 36 | 37 | if ((event.target != dialogElement) && !dialogElement.contains(event.target)) { 38 | closeDialog(); 39 | } 40 | } 41 | 42 | function closeButton() { 43 | return m('button', { href: '#', id: 'ssb-chess-settings-dialog-close', onclick: closeDialog }, 'Close'); 44 | } 45 | 46 | return { 47 | view: () => m('div', [ 48 | m('div', { class: 'ssb-chess-dialog-title' }, ''), 49 | labelCheckbox('Move confirmation', 50 | settingsCtrl.getMoveConfirmation, 51 | selected => settingsCtrl.setMoveConfirmation(selected)), 52 | labelCheckbox('Game sounds', settingsCtrl.getPlaySounds, settingsCtrl.setPlaySounds), 53 | closeButton(), 54 | ]), 55 | oncreate: () => { 56 | document.addEventListener('click', windowClickListener, true); 57 | }, 58 | }; 59 | }; 60 | -------------------------------------------------------------------------------- /css/chessground-examples/assets/images/pieces/merida/wP.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /css/chessground-examples/src/units/in3d.ts: -------------------------------------------------------------------------------- 1 | import { Chessground } from 'chessground'; 2 | import { Chess } from 'chess.js'; 3 | import { Unit } from './unit'; 4 | import { toDests, aiPlay } from '../util' 5 | 6 | export const defaults: Unit = { 7 | name: '3D theme', 8 | run(cont) { 9 | const el = wrapped(cont); 10 | const cg = Chessground(el, { 11 | addPieceZIndex: true, 12 | }); 13 | cg.redrawAll(); 14 | return cg; 15 | } 16 | } 17 | 18 | export const vsRandom: Unit = { 19 | name: '3D theme: play vs random AI', 20 | run(cont) { 21 | const el = wrapped(cont); 22 | const chess = new Chess(); 23 | const cg = Chessground(el, { 24 | orientation: 'black', 25 | addPieceZIndex: true, 26 | movable: { 27 | color: 'white', 28 | free: false, 29 | dests: toDests(chess) 30 | } 31 | }); 32 | cg.redrawAll(); 33 | cg.set({ 34 | movable: { 35 | events: { 36 | after: aiPlay(cg, chess, 1000, false) 37 | } 38 | } 39 | }); 40 | return cg; 41 | } 42 | }; 43 | 44 | export const fullRandom: Unit = { 45 | name: '3D theme: watch 2 random AIs', 46 | run(cont) { 47 | const el = wrapped(cont); 48 | const chess = new Chess(); 49 | const delay = 300; 50 | const cg = Chessground(el, { 51 | orientation: 'black', 52 | addPieceZIndex: true, 53 | movable: { 54 | free: false 55 | } 56 | }); 57 | cg.redrawAll(); 58 | function makeMove() { 59 | if (!cg.state.dom.elements.board.offsetParent) return; 60 | const moves = chess.moves({verbose:true}); 61 | const move = moves[Math.floor(Math.random() * moves.length)]; 62 | chess.move(move.san); 63 | cg.move(move.from, move.to); 64 | setTimeout(makeMove, delay); 65 | } 66 | setTimeout(makeMove, delay); 67 | return cg; 68 | } 69 | } 70 | 71 | function wrapped(cont: HTMLElement) { 72 | const el = document.createElement('div'); 73 | cont.className = 'in3d staunton'; 74 | cont.innerHTML = ''; 75 | cont.appendChild(el); 76 | return el; 77 | } 78 | -------------------------------------------------------------------------------- /ui/miniboard/miniboard_list.js: -------------------------------------------------------------------------------- 1 | const m = require('mithril'); 2 | const watch = require('mutant/watch'); 3 | const R = require('ramda'); 4 | const Miniboard = require('./miniboard'); 5 | 6 | /** 7 | * Takes an observable list of game summaries (non-observable inner objects) 8 | * and renders them into a page of miniboards. 9 | * Those miniboards create an observable to change the board when a move is 10 | * made. 11 | * 12 | * The list of games is updated if the gameSummaryListObs fires (e.g. if a 13 | * game ends or begins on a page of games a user is playing.) 14 | */ 15 | module.exports = (gameCtrl, gameSummaryListObs, ident) => { 16 | let gameSummaries = []; 17 | 18 | const oneMinuteMillseconds = 60000; 19 | 20 | this.ident = ident; 21 | 22 | let unlistenUpdates = null; 23 | 24 | let updateTimeAgoTimesTimer = null; 25 | 26 | function keepMiniboardsUpdated() { 27 | unlistenUpdates = watch(gameSummaryListObs, (summaries) => { 28 | if (hasDifferentGameIds(gameSummaries, summaries)) { 29 | // Only redraw if there is an additional game or a game has ended 30 | setTimeout(m.redraw); 31 | } 32 | 33 | gameSummaries = summaries; 34 | }); 35 | } 36 | 37 | function hasDifferentGameIds(oldSummaries, newSummaries) { 38 | const comparer = (oldSummary, newSummary) => oldSummary.gameId === newSummary.gameId; 39 | return R.symmetricDifferenceWith(comparer, oldSummaries, newSummaries).length !== 0; 40 | } 41 | 42 | return { 43 | view: () => m('div', { 44 | class: 'ssb-chess-miniboards', 45 | }, 46 | gameSummaries.map((summary) => { 47 | const situationObservable = gameCtrl.getGameCtrl().getSituationSummaryObservable(summary.gameId); 48 | 49 | return m( 50 | Miniboard(situationObservable, summary, this.ident), 51 | ); 52 | })), 53 | oncreate() { 54 | keepMiniboardsUpdated(); 55 | 56 | updateTimeAgoTimesTimer = setInterval( 57 | () => setTimeout(m.redraw), oneMinuteMillseconds, 58 | ); 59 | }, 60 | onremove: () => { 61 | clearInterval(updateTimeAgoTimesTimer); 62 | unlistenUpdates(); 63 | }, 64 | }; 65 | }; 66 | -------------------------------------------------------------------------------- /ui/notify/notifier.js: -------------------------------------------------------------------------------- 1 | const pull = require('pull-stream'); 2 | const onceTrue = require('mutant/once-true'); 3 | const notify = require('./notify')(); 4 | const userLocationUtils = require('../viewer_perspective/user_location')(); 5 | 6 | module.exports = (mainCtrl) => { 7 | const me = mainCtrl.getMyIdent(); 8 | 9 | const userGamesWatcher = mainCtrl.getUserGameWatcherCtrl(); 10 | 11 | function getOpponentName(situation, msg) { 12 | return situation.players[msg.value.author] ? situation.players[msg.value.author].name : ''; 13 | } 14 | 15 | function notifyIfRelevant(gameMsg) { 16 | if (!userLocationUtils.chessAppIsVisible()) { 17 | const gameId = gameMsg.value.content.type === 'chess_invite' ? gameMsg.key : gameMsg.value.content.root; 18 | 19 | if (!gameId) { 20 | return; 21 | } 22 | 23 | const situation = mainCtrl.getGameCtrl().getSituationObservable(gameId); 24 | 25 | onceTrue(situation, (gameSituation) => { 26 | const opponentName = getOpponentName(gameSituation, gameMsg); 27 | let notification; 28 | 29 | if (gameMsg.value.content && gameMsg.value.content.type === 'chess_invite' && gameMsg.value.author != me) { 30 | notification = `${opponentName} has invited you to a game`; 31 | notify.showNotification(notification); 32 | } else if (gameMsg.value.content.type === 'chess_move' && gameMsg.value.author != me) { 33 | notification = `It's your move in your game against ${opponentName}`; 34 | notify.showNotification(notification); 35 | } else if (gameMsg.value.content.type === 'chess_game_end' && gameMsg.value.author != me) { 36 | notification = `Your game with ${opponentName} ended`; 37 | notify.showNotification(notification); 38 | } else if (gameMsg.value.content.type === 'chess_invite_accept' && gameMsg.value.author != me) { 39 | notification = `${opponentName} has accepted your game invite`; 40 | notify.showNotification(notification); 41 | } 42 | }); 43 | } 44 | } 45 | 46 | function startNotifying() { 47 | const opts = { 48 | live: true, 49 | since: Date.now(), 50 | }; 51 | 52 | const gameUpdateStream = userGamesWatcher.chessMessagesForPlayerGames( 53 | mainCtrl.getMyIdent(), 54 | opts, 55 | ); 56 | 57 | pull(gameUpdateStream, pull.drain(msg => notifyIfRelevant(msg))); 58 | } 59 | 60 | return { 61 | startNotifying, 62 | }; 63 | }; 64 | -------------------------------------------------------------------------------- /ui/recent_activity/recent.js: -------------------------------------------------------------------------------- 1 | const m = require('mithril'); 2 | const watch = require('mutant/watch'); 3 | const gameEndActivity = require('./gameEnd'); 4 | 5 | /** 6 | * A component to render updates about chess games based on the supplied observable 7 | * list of scuttlebutt chess game messages. 8 | * 9 | * If a game is one the player is participating in, then the information text will 10 | * reflect this and use the word 'you.' 11 | * 12 | * @gameCtrl The main game controller. Used to retrieve further information to support 13 | * the rendering. 14 | * @recentGameMessagesObs An observable array of recent chess game scuttlebutt 15 | * messages with their associated game situation state. e.g. 16 | * 17 | * { 18 | * msg: ..., 19 | * situation: ... 20 | * } 21 | * 22 | * This list is expected to be a ring buffer (i.e. the least recent message 23 | * drops off the bottom when a new one arrives if the array has reached some 24 | * capacity). 25 | */ 26 | module.exports = (gameCtrl, recentGameMessagesObs) => { 27 | let messages = []; 28 | const watches = []; 29 | 30 | function renderGameEndMsg(entry) { 31 | return m('div', m(gameEndActivity(entry.msg, entry.situation, gameCtrl.getMyIdent()))); 32 | } 33 | 34 | const renderers = { 35 | chess_game_end: renderGameEndMsg, 36 | }; 37 | 38 | function renderMessage(entry) { 39 | const renderer = renderers[entry.msg.value.content.type]; 40 | 41 | if (renderer) { 42 | return m('div', { class: 'ssb-chess-game-activity-notification' }, renderer(entry)); 43 | } 44 | return m('div'); 45 | } 46 | 47 | function canRender(entry) { 48 | const { type } = entry.msg.value.content; 49 | return {}.hasOwnProperty.call(renderers, type); 50 | } 51 | 52 | function renderMessages() { 53 | return messages 54 | .filter(canRender) 55 | .map(renderMessage); 56 | } 57 | 58 | return { 59 | view: () => m('div', { class: 'ssb-chess-game-notifications' }, renderMessages()), 60 | oncreate: () => { 61 | const obs = watch(recentGameMessagesObs, 62 | (gameMessages) => { 63 | messages = gameMessages; 64 | 65 | if (messages && messages.length > 0) { 66 | gameCtrl.getRecentActivityCtrl().setLastseenMessage(messages[0].msg.timestamp); 67 | } 68 | 69 | m.redraw(); 70 | }); 71 | 72 | watches.push(obs); 73 | }, 74 | onremove: () => { 75 | watches.forEach(w => w()); 76 | }, 77 | }; 78 | }; 79 | -------------------------------------------------------------------------------- /css/chessground-examples/src/main.ts: -------------------------------------------------------------------------------- 1 | import { h, init } from 'snabbdom'; 2 | import { VNode } from 'snabbdom/vnode'; 3 | import { Api } from 'chessground/api'; 4 | import klass from 'snabbdom/modules/class'; 5 | import attributes from 'snabbdom/modules/attributes'; 6 | import listeners from 'snabbdom/modules/eventlisteners'; 7 | import * as page from 'page' 8 | import { Unit, list } from './units/unit' 9 | 10 | export function run(element: Element) { 11 | 12 | const patch = init([klass, attributes, listeners]); 13 | 14 | let unit: Unit, cg: Api, vnode: VNode; 15 | 16 | function redraw() { 17 | vnode = patch(vnode || element, render()); 18 | } 19 | 20 | function runUnit(vnode: VNode) { 21 | const el = vnode.elm as HTMLElement; 22 | el.className = 'cg-wrap'; 23 | cg = unit.run(el); 24 | window['cg'] = cg; // for messing up with it from the browser console 25 | } 26 | 27 | function setZoom(zoom: number) { 28 | const el = document.querySelector('.cg-wrap') as HTMLElement; 29 | if (el) { 30 | const px = `${zoom / 100 * 320}px`; 31 | el.style.width = px; 32 | el.style.height = px; 33 | document.body.dispatchEvent(new Event('chessground.resize')); 34 | } 35 | } 36 | 37 | function render() { 38 | return h('div#chessground-examples', [ 39 | h('menu', list.map((ex, id) => { 40 | return h('a', { 41 | class: { 42 | active: unit.name === ex.name 43 | }, 44 | on: { click: () => page(`/${id}`) } 45 | }, ex.name); 46 | })), 47 | h('section.blue.merida', [ 48 | h('div.cg-wrap', { 49 | hook: { 50 | insert: runUnit, 51 | postpatch: runUnit 52 | } 53 | }), 54 | h('p', unit.name) 55 | ]), 56 | h('control', [ 57 | h('button', { on: { click() { cg.toggleOrientation(); }}}, 'Toggle orientation'), 58 | h('label.zoom', [ 59 | 'Zoom', 60 | h('input', { 61 | attrs: { 62 | type: 'number', 63 | value: 100 64 | }, 65 | on: { 66 | change(e) { 67 | setZoom(parseFloat((e.target as HTMLInputElement).value)); 68 | } 69 | } 70 | }, 'Toggle orientation') 71 | ]) 72 | ]) 73 | ]); 74 | } 75 | 76 | page({ click: false, popstate: false, dispatch: false, hashbang: true }); 77 | page('/:id', ctx => { 78 | unit = list[parseInt(ctx.params.id) || 0]; 79 | redraw(); 80 | }); 81 | page(location.hash.slice(2) || '/0'); 82 | } 83 | -------------------------------------------------------------------------------- /css/chessground-examples/src/units/anim.ts: -------------------------------------------------------------------------------- 1 | import { Chessground } from 'chessground'; 2 | import { Unit } from './unit'; 3 | 4 | export const conflictingAnim: Unit = { 5 | name: 'Animation: conflict', 6 | run(el) { 7 | const cg = Chessground(el, { 8 | animation: { 9 | duration: 500 10 | }, 11 | fen: '8/8/5p2/4P3/4K3/8/8/8', 12 | turnColor: 'black', 13 | movable: { 14 | color: 'white', 15 | free: false 16 | } 17 | }); 18 | setTimeout(() => { 19 | cg.move('f6', 'e5'); 20 | cg.set({ 21 | turnColor: 'white', 22 | movable: { 23 | dests: {e4: ['e5', 'd5', 'f5']} 24 | } 25 | }); 26 | cg.playPremove(); 27 | }, 2000); 28 | return cg; 29 | } 30 | }; 31 | 32 | export const withSameRole: Unit = { 33 | name: 'Animation: same role', 34 | run(el) { 35 | const cg = Chessground(el, { 36 | animation: { 37 | duration: 2000 38 | }, 39 | highlight: { 40 | lastMove: false 41 | }, 42 | fen: '8/8/4p3/5p2/4B3/8/8/8', 43 | turnColor: 'white', 44 | }); 45 | setTimeout(() => { 46 | cg.move('e4', 'f5'); 47 | setTimeout(() => { 48 | cg.move('e6', 'f5'); 49 | }, 500); 50 | }, 200); 51 | return cg; 52 | } 53 | }; 54 | 55 | export const notSameRole: Unit = { 56 | name: 'Animation: different role', 57 | run(el) { 58 | const cg = Chessground(el, { 59 | animation: { 60 | duration: 2000 61 | }, 62 | highlight: { 63 | lastMove: false 64 | }, 65 | fen: '8/8/4n3/5p2/4P3/8/8/8', 66 | turnColor: 'white', 67 | }); 68 | setTimeout(() => { 69 | cg.move('e4', 'f5'); 70 | setTimeout(() => { 71 | cg.move('e6', 'f5'); 72 | }, 500); 73 | }, 200); 74 | return cg; 75 | } 76 | }; 77 | 78 | export const whileHolding: Unit = { 79 | name: 'Animation: while holding', 80 | run(el) { 81 | const cg = Chessground(el, { 82 | fen: '8/8/5p2/4P3/4K3/8/8/8', 83 | turnColor: 'black', 84 | animation: { 85 | duration: 5000 86 | }, 87 | movable: { 88 | color: 'white', 89 | free: false, 90 | showDests: false 91 | } 92 | }); 93 | setTimeout(() => { 94 | cg.move('f6', 'e5'); 95 | cg.set({ 96 | turnColor: 'white', 97 | movable: { 98 | dests: {e4: ['e5', 'd5', 'f5']} 99 | } 100 | }); 101 | cg.playPremove(); 102 | }, 3000); 103 | return cg; 104 | } 105 | }; 106 | -------------------------------------------------------------------------------- /css/chessground-examples/assets/images/pieces/merida/bQ.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/challenge/challenge_control.js: -------------------------------------------------------------------------------- 1 | const m = require('mithril'); 2 | const _ = require('ramda'); 3 | 4 | module.exports = (gameCtrl) => { 5 | let challengableFriends = []; 6 | 7 | function renderFriendOption(friend) { 8 | return m('option', { 9 | value: friend.ident, 10 | }, friend.displayName); 11 | } 12 | 13 | function compareAlphabetically(a, b) { 14 | if (a.displayName.toLowerCase() < b.displayName.toLowerCase()) return -1; 15 | if (a.displayName.toLowerCase() > b.displayName.toLowerCase()) return 1; 16 | return 0; 17 | } 18 | 19 | function renderFriendsdropDown() { 20 | 21 | return m('select', { 22 | id: 'ssb-chess-challenge-control-select', 23 | name: 'friends', 24 | }, challengableFriends.map(renderFriendOption)); 25 | } 26 | 27 | function renderChallengeControl() { 28 | const invitePlayer = (e) => { 29 | const buttonElement = e.srcElement; 30 | buttonElement.disabled = true; 31 | 32 | const inviteDropdown = document.getElementById('ssb-chess-challenge-control-select'); 33 | const friendId = inviteDropdown.options[inviteDropdown.selectedIndex].value; 34 | 35 | gameCtrl.getInviteCtrl().inviteToPlay(friendId) 36 | .then(msg => m.route.set(`/games/${btoa(msg.key)}`)) 37 | .then(() => { buttonElement.disabled = false; }); 38 | }; 39 | 40 | const challengeButton = m('button', { 41 | class: 'ssb-chess-challenge-send-button', 42 | onclick: invitePlayer, 43 | }, 'Challenge'); 44 | 45 | return m('div', { 46 | class: 'ssb-chess-challenge-control', 47 | }, [renderFriendsdropDown(), challengeButton]); 48 | } 49 | 50 | function getWeight(playerId, frequencies) { 51 | return frequencies[playerId] ? frequencies[playerId] : 0; 52 | } 53 | 54 | /** 55 | * Sort the list of invitable people by the frequency with which we've played them (weighted slightly by recency.) 56 | */ 57 | function updateFriends() { 58 | 59 | var playedFrequencies = gameCtrl.getSocialCtrl().getWeightedPlayFrequencyList(); 60 | var followedByMe = gameCtrl.getSocialCtrl().followedByMeWithNames() 61 | 62 | Promise.all([followedByMe, playedFrequencies]).then(result => { 63 | var [following, playedWeights] = result; 64 | 65 | following.sort( (a,b) => { 66 | var comp = getWeight(b.ident, playedWeights) - getWeight (a.ident, playedWeights); 67 | 68 | if (comp === 0) { 69 | return compareAlphabetically(a,b); 70 | } else { 71 | return comp; 72 | } 73 | 74 | }); 75 | 76 | 77 | challengableFriends = following; 78 | }).then(m.redraw); 79 | } 80 | 81 | return { 82 | view: renderChallengeControl, 83 | 84 | oncreate: () => { 85 | updateFriends(); 86 | }, 87 | }; 88 | }; 89 | -------------------------------------------------------------------------------- /css/chessground-examples/assets/3d.css: -------------------------------------------------------------------------------- 1 | .in3d .cg-wrap { 2 | width: 512px; 3 | height: 464.5px; 4 | } 5 | .in3d .cg-board::before { 6 | position: absolute; 7 | top: -0.730688%; 8 | left: 0; 9 | width: 100%; 10 | height: 103.2%; 11 | content: ''; 12 | background-size: cover; 13 | background-image: url('images/board/3d/woodi.1024.png'); 14 | } 15 | .in3d .ghost, 16 | .in3d .over { 17 | display: none; 18 | } 19 | .in3d square[data-coord-x]::after { 20 | bottom: calc(-10px - 22%); 21 | } 22 | .in3d piece { 23 | /* original size: 24 | width: 140.625%; 25 | height: 179.6875%; */ 26 | /* size on 3D board, with height/width = 90.78571% */ 27 | width: 16.741%; 28 | height: 23.563%; 29 | left: -1.85%; 30 | top: -9.1%; 31 | } 32 | .cg-board piece.dragging { 33 | cursor: move; 34 | z-index: 70!important; 35 | } 36 | .in3d .cg-wrap piece.pawn.white { 37 | background-image: url('images/pieces/staunton/basic/White-Pawn.png'); 38 | } 39 | .in3d .cg-wrap piece.bishop.white { 40 | background-image: url('images/pieces/staunton/basic/White-Bishop.png'); 41 | } 42 | .in3d .cg-wrap.orientation-black div.bishop.white { 43 | background-image: url('images/pieces/staunton/basic/White-Bishop-Flipped.png'); 44 | } 45 | .in3d .cg-wrap piece.knight.white { 46 | background-image: url('images/pieces/staunton/basic/White-Knight.png'); 47 | } 48 | .in3d .cg-wrap.orientation-black div.knight.white { 49 | background-image: url('images/pieces/staunton/basic/White-Knight-Flipped.png'); 50 | } 51 | .in3d .cg-wrap piece.rook.white { 52 | background-image: url('images/pieces/staunton/basic/White-Rook.png'); 53 | } 54 | .in3d .cg-wrap piece.queen.white { 55 | background-image: url('images/pieces/staunton/basic/White-Queen.png'); 56 | } 57 | .in3d .cg-wrap piece.king.white { 58 | background-image: url('images/pieces/staunton/basic/White-King.png'); 59 | } 60 | .in3d .cg-wrap piece.pawn.black { 61 | background-image: url('images/pieces/staunton/basic/Black-Pawn.png'); 62 | } 63 | .in3d .cg-wrap piece.bishop.black { 64 | background-image: url('images/pieces/staunton/basic/Black-Bishop.png'); 65 | } 66 | .in3d .cg-wrap.orientation-white div.bishop.black { 67 | background-image: url('images/pieces/staunton/basic/Black-Bishop-Flipped.png'); 68 | } 69 | .in3d .cg-wrap piece.knight.black { 70 | background-image: url('images/pieces/staunton/basic/Black-Knight.png'); 71 | } 72 | .in3d .cg-wrap.orientation-white div.knight.black { 73 | background-image: url('images/pieces/staunton/basic/Black-Knight-Flipped.png'); 74 | } 75 | .in3d .cg-wrap piece.rook.black { 76 | background-image: url('images/pieces/staunton/basic/Black-Rook.png'); 77 | } 78 | .in3d .cg-wrap piece.queen.black { 79 | background-image: url('images/pieces/staunton/basic/Black-Queen.png'); 80 | } 81 | .in3d .cg-wrap piece.king.black { 82 | background-image: url('images/pieces/staunton/basic/Black-King.png'); 83 | } 84 | -------------------------------------------------------------------------------- /ui/recent_activity/gameEnd.js: -------------------------------------------------------------------------------- 1 | const computed = require('mutant/computed'); 2 | const m = require('mithril'); 3 | 4 | const Miniboard = require('../miniboard/miniboard'); 5 | 6 | module.exports = (msg, situation, myIdent) => { 7 | function loading() { 8 | return m('div', 'Loading...'); 9 | } 10 | 11 | function renderMateMessage(gameState) { 12 | let otherPlayer = gameState.getOtherPlayer(myIdent); 13 | const name = otherPlayer ? otherPlayer.name : ''; 14 | if (gameState.status.winner === myIdent) { 15 | return m('div', `You won your game against ${name}`); 16 | } if (gameState.hasPlayer(myIdent)) { 17 | return m('div', `You lost your game against ${name}`); 18 | } 19 | const winnerName = gameState.players[gameState.status.winner].name; 20 | otherPlayer = gameState.otherPlayer(gameState.status.winner).name; 21 | return m('div', `${winnerName} won their game against ${otherPlayer}`); 22 | } 23 | 24 | function renderResignMessage(gameState) { 25 | let otherPlayer = gameState.getOtherPlayer(myIdent); 26 | const name = otherPlayer ? otherPlayer.name : ''; 27 | 28 | if (gameState.status.winner === myIdent) { 29 | return ('div', `${name} resigned their game against you.`); 30 | } if (gameState.hasPlayer(myIdent)) { 31 | return ('div', `You resigned your game against ${name}`); 32 | } 33 | const winnerName = gameState.players[gameState.status.winner].name; 34 | otherPlayer = gameState.otherPlayer(gameState.status.winner).name; 35 | 36 | return m('div', `${winnerName} won their game against ${otherPlayer}`); 37 | } 38 | 39 | function renderStalemateMessage(gameState) { 40 | const otherPlayer = gameState.getOtherPlayer(myIdent); 41 | const name = otherPlayer ? otherPlayer.name : ''; 42 | 43 | return ('div', `Your game with ${name} ended in a stalemate`); 44 | } 45 | 46 | function renderInformation(gameState) { 47 | let message; 48 | 49 | if (gameState.status.status === 'mate') { 50 | message = renderMateMessage(gameState); 51 | } if (gameState.status.status === 'resigned') { 52 | message = renderResignMessage(gameState); 53 | } if (gameState.status.status === 'stalemate') { 54 | message = renderStalemateMessage(gameState); 55 | } 56 | 57 | if (message) { 58 | const className = 'ssb-chess-game-activity-notification-text'; 59 | return m('div', { class: className }, message); 60 | } 61 | throw new Error(`Unexpected game state: ${gameState.status.status}`); 62 | } 63 | 64 | function render() { 65 | if (!situation) { 66 | return loading(); 67 | } 68 | const opts = { 69 | small: true, 70 | }; 71 | 72 | return m('div', { class: 'ssb-chess-game-end-notification' }, [ 73 | m('div', m(Miniboard(computed([situation], s => s), situation, myIdent, opts))), 74 | renderInformation(situation), 75 | ]); 76 | } 77 | 78 | return { 79 | view: render, 80 | oncreate: () => {}, 81 | }; 82 | }; 83 | -------------------------------------------------------------------------------- /css/chessground-examples/assets/images/pieces/merida/bN.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /css/chessground-examples/assets/images/pieces/merida/bB.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /css/chessground-examples/assets/images/pieces/merida/wK.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/pageLayout/navigation.js: -------------------------------------------------------------------------------- 1 | const m = require('mithril'); 2 | const SettingsDialog = require('../settings/settings-dialog'); 3 | 4 | module.exports = (mainCtrl, settings) => { 5 | const gamesInProgress = { 6 | name: 'Games', 7 | link: '/my_games', 8 | count: 0, 9 | countHoverText: () => `You have ${gamesInProgress.count} games in progress.`, 10 | }; 11 | const gamesMyMove = { 12 | name: 'My Move', 13 | link: '/games_my_move', 14 | count: 0, 15 | countUpdateFn: mainCtrl.getGameCtrl().getGamesWhereMyMove, 16 | countHoverText: () => `${gamesMyMove.count} games awaiting your move.`, 17 | }; 18 | const invitations = { 19 | name: 'Invitations', 20 | link: '/invitations', 21 | count: 0, 22 | countUpdateFn: mainCtrl.getInviteCtrl().pendingChallengesReceived, 23 | countHoverText: () => `${invitations.count} pending invitations received.`, 24 | }; 25 | const observable = { 26 | name: 'Observe', 27 | link: '/observable', 28 | count: 0, 29 | countUpdateFn: mainCtrl.getGameCtrl().getFriendsObservableGames, 30 | countOnHoverOnly: true, 31 | countHoverText: () => `${observable.count} observable games.`, 32 | }; 33 | const recent = { 34 | name: 'Recent Activity', 35 | link: '/activity', 36 | count: 0, 37 | countUpdateFn: mainCtrl.getRecentActivityCtrl().unseenNotifications, 38 | countHoverText: () => 'Recent Activity', 39 | }; 40 | 41 | const navItems = [gamesInProgress, gamesMyMove, invitations, observable, recent]; 42 | 43 | function renderNavItem(navItem) { 44 | return m('span', { 45 | class: 'ssb-chess-nav-item', 46 | }, m(`a[href=${navItem.link}]`, { 47 | oncreate: m.route.link, 48 | title: navItem.countHoverText(), 49 | }, 50 | 51 | [m('span', navItem.name), 52 | m('span', { 53 | style: (navItem.countOnHoverOnly || navItem.count === 0) ? 'display: none' : '', 54 | class: 'ssb-chess-nav-count', 55 | }, `(${navItem.count})`), 56 | ])); 57 | } 58 | 59 | const closeSettingsDialog = () => { 60 | const dialogElementId = 'ssb-chess-settings-dialog'; 61 | const element = document.getElementById(dialogElementId); 62 | 63 | if (element) { 64 | element.parentNode.removeChild(element); 65 | } 66 | }; 67 | 68 | const showSettings = () => { 69 | const element = document.getElementById('ssb-chess-settings-dialog'); 70 | 71 | if (!element) { 72 | const container = document.createElement('div'); 73 | container.id = 'ssb-chess-settings-dialog'; 74 | 75 | const settingsDialog = SettingsDialog(settings, closeSettingsDialog); 76 | 77 | document.body.appendChild(container); 78 | m.render(container, m(settingsDialog)); 79 | } 80 | }; 81 | 82 | function renderNavigation() { 83 | return m('div', [ 84 | navItems.map(renderNavItem), 85 | m('a', { id: 'ssb-chess-settings-nav-item', href: '#', onclick: showSettings }, 'SETTINGS'), 86 | ]); 87 | } 88 | 89 | function keepCountsUpdated() { 90 | navItems.forEach((navItem) => { 91 | if (navItem.countUpdateFn) { 92 | navItem.countUpdateFn()((items) => { 93 | const numItems = items.length; 94 | navItem.count = numItems; 95 | m.redraw(); 96 | }); 97 | } 98 | }); 99 | } 100 | 101 | return { 102 | view: () => renderNavigation(), 103 | oncreate: () => { 104 | keepCountsUpdated(); 105 | }, 106 | onremove: () => { 107 | 108 | }, 109 | }; 110 | }; 111 | -------------------------------------------------------------------------------- /ui/game/PieceGraveyard.js: -------------------------------------------------------------------------------- 1 | const m = require('mithril'); 2 | const watchAll = require('mutant/watch-all'); 3 | 4 | const { opposite } = require('chessground/util'); 5 | 6 | const R = require('ramda'); 7 | 8 | /** 9 | * A view of the pieces of the differences in pieces compared to the other 10 | * player. 11 | * 12 | * @param @chessGroundObservable The chessground observable becomes populated with a value when the 13 | * board has been initialised. 14 | * @param @situationObservable Fires when the game has been updated with a new 15 | * move, etc. This may have involved a piece being captured so we may 16 | * have to update. 17 | * @param @moveSelectedObservable Fires when the user has chosen a move in the move history, 18 | * so we display the material difference for that move in the history. 19 | * @param @myIdent The user's identity (used to decide the viewing perspective of the board.) 20 | * @param @bottom boolean of whether it is the bottom or top piece graveyard 21 | */ 22 | module.exports = ( 23 | chessGroundObservable, 24 | situationObservable, 25 | moveSelectedObservable, 26 | myIdent, 27 | bottom, 28 | ) => { 29 | let materialDiff = {}; 30 | 31 | let playerColour = null; 32 | let opponentColor = null; 33 | 34 | // Copied from lila (lichess) and translated to JavaScript from typescript :P 35 | // https://github.com/ornicar/lila/blob/c72ca979a846304a772e1c4f2b0d1851b076849d/ui/round/src/util.ts#L49 36 | function getMaterialDiff(pieces) { 37 | const diff = { 38 | white: { 39 | king: 0, queen: 0, rook: 0, bishop: 0, knight: 0, pawn: 0, 40 | }, 41 | black: { 42 | king: 0, queen: 0, rook: 0, bishop: 0, knight: 0, pawn: 0, 43 | }, 44 | }; 45 | 46 | Object.keys(pieces).forEach((k) => { 47 | const p = pieces[k]; 48 | const them = diff[opposite(p.color)]; 49 | const i = 1; 50 | if (them[p.role] > 0) { 51 | them[p.role] -= i; 52 | } else { 53 | diff[p.color][p.role] += i; 54 | } 55 | }); 56 | 57 | return diff; 58 | } 59 | 60 | function setPlayerColours(situation) { 61 | playerColour = situation.players[myIdent] ? situation.players[myIdent].colour : 'white'; 62 | opponentColor = playerColour === 'white' ? 'black' : 'white'; 63 | } 64 | 65 | function renderPiecesForColour(colour) { 66 | let pieces = []; 67 | if (typeof materialDiff[colour] === 'object') { 68 | Object.keys(materialDiff[colour]).forEach((pieceName) => { 69 | const numPieces = materialDiff[colour][pieceName]; 70 | const repeated = R.repeat(pieceName, numPieces); 71 | 72 | pieces = pieces.concat(repeated); 73 | }); 74 | } 75 | 76 | return pieces.map(p => m('mono-piece', { class: p })); 77 | } 78 | 79 | return { 80 | view: () => m('div', { 81 | class: 'ssb-chess-graveyard', 82 | }, bottom ? renderPiecesForColour(playerColour) : renderPiecesForColour(opponentColor)), 83 | oncreate: () => { 84 | this.removeWatches = watchAll( 85 | [chessGroundObservable, situationObservable, moveSelectedObservable], 86 | (chessground, situation) => { 87 | if (situation) { 88 | setPlayerColours(situation); 89 | } 90 | 91 | if (chessground) { 92 | const { pieces } = chessground.state; 93 | materialDiff = getMaterialDiff(pieces); 94 | m.redraw(); 95 | } 96 | }, 97 | ); 98 | }, 99 | onremove: () => { 100 | if (this.removeWatches) { 101 | this.removeWatches(); 102 | } 103 | }, 104 | 105 | }; 106 | }; 107 | -------------------------------------------------------------------------------- /css/chessground-examples/src/units/play.ts: -------------------------------------------------------------------------------- 1 | import { Chess } from 'chess.js'; 2 | import { Chessground } from 'chessground'; 3 | import { Unit } from './unit'; 4 | import { toColor, toDests, aiPlay, playOtherSide } from '../util' 5 | 6 | export const initial: Unit = { 7 | name: 'Play legal moves from initial position', 8 | run(el) { 9 | const chess = new Chess(); 10 | const cg = Chessground(el, { 11 | movable: { 12 | color: 'white', 13 | free: false, 14 | dests: toDests(chess), 15 | }, 16 | draggable: { 17 | showGhost: true 18 | } 19 | }); 20 | cg.set({ 21 | movable: { events: { after: playOtherSide(cg, chess) } } 22 | }); 23 | return cg; 24 | } 25 | }; 26 | 27 | export const castling: Unit = { 28 | name: 'Castling', 29 | run(el) { 30 | const fen = 'rnbqk2r/pppp1ppp/5n2/2b1p3/2B1P3/5N2/PPPP1PPP/RNBQK2R w KQkq - 4 4'; 31 | const chess = new Chess(fen); 32 | const cg = Chessground(el, { 33 | fen: fen, 34 | turnColor: toColor(chess), 35 | movable: { 36 | color: 'white', 37 | free: false, 38 | dests: toDests(chess) 39 | } 40 | }); 41 | cg.set({ 42 | movable: { events: { after: playOtherSide(cg, chess) } } 43 | }); 44 | return cg; 45 | } 46 | }; 47 | 48 | export const vsRandom: Unit = { 49 | name: 'Play vs random AI', 50 | run(el) { 51 | const chess = new Chess(); 52 | const cg = Chessground(el, { 53 | movable: { 54 | color: 'white', 55 | free: false, 56 | dests: toDests(chess) 57 | } 58 | }); 59 | cg.set({ 60 | movable: { 61 | events: { 62 | after: aiPlay(cg, chess, 1000, false) 63 | } 64 | } 65 | }); 66 | return cg; 67 | } 68 | }; 69 | 70 | export const fullRandom: Unit = { 71 | name: 'Watch 2 random AIs', 72 | run(el) { 73 | const chess = new Chess(); 74 | const cg = Chessground(el, { 75 | animation: { 76 | duration: 1000 77 | }, 78 | movable: { 79 | free: false 80 | } 81 | }); 82 | function makeMove() { 83 | if (!cg.state.dom.elements.board.offsetParent) return; 84 | const moves = chess.moves({verbose:true}); 85 | const move = moves[Math.floor(Math.random() * moves.length)]; 86 | chess.move(move.san); 87 | cg.move(move.from, move.to); 88 | setTimeout(makeMove, 700); 89 | } 90 | setTimeout(makeMove, 700); 91 | return cg; 92 | } 93 | } 94 | 95 | export const slowAnim: Unit = { 96 | name: 'Play vs random AI; slow animations', 97 | run(el) { 98 | const chess = new Chess(); 99 | const cg = Chessground(el, { 100 | animation: { 101 | duration: 5000 102 | }, 103 | movable: { 104 | color: 'white', 105 | free: false, 106 | dests: toDests(chess) 107 | } 108 | }); 109 | cg.set({ 110 | movable: { 111 | events: { 112 | after: aiPlay(cg, chess, 1000, false) 113 | } 114 | } 115 | }); 116 | return cg; 117 | } 118 | }; 119 | 120 | export const conflictingHold: Unit = { 121 | name: 'Conflicting hold/premove', 122 | run(el) { 123 | const cg = Chessground(el, { 124 | fen: '8/8/5p2/4P3/8/8/8/8', 125 | turnColor: 'black', 126 | movable: { 127 | color: 'white', 128 | free: false, 129 | dests: {e5: ['f6']} 130 | } 131 | }); 132 | setTimeout(() => { 133 | cg.move('f6', 'e5'); 134 | cg.playPremove(); 135 | cg.set({ 136 | turnColor: 'white', 137 | movable: { 138 | dests: undefined 139 | } 140 | }); 141 | }, 1000); 142 | return cg; 143 | } 144 | }; 145 | -------------------------------------------------------------------------------- /ui/game/nextGameControl.js: -------------------------------------------------------------------------------- 1 | const computed = require('mutant/computed'); 2 | const m = require('mithril'); 3 | const watch = require('mutant/watch'); 4 | 5 | module.exports = (currentGameObservable, gameCollectionObservable) => { 6 | 7 | const watchesToClear = []; 8 | 9 | const buttonGames = computed([getPreviousGame(), getNextGame(), currentGameObservable], (next, previous, current) => { 10 | return { 11 | nextGame: next, 12 | previousGame: previous, 13 | currentGame: current 14 | } 15 | }); 16 | 17 | function getGameInDirection(isForward) { 18 | return computed([currentGameObservable, gameCollectionObservable], (currentGame, gamesMyMove) => { 19 | 20 | if (gamesMyMove.size === 0) { 21 | return null; 22 | } 23 | else { 24 | const sorted = sortGamesByTimestamp(gamesMyMove, isForward); 25 | const idxCurrentGame = sorted.findIndex(game => game.gameId === currentGame.gameId); 26 | 27 | const nextGame = sorted[idxCurrentGame + 1]; 28 | 29 | if (idxCurrentGame === null || idxCurrentGame === undefined) { 30 | return sorted[0]; 31 | } 32 | if (nextGame) { 33 | return nextGame; 34 | } else { 35 | // Circle round to the first game 36 | return sorted[0]; 37 | } 38 | } 39 | 40 | }); 41 | } 42 | 43 | function getNextGame() { 44 | return getGameInDirection(true); 45 | } 46 | 47 | function getPreviousGame() { 48 | return getGameInDirection(false); 49 | } 50 | 51 | function sortGamesByTimestamp(games, inAscendingOrder) { 52 | const comparer = (g1, g2) => inAscendingOrder ? (g1.lastUpdateTime - g2.lastUpdateTime) : (g2.lastUpdateTime - g1.lastUpdateTime); 53 | 54 | // .concat to copy the array so that we don't change the observable's value. 55 | return games.concat().sort(comparer); 56 | } 57 | 58 | function goToGame(gameId) { 59 | const url = '/games/:gameId'; 60 | 61 | m.route.set(url, { 62 | gameId: btoa(gameId), 63 | }); 64 | } 65 | 66 | function renderButton(text, gameId, isHidden) { 67 | let classes = 'ssb-chess-next-previous-button'; 68 | 69 | if (isHidden) { 70 | classes = classes + ' ssb-chess-next-previous-button-hidden' 71 | } 72 | 73 | return m('button', { 74 | class: classes, 75 | onclick: () => goToGame(gameId) 76 | }, text) 77 | } 78 | 79 | function renderButtons() { 80 | var games = buttonGames(); 81 | 82 | if (!games) { 83 | return []; 84 | } else { 85 | var previous = games.previousGame; 86 | var hasPrevious = previous != null && previous.gameId !== games.currentGame.gameId; 87 | 88 | var next = games.nextGame; 89 | var hasNext = next != null && next.gameId !== games.currentGame.gameId; 90 | 91 | return [ 92 | renderButton("<-", hasPrevious ? previous.gameId : null, !hasPrevious), 93 | renderButton("->", hasNext ? next.gameId : null, !hasNext) 94 | ] 95 | } 96 | } 97 | 98 | return { 99 | oncreate: () => { 100 | w = watch(buttonGames, m.redraw); 101 | watchesToClear.push(w); 102 | }, 103 | view: () => { 104 | return m('div', { className: 'ssb-chess-next-previous-buttons-container' }, 105 | renderButtons() 106 | ); 107 | }, 108 | onremove: () => { 109 | watchesToClear.forEach(w => w()); 110 | } 111 | } 112 | } -------------------------------------------------------------------------------- /ui/invitations/invitations.js: -------------------------------------------------------------------------------- 1 | const m = require('mithril'); 2 | const Miniboard = require('../miniboard/miniboard'); 3 | const ChallengeComponent = require('../challenge/challenge_control'); 4 | 5 | module.exports = (mainCtrl) => { 6 | let invitationsReceived = []; 7 | let invitationsSent = []; 8 | 9 | let watches = []; 10 | 11 | const challengeComponent = ChallengeComponent(mainCtrl); 12 | 13 | function renderAcceptOrRejectControls(gameId, inviteSent) { 14 | const acceptInvite = () => { 15 | mainCtrl.getInviteCtrl().acceptChallenge(gameId) 16 | .then(() => m.route.set(`/games/${btoa(gameId)}`)); 17 | }; 18 | 19 | // Hide for now since it doesn't do anything yet ;x 20 | // Will unhide once I implement 'cancel invites' controllers 21 | // and take them into account when indexing games. 22 | const cancelButton = m('button', { 23 | style: 'display: none;', 24 | class: 'ssb-chess-miniboard-controls', 25 | disabled: true, 26 | }, 'cancel'); 27 | 28 | const acceptOrRejectButtons = [ 29 | m('button', { 30 | class: 'ssb-chess-miniboard-control', 31 | onclick: acceptInvite, 32 | }, 'accept'), 33 | m('button', { 34 | class: 'ssb-chess-miniboard-control', 35 | disabled: true, 36 | }, 'decline'), 37 | ]; 38 | 39 | return m('div', { 40 | class: 'ssb-chess-miniboard-controls', 41 | }, (inviteSent ? cancelButton : acceptOrRejectButtons)); 42 | } 43 | 44 | function renderInvite(gameSummary, sent) { 45 | const gameSummaryObservable = mainCtrl.getGameCtrl().getSituationSummaryObservable(gameSummary.gameId); 46 | 47 | return m('div', { 48 | class: 'ssb-chess-miniboard', 49 | }, [ 50 | m(Miniboard(gameSummaryObservable, gameSummary, mainCtrl.getMyIdent())), 51 | renderAcceptOrRejectControls(gameSummary.gameId, sent), 52 | ]); 53 | } 54 | 55 | function invitesToSituations(invites) { 56 | return Promise.all( 57 | invites.map(invite => mainCtrl.getGameCtrl().getSituation(invite.gameId)), 58 | ); 59 | } 60 | 61 | function keepInvitesUpdated() { 62 | const invitesReceived = mainCtrl.getInviteCtrl().pendingChallengesReceived(); 63 | const invitesSent = mainCtrl.getInviteCtrl().pendingChallengesSent(); 64 | 65 | const w1 = invitesReceived((received) => { 66 | invitesToSituations(received) 67 | .then((inviteSituations) => { invitationsReceived = inviteSituations; }) 68 | .then(m.redraw); 69 | }); 70 | 71 | const w2 = invitesSent((sent) => { 72 | invitesToSituations(sent) 73 | .then((inviteSituations) => { invitationsSent = inviteSituations; }) 74 | .then(m.redraw); 75 | }); 76 | 77 | watches.push(w1); 78 | watches.push(w2); 79 | } 80 | 81 | function renderMiniboards(invites, sent, title) { 82 | const titleDiv = m('div', { 83 | class: 'ssb-chess-invites-section-title', 84 | }, title); 85 | 86 | const miniboards = m('div', { 87 | class: 'ssb-chess-miniboards', 88 | }, 89 | 90 | invites.map(invite => renderInvite(invite, sent))); 91 | 92 | return m('div', {}, [titleDiv, miniboards]); 93 | } 94 | 95 | return { 96 | oncreate() { 97 | keepInvitesUpdated(); 98 | }, 99 | view() { 100 | const invitationsReceivedMiniboards = renderMiniboards(invitationsReceived, false, 'Received'); 101 | const invitationsSentMiniboards = renderMiniboards(invitationsSent, true, 'Sent'); 102 | 103 | const challengeCtrl = m(challengeComponent); 104 | 105 | return m('div', [ 106 | challengeCtrl, 107 | invitationsReceivedMiniboards, 108 | invitationsSentMiniboards, 109 | ]); 110 | }, 111 | onremove() { 112 | watches.forEach(w => w()); 113 | watches = []; 114 | }, 115 | }; 116 | }; 117 | -------------------------------------------------------------------------------- /css/chessground-examples/assets/images/pieces/merida/wN.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/miniboard/miniboard.js: -------------------------------------------------------------------------------- 1 | const m = require('mithril'); 2 | const { Chessground } = require('chessground'); 3 | const timeAgo = require('./timeAgo'); 4 | 5 | module.exports = (gameSummaryObservable, summary, identPerspective, opts) => { 6 | let chessground = null; 7 | let observables = []; 8 | let lastActivityTimestamp = summary.lastUpdateTime; 9 | 10 | // An observer might not be in the 'players' list so we need a default 11 | // perspective of white for them. 12 | const playerColour = (summary.players[identPerspective] 13 | && summary.players[identPerspective].colour) ? summary.players[identPerspective].colour : 'white'; 14 | 15 | function renderPlayerName(player) { 16 | return m(`a[href=/player/${btoa(player.id)}]`, { 17 | class: 'ssb-chess-miniboard-name', 18 | oncreate: m.route.link, 19 | }, 20 | player.name.substring(0, 10)); 21 | } 22 | 23 | function renderSummaryBottom() { 24 | if (!(opts && opts.small)) { 25 | const coloursNames = summary.coloursToPlayer(); 26 | const otherPlayerColour = playerColour == 'white' ? 'black' : 'white'; 27 | 28 | const leftPlayer = coloursNames[playerColour]; 29 | const rightPlayer = coloursNames[otherPlayerColour]; 30 | 31 | return m('div', { 32 | class: 'ssb-chess-miniboard-bottom', 33 | }, [m('center', { 34 | class: 'ssb-chess-miniboard-name', 35 | }, renderPlayerName(leftPlayer)), 36 | m('small', { 37 | class: 'ssb-chess-miniboard-time-ago', 38 | }, lastActivityTimestamp ? timeAgo(lastActivityTimestamp)() : ''), 39 | m('center', { 40 | class: 'ssb-chess-miniboard-name', 41 | }, renderPlayerName(rightPlayer)), 42 | ]); 43 | } 44 | return m('div'); 45 | } 46 | 47 | function renderSummary() { 48 | const observing = Object.keys(summary.players).indexOf(identPerspective) === -1; 49 | const boardSizeClass = opts && opts.small ? 'ssb-chess-board-small' : 'ssb-chess-board-medium'; 50 | 51 | return m('div', { 52 | class: 'ssb-chess-miniboard ssb-chess-board-background-blue3 merida', 53 | }, [ 54 | m(`${'a[href=/games/'}${btoa(summary.gameId)}?observing=${observing}]`, { 55 | class: `ssb-chessground-container cg-wrap ${boardSizeClass}`, 56 | title: summary.gameId, 57 | id: summary.gameId, 58 | oncreate: m.route.link, 59 | }), renderSummaryBottom(), 60 | ]); 61 | } 62 | 63 | function summaryToChessgroundConfig(s) { 64 | const config = { 65 | fen: s.fen, 66 | viewOnly: true, 67 | orientation: playerColour, 68 | turnColor: s.players[s.toMove].colour, 69 | check: s.check, 70 | coordinates: false, 71 | }; 72 | 73 | if (s.lastMove) { 74 | config.lastMove = [s.lastMove.orig, s.lastMove.dest]; 75 | } 76 | 77 | return config; 78 | } 79 | 80 | return { 81 | view() { 82 | return renderSummary(); 83 | }, 84 | oncreate(vNode) { 85 | // This lifecycle event tells us that the DOM is ready. That means we 86 | // can attach chessground to our chessground container element that was 87 | // prepared for it during the 'view' lifecycle method. 88 | 89 | const config = summaryToChessgroundConfig(summary); 90 | 91 | const { dom } = vNode; 92 | const chessGroundParent = dom.querySelector('.ssb-chessground-container'); 93 | chessground = Chessground(chessGroundParent, config); 94 | 95 | // Listen for game updates 96 | 97 | const situationObs = gameSummaryObservable((newSummary) => { 98 | const newConfig = summaryToChessgroundConfig(newSummary); 99 | chessground.set(newConfig); 100 | lastActivityTimestamp = newSummary.lastUpdateTime; 101 | }); 102 | 103 | observables.push(situationObs); 104 | }, 105 | onremove() { 106 | if (chessground) { 107 | chessground.destroy(); 108 | } 109 | 110 | observables.forEach(w => w()); 111 | observables = []; 112 | }, 113 | }; 114 | }; 115 | -------------------------------------------------------------------------------- /css/chessground-examples/assets/chessground.css: -------------------------------------------------------------------------------- 1 | /* 2 | * Chessground base css properties. 3 | * 4 | * You need to include the css files in themes folder in order to have the 5 | * board and pieces displayed! 6 | */ 7 | 8 | .cg-wrap { 9 | width: 320px; 10 | height: 320px; 11 | position: relative; 12 | display: block; 13 | } 14 | 15 | cg-helper { 16 | position: absolute; 17 | width: 12.5%; 18 | padding-bottom: 12.5%; 19 | display: table; /* hack: round to full pixel size in chrome */ 20 | bottom: 0; 21 | } 22 | 23 | cg-container { 24 | position: absolute; 25 | width: 800%; 26 | height: 800%; 27 | display: block; 28 | bottom: 0; 29 | } 30 | 31 | cg-board { 32 | position: absolute; 33 | top: 0; 34 | left: 0; 35 | width: 100%; 36 | height: 100%; 37 | -webkit-user-select: none; 38 | -moz-user-select: none; 39 | -ms-user-select: none; 40 | user-select: none; 41 | line-height: 0; 42 | background-size: cover; 43 | cursor: pointer; 44 | } 45 | cg-board square { 46 | position: absolute; 47 | top: 0; 48 | left: 0; 49 | width: 12.5%; 50 | height: 12.5%; 51 | pointer-events: none; 52 | } 53 | cg-board square.move-dest { 54 | background: radial-gradient(rgba(20, 85, 30, 0.5) 22%, #208530 0, rgba(0, 0, 0, 0.3) 0, rgba(0, 0, 0, 0) 0); 55 | pointer-events: auto; 56 | } 57 | cg-board square.premove-dest { 58 | background: radial-gradient(rgba(20, 30, 85, 0.5) 22%, #203085 0, rgba(0, 0, 0, 0.3) 0, rgba(0, 0, 0, 0) 0); 59 | } 60 | cg-board square.oc.move-dest { 61 | background: radial-gradient(transparent 0%, transparent 80%, rgba(20, 85, 0, 0.3) 80%); 62 | } 63 | cg-board square.oc.premove-dest { 64 | background: radial-gradient(transparent 0%, transparent 80%, rgba(20, 30, 85, 0.2) 80%); 65 | } 66 | cg-board square.move-dest:hover { 67 | background: rgba(20, 85, 30, 0.3); 68 | } 69 | cg-board square.premove-dest:hover { 70 | background: rgba(20, 30, 85, 0.2); 71 | } 72 | cg-board square.last-move { 73 | will-change: transform; 74 | background-color: rgba(155, 199, 0, 0.41); 75 | } 76 | cg-board square.selected { 77 | background-color: rgba(20, 85, 30, 0.5); 78 | } 79 | cg-board square.check { 80 | background: radial-gradient(ellipse at center, rgba(255, 0, 0, 1) 0%, rgba(231, 0, 0, 1) 25%, rgba(169, 0, 0, 0) 89%, rgba(158, 0, 0, 0) 100%); 81 | } 82 | cg-board square.current-premove { 83 | background-color: rgba(20, 30, 85, 0.5); 84 | } 85 | .cg-wrap piece { 86 | position: absolute; 87 | top: 0; 88 | left: 0; 89 | width: 12.5%; 90 | height: 12.5%; 91 | background-size: cover; 92 | z-index: 2; 93 | will-change: transform; 94 | pointer-events: none; 95 | } 96 | cg-board piece.dragging { 97 | cursor: move; 98 | z-index: 9; 99 | } 100 | cg-board piece.anim { 101 | z-index: 8; 102 | } 103 | cg-board piece.fading { 104 | z-index: 1; 105 | opacity: 0.5; 106 | } 107 | .cg-wrap square.move-dest:hover { 108 | background-color: rgba(20, 85, 30, 0.3); 109 | } 110 | .cg-wrap piece.ghost { 111 | opacity: 0.3; 112 | } 113 | .cg-wrap svg { 114 | overflow: hidden; 115 | position: relative; 116 | top: 0px; 117 | left: 0px; 118 | width: 100%; 119 | height: 100%; 120 | pointer-events: none; 121 | z-index: 2; 122 | opacity: 0.6; 123 | } 124 | .cg-wrap svg image { 125 | opacity: 0.5; 126 | } 127 | .cg-wrap coords { 128 | position: absolute; 129 | display: flex; 130 | pointer-events: none; 131 | opacity: 0.8; 132 | font-size: 9px; 133 | } 134 | .cg-wrap coords.ranks { 135 | right: -15px; 136 | top: 0; 137 | flex-flow: column-reverse; 138 | height: 100%; 139 | width: 12px; 140 | } 141 | .cg-wrap coords.ranks.black { 142 | flex-flow: column; 143 | } 144 | .cg-wrap coords.files { 145 | bottom: -16px; 146 | left: 0; 147 | flex-flow: row; 148 | width: 100%; 149 | height: 16px; 150 | text-transform: uppercase; 151 | text-align: center; 152 | } 153 | .cg-wrap coords.files.black { 154 | flex-flow: row-reverse; 155 | } 156 | .cg-wrap coords coord { 157 | flex: 1 1 auto; 158 | } 159 | .cg-wrap coords.ranks coord { 160 | transform: translateY(39%); 161 | } 162 | -------------------------------------------------------------------------------- /css/chessground-examples/assets/images/pieces/merida/wQ.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /css/chessground-examples/assets/images/pieces/merida/wB.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /css/chessground-examples/assets/images/pieces/merida/bK.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const m = require('mithril'); 2 | const onceTrue = require('mutant/once-true'); 3 | 4 | const MainCtrl = require('ssb-chess'); 5 | 6 | const MiniboardListComponent = require('./ui/miniboard/miniboard_list'); 7 | const NavigationBar = require('./ui/pageLayout/navigation'); 8 | const GameComponent = require('./ui/game/gameView'); 9 | const PlayerProfileComponent = require('./ui/player/player_profile'); 10 | const InvitationsComponent = require('./ui/invitations/invitations'); 11 | const RecentActivityComponent = require('./ui/recent_activity/recent'); 12 | const PgnExportComponent = require('./ui/export/pgnExport'); 13 | const Notifier = require('./ui/notify/notifier'); 14 | 15 | module.exports = (attachToElement, dataAccess, opts = {}) => { 16 | const { initialView } = opts; 17 | 18 | const cssFiles = [ 19 | './css/global.css', 20 | './css/chessground-examples/assets/chessground.css', 21 | './css/chessground-examples/assets/theme.css', 22 | './css/board-theme.css', 23 | './css/miniboards.css', 24 | './css/largeBoard.css', 25 | './css/invites.css', 26 | './css/loading.css', 27 | './css/promote.css', 28 | './css/game.css', 29 | './css/historyArea.css', 30 | './css/playerProfiles.css', 31 | './css/actionButtons.css', 32 | './css/activity.css', 33 | './css/pgnExport.css', 34 | './css/nextPrevious.css' 35 | ]; 36 | 37 | // h4cky0 strikes again? mebbe there's a better way? ;x 38 | function cssFilesToStyleTag(dom) { 39 | 40 | const rootDir = `${__dirname}/`; 41 | 42 | const styles = m('div', {}, cssFiles.map(file => m('link', { rel: 'stylesheet', href: rootDir + file }))); 43 | 44 | m.render(dom, styles); 45 | } 46 | 47 | function renderPageTop(parent, mainCtrl, settingsCtrl) { 48 | const navBar = NavigationBar(mainCtrl, settingsCtrl); 49 | 50 | const TopComponent = { 51 | view: () => m('div', [ 52 | m(navBar), 53 | ]), 54 | }; 55 | 56 | m.mount(parent, TopComponent); 57 | } 58 | 59 | function appRouter(mainBody, mainCtrl, settingsCtrl) { 60 | const gamesInProgressObs = mainCtrl.getGameCtrl().getMyGamesInProgress(); 61 | const gamesMyMoveObs = mainCtrl.getGameCtrl().getGamesWhereMyMove(); 62 | const observableGamesObs = mainCtrl.getGameCtrl().getFriendsObservableGames(); 63 | const userRecentActivity = mainCtrl.getRecentActivityCtrl().getRecentActivityForUserGames(); 64 | 65 | // Hack: keep observables loaded with the latest value. 66 | gamesInProgressObs(e => e); 67 | gamesMyMoveObs(e => e); 68 | observableGamesObs(e => e); 69 | userRecentActivity(e => e); 70 | 71 | const defaultView = initialView || '/my_games'; 72 | 73 | m.route(mainBody, defaultView, { 74 | '/my_games': MiniboardListComponent(mainCtrl, gamesInProgressObs, mainCtrl.getMyIdent()), 75 | '/games_my_move': MiniboardListComponent(mainCtrl, gamesMyMoveObs, mainCtrl.getMyIdent()), 76 | '/games/:gameId': { 77 | onmatch(args) { 78 | const gameId = atob(args.gameId); 79 | const gameSituationObs = mainCtrl.getGameCtrl().getSituationObservable(gameId); 80 | 81 | const observables = { 82 | situationObservable: gameSituationObs, 83 | gamesWhereMyMove: gamesMyMoveObs, 84 | observableGames: observableGamesObs 85 | } 86 | 87 | // Only load the game page once we have the initial game situation state. 88 | // The mithril router allows us to return a component in a promise. 89 | return new Promise((resolve) => { 90 | onceTrue(gameSituationObs, () => { 91 | const gameComponent = GameComponent(dataAccess, mainCtrl, observables, settingsCtrl); 92 | resolve(gameComponent); 93 | }); 94 | }); 95 | }, 96 | }, 97 | '/invitations': InvitationsComponent(mainCtrl), 98 | '/activity': RecentActivityComponent(mainCtrl, userRecentActivity), 99 | '/observable': MiniboardListComponent(mainCtrl, observableGamesObs, mainCtrl.getMyIdent()), 100 | '/player/:playerId': PlayerProfileComponent(mainCtrl), 101 | '/games/:gameId/pgn': { 102 | onmatch(args) { 103 | const gameId = atob(args.gameId); 104 | return mainCtrl.getPgnCtrl() 105 | .getPgnExport(gameId) 106 | .then(pgnText => PgnExportComponent(gameId, pgnText)); 107 | }, 108 | }, 109 | }); 110 | } 111 | 112 | dataAccess.whoAmI((err, ident) => { 113 | const mainCtrl = MainCtrl(dataAccess, ident.id); 114 | 115 | const settingsCtrl = mainCtrl.getSettingsCtrl(); 116 | 117 | const mainBody = attachToElement; 118 | const navDiv = document.createElement('div'); 119 | navDiv.id = 'ssb-nav'; 120 | const bodyDiv = document.createElement('div'); 121 | 122 | const cssDiv = document.createElement('div'); 123 | cssFilesToStyleTag(cssDiv); 124 | 125 | mainBody.appendChild(cssDiv); 126 | mainBody.appendChild(navDiv); 127 | mainBody.appendChild(bodyDiv); 128 | 129 | renderPageTop(navDiv, mainCtrl, settingsCtrl); 130 | 131 | // Display HTML5 notifications if the user is not viewing the chess app 132 | // and one of their games has an update. 133 | const notifier = Notifier(mainCtrl); 134 | notifier.startNotifying(); 135 | 136 | appRouter(bodyDiv, mainCtrl, settingsCtrl); 137 | }); 138 | 139 | return { 140 | goToGame: (gameId) => { 141 | const gameRoute = `/games/${btoa(gameId)}`; 142 | m.route.set(gameRoute); 143 | }, 144 | }; 145 | }; 146 | -------------------------------------------------------------------------------- /css/chessground-examples/src/units/svg.ts: -------------------------------------------------------------------------------- 1 | import { Chessground } from 'chessground'; 2 | import { DrawShape } from 'chessground/draw'; 3 | import { Unit } from './unit'; 4 | 5 | export const presetUserShapes: Unit = { 6 | name: 'Preset user shapes', 7 | run: el => Chessground(el, { drawable: { shapes: shapeSet1 } }) 8 | }; 9 | 10 | export const changingShapesHigh: Unit = { 11 | name: 'Automatically changing shapes (high diff)', 12 | run(el) { 13 | const cg = Chessground(el, { drawable: { shapes: shapeSet1 } }); 14 | const delay = 1000; 15 | const sets = [shapeSet1, shapeSet2, shapeSet3]; 16 | let i = 0; 17 | function run() { 18 | if (!cg.state.dom.elements.board.offsetParent) return; 19 | cg.setShapes(sets[++i % sets.length]); 20 | setTimeout(run, delay); 21 | } 22 | setTimeout(run, delay); 23 | return cg; 24 | } 25 | }; 26 | 27 | export const changingShapesLow: Unit = { 28 | name: 'Automatically changing shapes (low diff)', 29 | run(el) { 30 | const cg = Chessground(el, { drawable: { shapes: shapeSet1 } }); 31 | const delay = 1000; 32 | const sets = [shapeSet1, shapeSet1b, shapeSet1c]; 33 | let i = 0; 34 | function run() { 35 | if (!cg.state.dom.elements.board.offsetParent) return; 36 | cg.setShapes(sets[++i % sets.length]); 37 | setTimeout(run, delay); 38 | } 39 | setTimeout(run, delay); 40 | return cg; 41 | } 42 | }; 43 | 44 | export const brushModifiers: Unit = { 45 | name: 'Brush modifiers', 46 | run(el) { 47 | function sets() { 48 | return [shapeSet1, shapeSet1b, shapeSet1c].map(set => set.map(shape => { 49 | shape.modifiers = Math.round(Math.random()) ? undefined : { 50 | lineWidth: 2 + Math.round(Math.random() * 3) * 4 51 | }; 52 | return shape; 53 | })); 54 | }; 55 | const cg = Chessground(el, { drawable: { shapes: sets()[0] } }); 56 | const delay = 1000; 57 | let i = 0; 58 | function run() { 59 | if (!cg.state.dom.elements.board.offsetParent) return; 60 | cg.setShapes(sets()[++i % sets().length]); 61 | setTimeout(run, delay); 62 | } 63 | setTimeout(run, delay); 64 | return cg; 65 | } 66 | }; 67 | 68 | export const autoShapes: Unit = { 69 | name: 'Autoshapes', 70 | run(el) { 71 | function sets() { 72 | return [shapeSet1, shapeSet1b, shapeSet1c].map(set => set.map(shape => { 73 | shape.modifiers = Math.round(Math.random()) ? undefined : { 74 | lineWidth: 2 + Math.round(Math.random() * 3) * 4 75 | }; 76 | return shape; 77 | })); 78 | }; 79 | const cg = Chessground(el); 80 | const delay = 1000; 81 | let i = 0; 82 | function run() { 83 | if (!cg.state.dom.elements.board.offsetParent) return; 84 | cg.setAutoShapes(sets()[++i % sets().length]); 85 | setTimeout(run, delay); 86 | } 87 | setTimeout(run, delay); 88 | return cg; 89 | } 90 | }; 91 | 92 | export const visibleFalse: Unit = { 93 | name: 'Shapes not visible', 94 | run: el => Chessground(el, { 95 | drawable: { 96 | visible: false, 97 | shapes: shapeSet1 98 | } 99 | }) 100 | }; 101 | 102 | export const enabledFalse: Unit = { 103 | name: 'Shapes not enabled, but visible', 104 | run: el => Chessground(el, { 105 | drawable: { 106 | enabled: false, 107 | shapes: shapeSet1 108 | } 109 | }) 110 | }; 111 | 112 | const shapeSet1: DrawShape[] = [ 113 | { orig: 'a3', brush: 'green' }, 114 | { orig: 'a4', brush: 'blue' }, 115 | { orig: 'a5', brush: 'yellow' }, 116 | { orig: 'a6', brush: 'red' }, 117 | { orig: 'e2', dest: 'e4', brush: 'green' }, 118 | { orig: 'a6', dest: 'c8', brush: 'blue' }, 119 | { orig: 'f8', dest: 'f4', brush: 'yellow' }, 120 | { orig: 'h5', brush: 'green', piece: { 121 | color: 'white', 122 | role: 'knight' 123 | }}, 124 | { orig: 'h6', brush: 'red', piece: { 125 | color: 'black', 126 | role: 'queen', 127 | scale: 0.6 128 | }} 129 | ]; 130 | 131 | const shapeSet2: DrawShape[] = [ 132 | { orig: 'c1', brush: 'green' }, 133 | { orig: 'd1', brush: 'blue' }, 134 | { orig: 'e1', brush: 'yellow' }, 135 | { orig: 'e2', dest: 'e4', brush: 'green' }, 136 | { orig: 'h6', dest: 'h8', brush: 'blue' }, 137 | { orig: 'b3', dest: 'd6', brush: 'red' }, 138 | { orig: 'a1', dest: 'e1', brush: 'red' }, 139 | { orig: 'f5', brush: 'green', piece: { 140 | color: 'black', 141 | role: 'bishop' 142 | }} 143 | ]; 144 | 145 | const shapeSet3: DrawShape[] = [ 146 | { orig: 'e5', brush: 'blue' } 147 | ]; 148 | 149 | const shapeSet1b: DrawShape[] = [ 150 | { orig: 'a3', brush: 'green' }, 151 | { orig: 'a5', brush: 'yellow' }, 152 | { orig: 'a6', brush: 'red' }, 153 | { orig: 'e2', dest: 'e4', brush: 'green' }, 154 | { orig: 'a6', dest: 'c8', brush: 'blue' }, 155 | { orig: 'f8', dest: 'f4', brush: 'yellow' }, 156 | { orig: 'h5', brush: 'green', piece: { 157 | color: 'white', 158 | role: 'knight' 159 | }}, 160 | { orig: 'h6', brush: 'red', piece: { 161 | color: 'black', 162 | role: 'queen', 163 | scale: 0.6 164 | }} 165 | ]; 166 | 167 | const shapeSet1c: DrawShape[] = [ 168 | { orig: 'a3', brush: 'green' }, 169 | { orig: 'a5', brush: 'yellow' }, 170 | { orig: 'a6', brush: 'red' }, 171 | { orig: 'e2', dest: 'e4', brush: 'green' }, 172 | { orig: 'a6', dest: 'c8', brush: 'blue' }, 173 | { orig: 'b6', dest: 'd8', brush: 'blue' }, 174 | { orig: 'f8', dest: 'f4', brush: 'yellow' }, 175 | { orig: 'h5', brush: 'green', piece: { 176 | color: 'white', 177 | role: 'knight' 178 | }}, 179 | { orig: 'h6', brush: 'red', piece: { 180 | color: 'black', 181 | role: 'queen', 182 | scale: 0.6 183 | }} 184 | ]; 185 | -------------------------------------------------------------------------------- /ui/game/gameHistory.js: -------------------------------------------------------------------------------- 1 | const Value = require('mutant/value'); 2 | const m = require('mithril'); 3 | const watch = require('mutant/watch'); 4 | 5 | const R = require('ramda'); 6 | 7 | const UserLocationUtils = require('../viewer_perspective/user_location')(); 8 | 9 | module.exports = (gameObservable) => { 10 | let watchesToClear = []; 11 | 12 | let moveNumberSelected = 'live'; 13 | const moveSelectedObservable = Value(moveNumberSelected); 14 | 15 | let pgnMoves = []; 16 | let status = null; 17 | let players = []; 18 | let gameSituation = null; 19 | 20 | let latestMove = 0; 21 | 22 | function renderPlayerName(player) { 23 | return m(`a[href=/player/${btoa(player.id)}]`, { 24 | oncreate: m.route.link, 25 | }, 26 | player.name.substring(0, 10)); 27 | } 28 | 29 | function renderPlayers() { 30 | if (!gameSituation) { 31 | return m('div', {}, ''); 32 | } 33 | 34 | const coloursToPlayers = gameSituation.coloursToPlayer(); 35 | 36 | const whitePlayer = coloursToPlayers.white; 37 | const blackPlayer = coloursToPlayers.black; 38 | return m('div', { class: 'ssb-chess-history-player-container' }, [ 39 | m('div', { class: 'ssb-chess-history-player' }, renderPlayerName(whitePlayer)), 40 | m('div', { class: 'ssb-chess-history-player' }, renderPlayerName(blackPlayer)), 41 | ]); 42 | } 43 | 44 | function renderStatus() { 45 | if (!status) { 46 | return m('div', ''); 47 | } 48 | 49 | switch (status.status) { 50 | case 'invited': 51 | return m('div', { class: 'ssb-chess-status-text' }, 'Awaiting invite being accepted.'); 52 | case 'resigned': 53 | return m('div', { class: 'ssb-chess-status-text' }, 54 | `${players[status.winner].name} wins by resignation.`); 55 | case 'mate': 56 | return m('div', { class: 'ssb-chess-status-text' }, 57 | `${players[status.winner].name} wins.`); 58 | case 'draw': 59 | return m('div', { class: 'ssb-chess-status-text' }, 'Draw.'); 60 | default: 61 | return m('div'); 62 | } 63 | } 64 | 65 | function renderHistory() { 66 | return m('div', { 67 | class: '', 68 | }, [renderPlayers(), renderMoveHistory(), renderStatus()]); 69 | } 70 | 71 | function renderHalfMove(pgn, moveNumber) { 72 | const clickHandler = () => { 73 | if (moveNumber === latestMove) { 74 | moveNumberSelected = 'live'; 75 | } else { 76 | moveNumberSelected = moveNumber; 77 | } 78 | 79 | moveSelectedObservable.set(moveNumberSelected); 80 | }; 81 | 82 | const highlightClass = ((moveNumberSelected === moveNumber) 83 | || (moveNumber === latestMove && moveNumberSelected === 'live')) ? ' ssb-chess-pgn-move-selected' : ''; 84 | 85 | return m('div', { 86 | class: `ssb-chess-pgn-cell${highlightClass}`, 87 | onclick: clickHandler, 88 | }, pgn); 89 | } 90 | 91 | function renderMoveHistory() { 92 | const halves = R.splitEvery(2, pgnMoves); 93 | 94 | return m('div', { class: 'ssb-chess-pgn-moves-list' }, 95 | halves.map((half, halfNumber) => m('div', { 96 | class: 'ssb-chess-pgn-move', 97 | }, [ 98 | renderHalfMove(half[0], ((halfNumber + 1) * 2) - 1), 99 | renderHalfMove(half[1], (halfNumber + 1) * 2), 100 | ]))); 101 | } 102 | 103 | function hasChatInputBoxFocused() { 104 | return document.activeElement.className.indexOf('ssb-embedded-chat-input-box') > -1; 105 | } 106 | 107 | function handleArrowKeys() { 108 | const left = 37; 109 | const up = 38; 110 | const right = 39; 111 | const down = 40; 112 | 113 | document.onkeydown = function (evt) { 114 | if (!UserLocationUtils.chessAppIsVisible() || hasChatInputBoxFocused()) { 115 | return; 116 | } 117 | 118 | evt = evt || window.event; 119 | if (evt.keyCode === left && (moveNumberSelected !== 0)) { 120 | if (moveNumberSelected === 'live') { 121 | moveNumberSelected = latestMove; 122 | } 123 | 124 | moveNumberSelected -= 1; 125 | } else if (evt.keyCode === right && moveNumberSelected !== 'live') { 126 | moveNumberSelected += 1; 127 | 128 | if (moveNumberSelected === latestMove) { 129 | moveNumberSelected = 'live'; 130 | } 131 | } else if (evt.keyCode === up) { 132 | moveNumberSelected = 0; 133 | } else if (evt.keyCode === down) { 134 | moveNumberSelected = 'live'; 135 | } 136 | 137 | const allArrowKeys = [left, up, right, down]; 138 | 139 | if (allArrowKeys.indexOf(evt.keyCode) !== -1) { 140 | moveSelectedObservable.set(moveNumberSelected); 141 | } 142 | 143 | m.redraw(); 144 | }; 145 | } 146 | 147 | /** 148 | * This observable changes as the user selects old positions in the move 149 | * history to view the move of. The value emitted is the ply number of the 150 | * move 151 | */ 152 | function getMoveSelectedObservable() { 153 | return moveSelectedObservable; 154 | } 155 | 156 | function scrollToBottomIfLive() { 157 | if (moveNumberSelected === 'live') { 158 | const moveListElement = document.getElementsByClassName('ssb-chess-pgn-moves-list')[0]; 159 | if (moveListElement) { 160 | moveListElement.scrollTop = moveListElement.scrollHeight; 161 | } 162 | } 163 | } 164 | 165 | function updateModelOnGameUpdates() { 166 | const w = watch(gameObservable, (situation) => { 167 | if (situation) { 168 | ({ pgnMoves, status, players } = situation); 169 | gameSituation = situation; 170 | 171 | latestMove = situation.ply; 172 | } 173 | }); 174 | 175 | watchesToClear.push(w); 176 | } 177 | 178 | function goToLiveMode() { 179 | moveNumberSelected = 'live'; 180 | moveSelectedObservable.set(moveNumberSelected); 181 | } 182 | 183 | function scrollToBottomOnGameUpdates() { 184 | const w = watch(gameObservable, () => { 185 | scrollToBottomIfLive(); 186 | m.redraw(); 187 | }); 188 | 189 | watchesToClear.push(w); 190 | } 191 | 192 | return { 193 | view: renderHistory, 194 | oninit: () => { 195 | updateModelOnGameUpdates(); 196 | }, 197 | oncreate: () => { 198 | handleArrowKeys(); 199 | scrollToBottomOnGameUpdates(); 200 | }, 201 | onremove: () => { 202 | watchesToClear.forEach(w => w()); 203 | watchesToClear = []; 204 | }, 205 | getMoveSelectedObservable, 206 | goToLiveMode, 207 | }; 208 | }; 209 | -------------------------------------------------------------------------------- /ui/game/gameActions.js: -------------------------------------------------------------------------------- 1 | const m = require('mithril'); 2 | const onceTrue = require('mutant/once-true'); 3 | const Value = require('mutant/value'); 4 | const when = require('mutant/when'); 5 | const watch = require('mutant/watch'); 6 | const computed = require('mutant/computed'); 7 | 8 | module.exports = (gameMoveCtrl, inviteCtrl, myIdent, situationObservable) => { 9 | let watchesToClear = []; 10 | 11 | let observing = true; 12 | 13 | const moveConfirmationObservable = makeMoveObservationListener(); 14 | const resignationConfirmationObservable = makeResignConfirmationListener(); 15 | 16 | function moveConfirmButtons() { 17 | const confirmMove = () => { 18 | moveConfirmationObservable.set({ 19 | moveNeedsConfirmed: false, 20 | confirmed: true, 21 | }); 22 | }; 23 | 24 | const cancelMove = () => { 25 | moveConfirmationObservable.set({ 26 | moveNeedsConfirmed: false, 27 | confirmed: false, 28 | }); 29 | }; 30 | 31 | return m('div', { 32 | class: 'ssb-chess-move-confirm-buttons', 33 | }, [ 34 | m('button', { 35 | onclick: confirmMove, 36 | }, 'Confirm'), 37 | m('button', { 38 | onclick: cancelMove, 39 | }, 'Cancel'), 40 | ]); 41 | } 42 | 43 | function renderResignConfirmation() { 44 | const confirmText = 'Yes'; 45 | const cancelText = 'No'; 46 | 47 | let doResignation = () => { 48 | resignGame(); 49 | cancelResignationConfirmation(); 50 | }; 51 | 52 | return m('div', {class: 'ssb-chess-resign-confirmation'}, [ 53 | m('p', {class: 'ssb-chess-resign-confirmation-prompt'}, "Really resign?"), 54 | m('div', {class: "ssb-chess-resign-confirmation-buttons"}, [ 55 | renderResignConfirmationButton(confirmText, doResignation), 56 | renderResignConfirmationButton(cancelText, cancelResignationConfirmation) 57 | ]) 58 | ]); 59 | 60 | } 61 | 62 | function cancelResignationConfirmation() { 63 | resignationConfirmationObservable.set(defaultResignationObservableValues()); 64 | m.redraw(); 65 | } 66 | 67 | function renderResignConfirmationButton(text, cb) { 68 | return m('button', { 69 | onclick: cb, 70 | class: 'ssb-chess-resign-confirmation-button' 71 | }, text); 72 | } 73 | 74 | function resignGame() { 75 | onceTrue(situationObservable, 76 | (situation) => { 77 | if (situation && situation.status.status === 'started') { 78 | gameMoveCtrl.resignGame(situation.gameId, situation.latestUpdateMsg); 79 | } 80 | }); 81 | } 82 | 83 | function resignButton() { 84 | 85 | let showResignationConfirmation = () => { 86 | var resignState = defaultResignationObservableValues(); 87 | resignState.resignationNeedsConfirmed = true; 88 | 89 | resignationConfirmationObservable.set(resignState); 90 | 91 | m.redraw(); 92 | } 93 | 94 | return m('button', { 95 | onclick: showResignationConfirmation, 96 | }, 'Resign'); 97 | } 98 | 99 | function handlePgnExport(gameId) { 100 | const url = '/games/:gameId/pgn'; 101 | 102 | m.route.set(url, { 103 | gameId: btoa(gameId), 104 | }); 105 | } 106 | 107 | function renderRematchInfo() { 108 | 109 | var offerRematch = function (event) { 110 | 111 | // We don't want the user being able to click the button twice 112 | this.disabled = true; 113 | 114 | onceTrue(situationObservable, situation => { 115 | var gameId = situation.gameId; 116 | 117 | var otherPlayer = situation.getOtherPlayer(myIdent); 118 | 119 | if (!otherPlayer) { 120 | throw new Error("Expected play to be participating in game when offering rematch."); 121 | } 122 | 123 | var myColour = situation.getWhitePlayer().id === myIdent ? "white" : "black"; 124 | 125 | inviteCtrl.inviteToPlay(otherPlayer.id, myColour, gameId); 126 | }) 127 | 128 | } 129 | 130 | return computed([situationObservable], situation => { 131 | if (!situation.currentPlayerIsInGame()) { 132 | return m('div'); 133 | } 134 | else if (situation.rematches.length === 0) { 135 | return m('button', { class: "ssb-game-action-button", onclick: offerRematch}, "Rematch"); 136 | } 137 | else { 138 | return m('div', {}, situation.rematches.map(renderRematchState)); 139 | } 140 | 141 | })(); 142 | } 143 | 144 | function renderRematchState(rematchInfo) { 145 | var goToGame = (gameId) => { 146 | const gameRoute = `/games/${btoa(gameId)}`; 147 | m.route.set(gameRoute); 148 | } 149 | 150 | var acceptInvite = (gameId) => { 151 | inviteCtrl.acceptChallenge(gameId); 152 | goToGame(gameId); 153 | } 154 | 155 | if (rematchInfo.status === "invited" && rematchInfo.isMyInvite) { 156 | return m('div', {class: "ssb-game-rematch-invite-text"}, "Awaiting rematch invite being accepted"); 157 | } else if (rematchInfo.status === "invited" && !rematchInfo.isMyInvite) { 158 | return m('div', [ 159 | m('div', {class: "ssb-game-rematch-invite-text"}, 'Rematch?'), 160 | m('button', {class: "ssb-game-rematch-accept-button", onclick: () => acceptInvite(rematchInfo.gameId)}, 'Accept') 161 | ]); 162 | } else if (rematchInfo.status === "accepted") { 163 | return m('button', {class: "ssb-game-action-button", onclick: () => goToGame(rematchInfo.gameId)}, "Go to rematch"); 164 | } else { 165 | return m('div'); 166 | } 167 | } 168 | 169 | function postGameButtons() { 170 | const exportPgn = () => { 171 | onceTrue(situationObservable, (situation) => { 172 | handlePgnExport(situation.gameId); 173 | }); 174 | }; 175 | 176 | return m('div', [ 177 | m('button', { 178 | onclick: exportPgn, 179 | class: "ssb-game-action-button", 180 | title: 'Export game.', 181 | }, 'Export game'), 182 | renderRematchInfo() 183 | ]) 184 | } 185 | 186 | function isObserving(situation) { 187 | return situation.players[myIdent] == null; 188 | } 189 | 190 | function makeMoveObservationListener() { 191 | const value = Value(); 192 | 193 | value.set({ 194 | moveNeedsConfirmed: false, 195 | moveConfirmed: false, 196 | }); 197 | 198 | return value; 199 | } 200 | 201 | function defaultResignationObservableValues () { 202 | return { 203 | resignationNeedsConfirmed: false, 204 | resignationConfirmed: false, 205 | } 206 | } 207 | 208 | function makeResignConfirmationListener() { 209 | const value = Value(defaultResignationObservableValues); 210 | 211 | value.set(defaultResignationObservableValues()); 212 | 213 | return value; 214 | } 215 | 216 | function moveNeedsConfirmed() { 217 | 218 | return computed(moveConfirmationObservable, 219 | confirmation => confirmation.moveNeedsConfirmed); 220 | } 221 | 222 | function resignationNeedsConfirmed() { 223 | return computed( 224 | resignationConfirmationObservable, 225 | confirmation => confirmation.resignationNeedsConfirmed 226 | ); 227 | } 228 | 229 | function usualButtons() { 230 | const gameInProgress = computed( 231 | situationObservable, 232 | situation => situation && (situation.status.status === 'started'), 233 | ); 234 | 235 | return when(gameInProgress, resignButton(), postGameButtons()); 236 | } 237 | 238 | return { 239 | view: () => { 240 | if (observing) { 241 | return postGameButtons(); 242 | } 243 | 244 | return m('div', { 245 | class: 'ssb-game-actions', 246 | }, 247 | when( 248 | resignationNeedsConfirmed(), 249 | renderResignConfirmation(), 250 | when(moveNeedsConfirmed(), moveConfirmButtons(), usualButtons())() 251 | )() 252 | ) 253 | }, 254 | oninit() { 255 | const w = watch(situationObservable, (situation) => { 256 | observing = isObserving(situation); 257 | }); 258 | 259 | watchesToClear.push(w); 260 | }, 261 | onremove: () => { 262 | watchesToClear.forEach(w => w()); 263 | watchesToClear = []; 264 | }, 265 | showMoveConfirmation() { 266 | moveConfirmationObservable.set({ 267 | moveNeedsConfirmed: true, 268 | confirmed: false, 269 | }); 270 | 271 | return moveConfirmationObservable; 272 | }, 273 | hideMoveConfirmation() { 274 | moveConfirmationObservable.set({ 275 | moveNeedsConfirmed: false, 276 | confirmed: false, 277 | }); 278 | 279 | m.redraw(); 280 | }, 281 | 282 | }; 283 | }; 284 | -------------------------------------------------------------------------------- /ui/game/gameView.js: -------------------------------------------------------------------------------- 1 | const m = require('mithril'); 2 | const { Chessground } = require('chessground'); 3 | const PubSub = require('pubsub-js'); 4 | const Value = require('mutant/value'); 5 | const watchAll = require('mutant/watch-all'); 6 | const computed = require('mutant/computed'); 7 | const when = require('mutant/when'); 8 | const { Howl } = require('howler'); 9 | 10 | const EmbeddedChat = require('ssb-embedded-chat'); 11 | const ActionButtons = require('./gameActions'); 12 | const GameHistory = require('./gameHistory'); 13 | const PromotionBox = require('./promote'); 14 | 15 | const PieceGraveyard = require('./PieceGraveyard'); 16 | const NextPreviousButtons = require('./nextGameControl'); 17 | 18 | const pull = require('pull-stream') 19 | 20 | module.exports = (dataAccess, gameCtrl, observables, settings) => { 21 | const myIdent = gameCtrl.getMyIdent(); 22 | 23 | const { situationObservable, gamesWhereMyMove, observableGames } = observables; 24 | 25 | let chessGround = null; 26 | const chessGroundObservable = Value(); 27 | 28 | const gameHistory = GameHistory(situationObservable, myIdent); 29 | const actionButtons = ActionButtons( 30 | gameCtrl.getMoveCtrl(), 31 | gameCtrl.getInviteCtrl(), 32 | myIdent, 33 | situationObservable, 34 | ); 35 | 36 | const gameHistoryObs = gameHistory.getMoveSelectedObservable(); 37 | 38 | const pieceGraveOpponent = PieceGraveyard( 39 | chessGroundObservable, 40 | situationObservable, 41 | gameHistoryObs, 42 | myIdent, 43 | false, 44 | ); 45 | 46 | const pieceGraveMe = PieceGraveyard( 47 | chessGroundObservable, 48 | situationObservable, 49 | gameHistoryObs, 50 | myIdent, 51 | true, 52 | ); 53 | 54 | const nextPrevButtons = NextPreviousButtons(situationObservable, arrowScrollsThroughGameCollection()); 55 | 56 | const rootDir = `${__dirname.replace('/ui/game', '')}/`; 57 | 58 | const moveSound = new Howl({ 59 | src: [`${rootDir}assets/sounds/Move.mp3`], 60 | }); 61 | 62 | const captureSound = new Howl({ 63 | src: [`${rootDir}assets/sounds/Capture.mp3`], 64 | }); 65 | 66 | function arrowScrollsThroughGameCollection() { 67 | const playerIsPlaying = computed([situationObservable], (situation) => situation.currentPlayerIsInGame()); 68 | 69 | return when(playerIsPlaying, 70 | gamesWhereMyMove, observableGames 71 | ); 72 | } 73 | 74 | function plyToColourToPlay(ply) { 75 | return ply % 2 === 0 ? 'white' : 'black'; 76 | } 77 | 78 | function isPromotionMove(cg, dest) { 79 | return (dest[1] === '8' || dest[1] === '1') 80 | && cg.state.pieces[dest] 81 | && (cg.state.pieces[dest].role === 'pawn'); 82 | } 83 | 84 | function renderBoard(gameId) { 85 | const chessDom = m('div', { 86 | class: 'cg-wrap ssb-chess-board-large', 87 | id: gameId, 88 | }); 89 | 90 | return m('div', { class: 'ssb-chess-board-area' }, [ 91 | chessDom, 92 | m(nextPrevButtons) 93 | ]); 94 | } 95 | 96 | function renderChat(gameId) { 97 | return m('div', { 98 | class: 'ssb-chess-chat', 99 | id: `chat-${gameId}`, 100 | }); 101 | } 102 | 103 | function setNotMovable(conf) { 104 | conf.movable = {}; 105 | conf.movable.color = null; 106 | } 107 | 108 | function watchForMoveConfirmation(situation, onConfirm, validMoves) { 109 | if (!settings.getMoveConfirmation()) { 110 | // If move confirmation is not enabled, perform the move immediately 111 | onConfirm(); 112 | return; 113 | } 114 | 115 | const confirmedObs = actionButtons.showMoveConfirmation(); 116 | m.redraw(); 117 | 118 | const watches = computed( 119 | [confirmedObs, gameHistory.getMoveSelectedObservable()], 120 | (confirmed, moveSelected) => ({ 121 | moveConfirmed: confirmed, 122 | moveSelected, 123 | }), 124 | ); 125 | 126 | const removeConfirmationListener = watches((value) => { 127 | if (value.moveConfirmed.confirmed) { 128 | onConfirm(); 129 | } else if (value.moveSelected !== 'live' || value.moveConfirmed.confirmed === false) { 130 | const oldConfig = situationToChessgroundConfig(situation, 'live', validMoves); 131 | 132 | if (value.moveSelected === 'live') { 133 | chessGround.set(oldConfig); 134 | } 135 | } 136 | 137 | removeConfirmationListener(); 138 | actionButtons.hideMoveConfirmation(); 139 | }); 140 | } 141 | 142 | function situationToChessgroundConfig(situation, moveSelected, validMoves) { 143 | const playerColour = situation.players[myIdent] ? situation.players[myIdent].colour : 'white'; 144 | 145 | const colourToPlay = plyToColourToPlay(situation.ply); 146 | 147 | const config = { 148 | fen: situation.fen, 149 | orientation: playerColour, 150 | turnColor: colourToPlay, 151 | ply: situation.ply, 152 | check: situation.check, 153 | movable: { 154 | dests: validMoves, 155 | free: false, 156 | color: situation.toMove === myIdent ? playerColour : null, 157 | events: { 158 | after: (orig, dest) => { 159 | if (isPromotionMove(chessGround, dest)) { 160 | const chessboardDom = document.getElementsByClassName('cg-wrap')[0]; 161 | 162 | PromotionBox(chessboardDom, colourToPlay, dest[0], 163 | (promotingToPiece) => { 164 | const onConfirmMove = () => gameCtrl.getMoveCtrl().makeMove( 165 | situation.gameId, 166 | orig, 167 | dest, 168 | promotingToPiece, 169 | ); 170 | watchForMoveConfirmation(situation, onConfirmMove); 171 | }).renderPromotionOptionsOverlay(); 172 | } else { 173 | const onConfirmMove = () => { 174 | gameCtrl.getMoveCtrl().makeMove(situation.gameId, orig, dest); 175 | }; 176 | 177 | watchForMoveConfirmation(situation, onConfirmMove, validMoves); 178 | } 179 | 180 | const notMovable = { 181 | check: false, 182 | movable: { 183 | color: null, 184 | }, 185 | }; 186 | 187 | chessGround.set(notMovable); 188 | }, 189 | }, 190 | }, 191 | }; 192 | 193 | if (situation.lastMove) { 194 | config.lastMove = [situation.lastMove.orig, situation.lastMove.dest]; 195 | } 196 | 197 | if (moveSelected !== 'live') { 198 | resetConfigToOlderPosition(situation, config, moveSelected); 199 | } 200 | 201 | return config; 202 | } 203 | 204 | function resetConfigToOlderPosition(newSituation, newConfig, moveNumber) { 205 | setNotMovable(newConfig); 206 | newConfig.fen = newSituation.fenHistory[moveNumber]; 207 | 208 | if (moveNumber > 0) { 209 | newConfig.lastMove = [ 210 | newSituation.origDests[moveNumber - 1].orig, 211 | newSituation.origDests[moveNumber - 1].dest, 212 | ]; 213 | } else { 214 | newConfig.lastMove = null; 215 | } 216 | 217 | const colourToPlay = plyToColourToPlay(moveNumber); 218 | newConfig.turnColor = colourToPlay; 219 | 220 | newConfig.check = newSituation.isCheckOnMoveNumber(moveNumber); 221 | } 222 | 223 | function makeEmbeddedChat(situation) { 224 | const config = { 225 | rootMessageId: situation.gameId, 226 | chatMessageType: 'chess_chat', 227 | chatMessageField: 'msg', 228 | chatboxEnabled: true, 229 | previousChatId: situation.rematchFrom 230 | }; 231 | 232 | if (situation.players[myIdent] != null) { 233 | config.isPublic = false; 234 | config.participants = Object.keys(situation.players); 235 | } else { 236 | config.isPublic = true; 237 | } 238 | 239 | config.publishPublic = dataAccess.publishPublicChessMessage.bind(dataAccess); 240 | config.publishPrivate = dataAccess.publishPrivateChessMessage.bind(dataAccess); 241 | 242 | config.getChatStream = (gameId, live) => 243 | pull(dataAccess.allGameMessages(gameId, live), pull.filter(msg => !msg.sync && msg.value && msg.value.content.type == "chess_chat")); 244 | 245 | config.aboutSelfChangeStream = (since) => dataAccess.aboutSelfChangesUserIds(since); 246 | 247 | config.getDisplayName = (id, cb) => dataAccess.getPlayerDisplayName(id, cb); 248 | 249 | const chat = EmbeddedChat(config); 250 | 251 | return chat; 252 | } 253 | 254 | function playMoveSound(situation, newConfig, cg, moveSelected) { 255 | if (newConfig.fen !== cg.state.fen && moveSelected !== 0) { 256 | const pgnMove = moveSelected === 'live' ? situation.pgnMoves[ 257 | situation.pgnMoves.length - 1] : situation.pgnMoves[moveSelected - 1]; 258 | 259 | // Hacky way of determining if it's a capture move. 260 | if (pgnMove.indexOf('x') !== -1) { 261 | captureSound.play(); 262 | } else { 263 | moveSound.play(); 264 | } 265 | } 266 | } 267 | 268 | return { 269 | 270 | view(ctrl) { 271 | const gameId = atob(ctrl.attrs.gameId); 272 | 273 | return m('div', { 274 | class: 'ssb-chess-board-background-blue3 merida ssb-chess-game-layout', 275 | }, [renderChat(gameId), renderBoard(gameId), 276 | m('div', { class: 'ssb-chess-history-area' }, [ 277 | m(pieceGraveOpponent), 278 | m(gameHistory), 279 | m(actionButtons), 280 | m(pieceGraveMe) 281 | ])]); 282 | }, 283 | oncreate(vNode) { 284 | const gameId = atob(vNode.attrs.gameId); 285 | const boardDom = document.getElementById(gameId); 286 | const chatDom = document.getElementById(`chat-${gameId}`); 287 | 288 | const originalSituation = situationObservable(); 289 | 290 | const config = situationToChessgroundConfig(originalSituation, 'live', {}); 291 | chessGround = Chessground(boardDom, config); 292 | chessGroundObservable.set(chessGround); 293 | 294 | this.embeddedChat = makeEmbeddedChat(originalSituation); 295 | chatDom.appendChild(this.embeddedChat.getChatboxElement()); 296 | 297 | const validMovesObservable = gameCtrl.getMovesFinderCtrl() 298 | .validMovesForSituationObs(situationObservable); 299 | 300 | this.removeWatches = watchAll([situationObservable, 301 | gameHistory.getMoveSelectedObservable(), validMovesObservable], 302 | (newSituation, moveSelected, validMoves) => { 303 | const newConfig = situationToChessgroundConfig(newSituation, moveSelected, validMoves); 304 | 305 | if (settings.getPlaySounds()) { 306 | playMoveSound(newSituation, newConfig, chessGround, moveSelected); 307 | } 308 | 309 | chessGround.set(newConfig); 310 | }); 311 | 312 | PubSub.publish('viewing_game', { 313 | gameId, 314 | }); 315 | }, 316 | onremove() { 317 | if (this.removeWatches) { 318 | this.removeWatches(); 319 | } 320 | 321 | if (chessGround) { 322 | // Yuck. This has been null for people at this stage before. Perhaps onremove 323 | // can be called before oncreate in edge cases? 324 | chessGround.destroy(); 325 | } 326 | 327 | if (this.embeddedChat) { 328 | this.embeddedChat.destroy(); 329 | } 330 | 331 | PubSub.publish('exited_game'); 332 | }, 333 | }; 334 | }; 335 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU General Public License is a free, copyleft license for 11 | software and other kinds of works. 12 | 13 | The licenses for most software and other practical works are designed 14 | to take away your freedom to share and change the works. By contrast, 15 | the GNU General Public License is intended to guarantee your freedom to 16 | share and change all versions of a program--to make sure it remains free 17 | software for all its users. We, the Free Software Foundation, use the 18 | GNU General Public License for most of our software; it applies also to 19 | any other work released this way by its authors. You can apply it to 20 | your programs, too. 21 | 22 | When we speak of free software, we are referring to freedom, not 23 | price. Our General Public Licenses are designed to make sure that you 24 | have the freedom to distribute copies of free software (and charge for 25 | them if you wish), that you receive source code or can get it if you 26 | want it, that you can change the software or use pieces of it in new 27 | free programs, and that you know you can do these things. 28 | 29 | To protect your rights, we need to prevent others from denying you 30 | these rights or asking you to surrender the rights. Therefore, you have 31 | certain responsibilities if you distribute copies of the software, or if 32 | you modify it: responsibilities to respect the freedom of others. 33 | 34 | For example, if you distribute copies of such a program, whether 35 | gratis or for a fee, you must pass on to the recipients the same 36 | freedoms that you received. You must make sure that they, too, receive 37 | or can get the source code. And you must show them these terms so they 38 | know their rights. 39 | 40 | Developers that use the GNU GPL protect your rights with two steps: 41 | (1) assert copyright on the software, and (2) offer you this License 42 | giving you legal permission to copy, distribute and/or modify it. 43 | 44 | For the developers' and authors' protection, the GPL clearly explains 45 | that there is no warranty for this free software. For both users' and 46 | authors' sake, the GPL requires that modified versions be marked as 47 | changed, so that their problems will not be attributed erroneously to 48 | authors of previous versions. 49 | 50 | Some devices are designed to deny users access to install or run 51 | modified versions of the software inside them, although the manufacturer 52 | can do so. This is fundamentally incompatible with the aim of 53 | protecting users' freedom to change the software. The systematic 54 | pattern of such abuse occurs in the area of products for individuals to 55 | use, which is precisely where it is most unacceptable. Therefore, we 56 | have designed this version of the GPL to prohibit the practice for those 57 | products. If such problems arise substantially in other domains, we 58 | stand ready to extend this provision to those domains in future versions 59 | of the GPL, as needed to protect the freedom of users. 60 | 61 | Finally, every program is threatened constantly by software patents. 62 | States should not allow patents to restrict development and use of 63 | software on general-purpose computers, but in those that do, we wish to 64 | avoid the special danger that patents applied to a free program could 65 | make it effectively proprietary. To prevent this, the GPL assures that 66 | patents cannot be used to render the program non-free. 67 | 68 | The precise terms and conditions for copying, distribution and 69 | modification follow. 70 | 71 | TERMS AND CONDITIONS 72 | 73 | 0. Definitions. 74 | 75 | "This License" refers to version 3 of the GNU General Public License. 76 | 77 | "Copyright" also means copyright-like laws that apply to other kinds of 78 | works, such as semiconductor masks. 79 | 80 | "The Program" refers to any copyrightable work licensed under this 81 | License. Each licensee is addressed as "you". "Licensees" and 82 | "recipients" may be individuals or organizations. 83 | 84 | To "modify" a work means to copy from or adapt all or part of the work 85 | in a fashion requiring copyright permission, other than the making of an 86 | exact copy. The resulting work is called a "modified version" of the 87 | earlier work or a work "based on" the earlier work. 88 | 89 | A "covered work" means either the unmodified Program or a work based 90 | on the Program. 91 | 92 | To "propagate" a work means to do anything with it that, without 93 | permission, would make you directly or secondarily liable for 94 | infringement under applicable copyright law, except executing it on a 95 | computer or modifying a private copy. Propagation includes copying, 96 | distribution (with or without modification), making available to the 97 | public, and in some countries other activities as well. 98 | 99 | To "convey" a work means any kind of propagation that enables other 100 | parties to make or receive copies. Mere interaction with a user through 101 | a computer network, with no transfer of a copy, is not conveying. 102 | 103 | An interactive user interface displays "Appropriate Legal Notices" 104 | to the extent that it includes a convenient and prominently visible 105 | feature that (1) displays an appropriate copyright notice, and (2) 106 | tells the user that there is no warranty for the work (except to the 107 | extent that warranties are provided), that licensees may convey the 108 | work under this License, and how to view a copy of this License. If 109 | the interface presents a list of user commands or options, such as a 110 | menu, a prominent item in the list meets this criterion. 111 | 112 | 1. Source Code. 113 | 114 | The "source code" for a work means the preferred form of the work 115 | for making modifications to it. "Object code" means any non-source 116 | form of a work. 117 | 118 | A "Standard Interface" means an interface that either is an official 119 | standard defined by a recognized standards body, or, in the case of 120 | interfaces specified for a particular programming language, one that 121 | is widely used among developers working in that language. 122 | 123 | The "System Libraries" of an executable work include anything, other 124 | than the work as a whole, that (a) is included in the normal form of 125 | packaging a Major Component, but which is not part of that Major 126 | Component, and (b) serves only to enable use of the work with that 127 | Major Component, or to implement a Standard Interface for which an 128 | implementation is available to the public in source code form. A 129 | "Major Component", in this context, means a major essential component 130 | (kernel, window system, and so on) of the specific operating system 131 | (if any) on which the executable work runs, or a compiler used to 132 | produce the work, or an object code interpreter used to run it. 133 | 134 | The "Corresponding Source" for a work in object code form means all 135 | the source code needed to generate, install, and (for an executable 136 | work) run the object code and to modify the work, including scripts to 137 | control those activities. However, it does not include the work's 138 | System Libraries, or general-purpose tools or generally available free 139 | programs which are used unmodified in performing those activities but 140 | which are not part of the work. For example, Corresponding Source 141 | includes interface definition files associated with source files for 142 | the work, and the source code for shared libraries and dynamically 143 | linked subprograms that the work is specifically designed to require, 144 | such as by intimate data communication or control flow between those 145 | subprograms and other parts of the work. 146 | 147 | The Corresponding Source need not include anything that users 148 | can regenerate automatically from other parts of the Corresponding 149 | Source. 150 | 151 | The Corresponding Source for a work in source code form is that 152 | same work. 153 | 154 | 2. Basic Permissions. 155 | 156 | All rights granted under this License are granted for the term of 157 | copyright on the Program, and are irrevocable provided the stated 158 | conditions are met. This License explicitly affirms your unlimited 159 | permission to run the unmodified Program. The output from running a 160 | covered work is covered by this License only if the output, given its 161 | content, constitutes a covered work. This License acknowledges your 162 | rights of fair use or other equivalent, as provided by copyright law. 163 | 164 | You may make, run and propagate covered works that you do not 165 | convey, without conditions so long as your license otherwise remains 166 | in force. You may convey covered works to others for the sole purpose 167 | of having them make modifications exclusively for you, or provide you 168 | with facilities for running those works, provided that you comply with 169 | the terms of this License in conveying all material for which you do 170 | not control copyright. Those thus making or running the covered works 171 | for you must do so exclusively on your behalf, under your direction 172 | and control, on terms that prohibit them from making any copies of 173 | your copyrighted material outside their relationship with you. 174 | 175 | Conveying under any other circumstances is permitted solely under 176 | the conditions stated below. Sublicensing is not allowed; section 10 177 | makes it unnecessary. 178 | 179 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 180 | 181 | No covered work shall be deemed part of an effective technological 182 | measure under any applicable law fulfilling obligations under article 183 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 184 | similar laws prohibiting or restricting circumvention of such 185 | measures. 186 | 187 | When you convey a covered work, you waive any legal power to forbid 188 | circumvention of technological measures to the extent such circumvention 189 | is effected by exercising rights under this License with respect to 190 | the covered work, and you disclaim any intention to limit operation or 191 | modification of the work as a means of enforcing, against the work's 192 | users, your or third parties' legal rights to forbid circumvention of 193 | technological measures. 194 | 195 | 4. Conveying Verbatim Copies. 196 | 197 | You may convey verbatim copies of the Program's source code as you 198 | receive it, in any medium, provided that you conspicuously and 199 | appropriately publish on each copy an appropriate copyright notice; 200 | keep intact all notices stating that this License and any 201 | non-permissive terms added in accord with section 7 apply to the code; 202 | keep intact all notices of the absence of any warranty; and give all 203 | recipients a copy of this License along with the Program. 204 | 205 | You may charge any price or no price for each copy that you convey, 206 | and you may offer support or warranty protection for a fee. 207 | 208 | 5. Conveying Modified Source Versions. 209 | 210 | You may convey a work based on the Program, or the modifications to 211 | produce it from the Program, in the form of source code under the 212 | terms of section 4, provided that you also meet all of these conditions: 213 | 214 | a) The work must carry prominent notices stating that you modified 215 | it, and giving a relevant date. 216 | 217 | b) The work must carry prominent notices stating that it is 218 | released under this License and any conditions added under section 219 | 7. This requirement modifies the requirement in section 4 to 220 | "keep intact all notices". 221 | 222 | c) You must license the entire work, as a whole, under this 223 | License to anyone who comes into possession of a copy. This 224 | License will therefore apply, along with any applicable section 7 225 | additional terms, to the whole of the work, and all its parts, 226 | regardless of how they are packaged. This License gives no 227 | permission to license the work in any other way, but it does not 228 | invalidate such permission if you have separately received it. 229 | 230 | d) If the work has interactive user interfaces, each must display 231 | Appropriate Legal Notices; however, if the Program has interactive 232 | interfaces that do not display Appropriate Legal Notices, your 233 | work need not make them do so. 234 | 235 | A compilation of a covered work with other separate and independent 236 | works, which are not by their nature extensions of the covered work, 237 | and which are not combined with it such as to form a larger program, 238 | in or on a volume of a storage or distribution medium, is called an 239 | "aggregate" if the compilation and its resulting copyright are not 240 | used to limit the access or legal rights of the compilation's users 241 | beyond what the individual works permit. Inclusion of a covered work 242 | in an aggregate does not cause this License to apply to the other 243 | parts of the aggregate. 244 | 245 | 6. Conveying Non-Source Forms. 246 | 247 | You may convey a covered work in object code form under the terms 248 | of sections 4 and 5, provided that you also convey the 249 | machine-readable Corresponding Source under the terms of this License, 250 | in one of these ways: 251 | 252 | a) Convey the object code in, or embodied in, a physical product 253 | (including a physical distribution medium), accompanied by the 254 | Corresponding Source fixed on a durable physical medium 255 | customarily used for software interchange. 256 | 257 | b) Convey the object code in, or embodied in, a physical product 258 | (including a physical distribution medium), accompanied by a 259 | written offer, valid for at least three years and valid for as 260 | long as you offer spare parts or customer support for that product 261 | model, to give anyone who possesses the object code either (1) a 262 | copy of the Corresponding Source for all the software in the 263 | product that is covered by this License, on a durable physical 264 | medium customarily used for software interchange, for a price no 265 | more than your reasonable cost of physically performing this 266 | conveying of source, or (2) access to copy the 267 | Corresponding Source from a network server at no charge. 268 | 269 | c) Convey individual copies of the object code with a copy of the 270 | written offer to provide the Corresponding Source. This 271 | alternative is allowed only occasionally and noncommercially, and 272 | only if you received the object code with such an offer, in accord 273 | with subsection 6b. 274 | 275 | d) Convey the object code by offering access from a designated 276 | place (gratis or for a charge), and offer equivalent access to the 277 | Corresponding Source in the same way through the same place at no 278 | further charge. You need not require recipients to copy the 279 | Corresponding Source along with the object code. If the place to 280 | copy the object code is a network server, the Corresponding Source 281 | may be on a different server (operated by you or a third party) 282 | that supports equivalent copying facilities, provided you maintain 283 | clear directions next to the object code saying where to find the 284 | Corresponding Source. Regardless of what server hosts the 285 | Corresponding Source, you remain obligated to ensure that it is 286 | available for as long as needed to satisfy these requirements. 287 | 288 | e) Convey the object code using peer-to-peer transmission, provided 289 | you inform other peers where the object code and Corresponding 290 | Source of the work are being offered to the general public at no 291 | charge under subsection 6d. 292 | 293 | A separable portion of the object code, whose source code is excluded 294 | from the Corresponding Source as a System Library, need not be 295 | included in conveying the object code work. 296 | 297 | A "User Product" is either (1) a "consumer product", which means any 298 | tangible personal property which is normally used for personal, family, 299 | or household purposes, or (2) anything designed or sold for incorporation 300 | into a dwelling. In determining whether a product is a consumer product, 301 | doubtful cases shall be resolved in favor of coverage. For a particular 302 | product received by a particular user, "normally used" refers to a 303 | typical or common use of that class of product, regardless of the status 304 | of the particular user or of the way in which the particular user 305 | actually uses, or expects or is expected to use, the product. A product 306 | is a consumer product regardless of whether the product has substantial 307 | commercial, industrial or non-consumer uses, unless such uses represent 308 | the only significant mode of use of the product. 309 | 310 | "Installation Information" for a User Product means any methods, 311 | procedures, authorization keys, or other information required to install 312 | and execute modified versions of a covered work in that User Product from 313 | a modified version of its Corresponding Source. The information must 314 | suffice to ensure that the continued functioning of the modified object 315 | code is in no case prevented or interfered with solely because 316 | modification has been made. 317 | 318 | If you convey an object code work under this section in, or with, or 319 | specifically for use in, a User Product, and the conveying occurs as 320 | part of a transaction in which the right of possession and use of the 321 | User Product is transferred to the recipient in perpetuity or for a 322 | fixed term (regardless of how the transaction is characterized), the 323 | Corresponding Source conveyed under this section must be accompanied 324 | by the Installation Information. But this requirement does not apply 325 | if neither you nor any third party retains the ability to install 326 | modified object code on the User Product (for example, the work has 327 | been installed in ROM). 328 | 329 | The requirement to provide Installation Information does not include a 330 | requirement to continue to provide support service, warranty, or updates 331 | for a work that has been modified or installed by the recipient, or for 332 | the User Product in which it has been modified or installed. Access to a 333 | network may be denied when the modification itself materially and 334 | adversely affects the operation of the network or violates the rules and 335 | protocols for communication across the network. 336 | 337 | Corresponding Source conveyed, and Installation Information provided, 338 | in accord with this section must be in a format that is publicly 339 | documented (and with an implementation available to the public in 340 | source code form), and must require no special password or key for 341 | unpacking, reading or copying. 342 | 343 | 7. Additional Terms. 344 | 345 | "Additional permissions" are terms that supplement the terms of this 346 | License by making exceptions from one or more of its conditions. 347 | Additional permissions that are applicable to the entire Program shall 348 | be treated as though they were included in this License, to the extent 349 | that they are valid under applicable law. If additional permissions 350 | apply only to part of the Program, that part may be used separately 351 | under those permissions, but the entire Program remains governed by 352 | this License without regard to the additional permissions. 353 | 354 | When you convey a copy of a covered work, you may at your option 355 | remove any additional permissions from that copy, or from any part of 356 | it. (Additional permissions may be written to require their own 357 | removal in certain cases when you modify the work.) You may place 358 | additional permissions on material, added by you to a covered work, 359 | for which you have or can give appropriate copyright permission. 360 | 361 | Notwithstanding any other provision of this License, for material you 362 | add to a covered work, you may (if authorized by the copyright holders of 363 | that material) supplement the terms of this License with terms: 364 | 365 | a) Disclaiming warranty or limiting liability differently from the 366 | terms of sections 15 and 16 of this License; or 367 | 368 | b) Requiring preservation of specified reasonable legal notices or 369 | author attributions in that material or in the Appropriate Legal 370 | Notices displayed by works containing it; or 371 | 372 | c) Prohibiting misrepresentation of the origin of that material, or 373 | requiring that modified versions of such material be marked in 374 | reasonable ways as different from the original version; or 375 | 376 | d) Limiting the use for publicity purposes of names of licensors or 377 | authors of the material; or 378 | 379 | e) Declining to grant rights under trademark law for use of some 380 | trade names, trademarks, or service marks; or 381 | 382 | f) Requiring indemnification of licensors and authors of that 383 | material by anyone who conveys the material (or modified versions of 384 | it) with contractual assumptions of liability to the recipient, for 385 | any liability that these contractual assumptions directly impose on 386 | those licensors and authors. 387 | 388 | All other non-permissive additional terms are considered "further 389 | restrictions" within the meaning of section 10. If the Program as you 390 | received it, or any part of it, contains a notice stating that it is 391 | governed by this License along with a term that is a further 392 | restriction, you may remove that term. If a license document contains 393 | a further restriction but permits relicensing or conveying under this 394 | License, you may add to a covered work material governed by the terms 395 | of that license document, provided that the further restriction does 396 | not survive such relicensing or conveying. 397 | 398 | If you add terms to a covered work in accord with this section, you 399 | must place, in the relevant source files, a statement of the 400 | additional terms that apply to those files, or a notice indicating 401 | where to find the applicable terms. 402 | 403 | Additional terms, permissive or non-permissive, may be stated in the 404 | form of a separately written license, or stated as exceptions; 405 | the above requirements apply either way. 406 | 407 | 8. Termination. 408 | 409 | You may not propagate or modify a covered work except as expressly 410 | provided under this License. Any attempt otherwise to propagate or 411 | modify it is void, and will automatically terminate your rights under 412 | this License (including any patent licenses granted under the third 413 | paragraph of section 11). 414 | 415 | However, if you cease all violation of this License, then your 416 | license from a particular copyright holder is reinstated (a) 417 | provisionally, unless and until the copyright holder explicitly and 418 | finally terminates your license, and (b) permanently, if the copyright 419 | holder fails to notify you of the violation by some reasonable means 420 | prior to 60 days after the cessation. 421 | 422 | Moreover, your license from a particular copyright holder is 423 | reinstated permanently if the copyright holder notifies you of the 424 | violation by some reasonable means, this is the first time you have 425 | received notice of violation of this License (for any work) from that 426 | copyright holder, and you cure the violation prior to 30 days after 427 | your receipt of the notice. 428 | 429 | Termination of your rights under this section does not terminate the 430 | licenses of parties who have received copies or rights from you under 431 | this License. If your rights have been terminated and not permanently 432 | reinstated, you do not qualify to receive new licenses for the same 433 | material under section 10. 434 | 435 | 9. Acceptance Not Required for Having Copies. 436 | 437 | You are not required to accept this License in order to receive or 438 | run a copy of the Program. Ancillary propagation of a covered work 439 | occurring solely as a consequence of using peer-to-peer transmission 440 | to receive a copy likewise does not require acceptance. However, 441 | nothing other than this License grants you permission to propagate or 442 | modify any covered work. These actions infringe copyright if you do 443 | not accept this License. Therefore, by modifying or propagating a 444 | covered work, you indicate your acceptance of this License to do so. 445 | 446 | 10. Automatic Licensing of Downstream Recipients. 447 | 448 | Each time you convey a covered work, the recipient automatically 449 | receives a license from the original licensors, to run, modify and 450 | propagate that work, subject to this License. You are not responsible 451 | for enforcing compliance by third parties with this License. 452 | 453 | An "entity transaction" is a transaction transferring control of an 454 | organization, or substantially all assets of one, or subdividing an 455 | organization, or merging organizations. If propagation of a covered 456 | work results from an entity transaction, each party to that 457 | transaction who receives a copy of the work also receives whatever 458 | licenses to the work the party's predecessor in interest had or could 459 | give under the previous paragraph, plus a right to possession of the 460 | Corresponding Source of the work from the predecessor in interest, if 461 | the predecessor has it or can get it with reasonable efforts. 462 | 463 | You may not impose any further restrictions on the exercise of the 464 | rights granted or affirmed under this License. For example, you may 465 | not impose a license fee, royalty, or other charge for exercise of 466 | rights granted under this License, and you may not initiate litigation 467 | (including a cross-claim or counterclaim in a lawsuit) alleging that 468 | any patent claim is infringed by making, using, selling, offering for 469 | sale, or importing the Program or any portion of it. 470 | 471 | 11. Patents. 472 | 473 | A "contributor" is a copyright holder who authorizes use under this 474 | License of the Program or a work on which the Program is based. The 475 | work thus licensed is called the contributor's "contributor version". 476 | 477 | A contributor's "essential patent claims" are all patent claims 478 | owned or controlled by the contributor, whether already acquired or 479 | hereafter acquired, that would be infringed by some manner, permitted 480 | by this License, of making, using, or selling its contributor version, 481 | but do not include claims that would be infringed only as a 482 | consequence of further modification of the contributor version. For 483 | purposes of this definition, "control" includes the right to grant 484 | patent sublicenses in a manner consistent with the requirements of 485 | this License. 486 | 487 | Each contributor grants you a non-exclusive, worldwide, royalty-free 488 | patent license under the contributor's essential patent claims, to 489 | make, use, sell, offer for sale, import and otherwise run, modify and 490 | propagate the contents of its contributor version. 491 | 492 | In the following three paragraphs, a "patent license" is any express 493 | agreement or commitment, however denominated, not to enforce a patent 494 | (such as an express permission to practice a patent or covenant not to 495 | sue for patent infringement). To "grant" such a patent license to a 496 | party means to make such an agreement or commitment not to enforce a 497 | patent against the party. 498 | 499 | If you convey a covered work, knowingly relying on a patent license, 500 | and the Corresponding Source of the work is not available for anyone 501 | to copy, free of charge and under the terms of this License, through a 502 | publicly available network server or other readily accessible means, 503 | then you must either (1) cause the Corresponding Source to be so 504 | available, or (2) arrange to deprive yourself of the benefit of the 505 | patent license for this particular work, or (3) arrange, in a manner 506 | consistent with the requirements of this License, to extend the patent 507 | license to downstream recipients. "Knowingly relying" means you have 508 | actual knowledge that, but for the patent license, your conveying the 509 | covered work in a country, or your recipient's use of the covered work 510 | in a country, would infringe one or more identifiable patents in that 511 | country that you have reason to believe are valid. 512 | 513 | If, pursuant to or in connection with a single transaction or 514 | arrangement, you convey, or propagate by procuring conveyance of, a 515 | covered work, and grant a patent license to some of the parties 516 | receiving the covered work authorizing them to use, propagate, modify 517 | or convey a specific copy of the covered work, then the patent license 518 | you grant is automatically extended to all recipients of the covered 519 | work and works based on it. 520 | 521 | A patent license is "discriminatory" if it does not include within 522 | the scope of its coverage, prohibits the exercise of, or is 523 | conditioned on the non-exercise of one or more of the rights that are 524 | specifically granted under this License. You may not convey a covered 525 | work if you are a party to an arrangement with a third party that is 526 | in the business of distributing software, under which you make payment 527 | to the third party based on the extent of your activity of conveying 528 | the work, and under which the third party grants, to any of the 529 | parties who would receive the covered work from you, a discriminatory 530 | patent license (a) in connection with copies of the covered work 531 | conveyed by you (or copies made from those copies), or (b) primarily 532 | for and in connection with specific products or compilations that 533 | contain the covered work, unless you entered into that arrangement, 534 | or that patent license was granted, prior to 28 March 2007. 535 | 536 | Nothing in this License shall be construed as excluding or limiting 537 | any implied license or other defenses to infringement that may 538 | otherwise be available to you under applicable patent law. 539 | 540 | 12. No Surrender of Others' Freedom. 541 | 542 | If conditions are imposed on you (whether by court order, agreement or 543 | otherwise) that contradict the conditions of this License, they do not 544 | excuse you from the conditions of this License. If you cannot convey a 545 | covered work so as to satisfy simultaneously your obligations under this 546 | License and any other pertinent obligations, then as a consequence you may 547 | not convey it at all. For example, if you agree to terms that obligate you 548 | to collect a royalty for further conveying from those to whom you convey 549 | the Program, the only way you could satisfy both those terms and this 550 | License would be to refrain entirely from conveying the Program. 551 | 552 | 13. Use with the GNU Affero General Public License. 553 | 554 | Notwithstanding any other provision of this License, you have 555 | permission to link or combine any covered work with a work licensed 556 | under version 3 of the GNU Affero General Public License into a single 557 | combined work, and to convey the resulting work. The terms of this 558 | License will continue to apply to the part which is the covered work, 559 | but the special requirements of the GNU Affero General Public License, 560 | section 13, concerning interaction through a network will apply to the 561 | combination as such. 562 | 563 | 14. Revised Versions of this License. 564 | 565 | The Free Software Foundation may publish revised and/or new versions of 566 | the GNU General Public License from time to time. Such new versions will 567 | be similar in spirit to the present version, but may differ in detail to 568 | address new problems or concerns. 569 | 570 | Each version is given a distinguishing version number. If the 571 | Program specifies that a certain numbered version of the GNU General 572 | Public License "or any later version" applies to it, you have the 573 | option of following the terms and conditions either of that numbered 574 | version or of any later version published by the Free Software 575 | Foundation. If the Program does not specify a version number of the 576 | GNU General Public License, you may choose any version ever published 577 | by the Free Software Foundation. 578 | 579 | If the Program specifies that a proxy can decide which future 580 | versions of the GNU General Public License can be used, that proxy's 581 | public statement of acceptance of a version permanently authorizes you 582 | to choose that version for the Program. 583 | 584 | Later license versions may give you additional or different 585 | permissions. However, no additional obligations are imposed on any 586 | author or copyright holder as a result of your choosing to follow a 587 | later version. 588 | 589 | 15. Disclaimer of Warranty. 590 | 591 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 592 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 593 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 594 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 595 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 596 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 597 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 598 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 599 | 600 | 16. Limitation of Liability. 601 | 602 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 603 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 604 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 605 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 606 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 607 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 608 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 609 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 610 | SUCH DAMAGES. 611 | 612 | 17. Interpretation of Sections 15 and 16. 613 | 614 | If the disclaimer of warranty and limitation of liability provided 615 | above cannot be given local legal effect according to their terms, 616 | reviewing courts shall apply local law that most closely approximates 617 | an absolute waiver of all civil liability in connection with the 618 | Program, unless a warranty or assumption of liability accompanies a 619 | copy of the Program in return for a fee. 620 | 621 | END OF TERMS AND CONDITIONS 622 | 623 | How to Apply These Terms to Your New Programs 624 | 625 | If you develop a new program, and you want it to be of the greatest 626 | possible use to the public, the best way to achieve this is to make it 627 | free software which everyone can redistribute and change under these terms. 628 | 629 | To do so, attach the following notices to the program. It is safest 630 | to attach them to the start of each source file to most effectively 631 | state the exclusion of warranty; and each file should have at least 632 | the "copyright" line and a pointer to where the full notice is found. 633 | 634 | Copyright (C) 2018 Gordon Martin 635 | 636 | This program is free software: you can redistribute it and/or modify 637 | it under the terms of the GNU General Public License as published by 638 | the Free Software Foundation, either version 3 of the License, or 639 | (at your option) any later version. 640 | 641 | This program is distributed in the hope that it will be useful, 642 | but WITHOUT ANY WARRANTY; without even the implied warranty of 643 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 644 | GNU General Public License for more details. 645 | 646 | You should have received a copy of the GNU General Public License 647 | along with this program. If not, see . 648 | 649 | Also add information on how to contact you by electronic and paper mail. 650 | 651 | If the program does terminal interaction, make it output a short 652 | notice like this when it starts in an interactive mode: 653 | 654 | Copyright (C) 655 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 656 | This is free software, and you are welcome to redistribute it 657 | under certain conditions; type `show c' for details. 658 | 659 | The hypothetical commands `show w' and `show c' should show the appropriate 660 | parts of the General Public License. Of course, your program's commands 661 | might be different; for a GUI interface, you would use an "about box". 662 | 663 | You should also get your employer (if you work as a programmer) or school, 664 | if any, to sign a "copyright disclaimer" for the program, if necessary. 665 | For more information on this, and how to apply and follow the GNU GPL, see 666 | . 667 | 668 | The GNU General Public License does not permit incorporating your program 669 | into proprietary programs. If your program is a subroutine library, you 670 | may consider it more useful to permit linking proprietary applications with 671 | the library. If this is what you want to do, use the GNU Lesser General 672 | Public License instead of this License. But first, please read 673 | . 674 | --------------------------------------------------------------------------------