├── .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 |
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 |
21 |
--------------------------------------------------------------------------------
/css/assets/mono/P.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
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 |
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 |
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 |
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 |
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 |
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 | 
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 |
--------------------------------------------------------------------------------