ssb-chess-mithril 2.0.8 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -2,7 +2,9 @@
2
2
 
3
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
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.
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/soapdog/patchwork) using [depject](https://github.com/depject/depject) so that they can take care of things like discovering friends to play with, etc.
6
+
7
+ It is currently integrated into this fork of [patchwork](https://github.com/soapdog/patchwork/).
6
8
 
7
9
  ### Installation
8
10
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ssb-chess-mithril",
3
- "version": "2.0.8",
3
+ "version": "2.1.0",
4
4
  "description": "An ssb-chess client",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -24,7 +24,7 @@
24
24
  "pull-scroll": "^1.0.9",
25
25
  "pull-stream": "^3.6.14",
26
26
  "ramda": "^0.25.0",
27
- "ssb-chess": "4.0.4",
28
- "ssb-embedded-chat": "2.0.1"
27
+ "ssb-chess": "4.0.5",
28
+ "ssb-embedded-chat": "2.0.2"
29
29
  }
30
30
  }
@@ -0,0 +1,51 @@
1
+
2
+ const m = require('mithril')
3
+ const EmbeddedChat = require('ssb-embedded-chat');
4
+ const pull = require('pull-stream')
5
+
6
+ module.exports = function ChatBox(myIdent, dataAccess, situationObservable) {
7
+ return {
8
+ view: () => {
9
+ return m('div', {className: 'ssb-chess-chat'});
10
+ },
11
+ oncreate: function (vNode) {
12
+ const originalSituation = situationObservable();
13
+ this.embeddedChat = makeEmbeddedChat(myIdent, dataAccess, originalSituation);
14
+ vNode.dom.appendChild(this.embeddedChat.getChatboxElement());
15
+ },
16
+ onremove: function() {
17
+ this.embeddedChat.destroy();
18
+ }
19
+ }
20
+ }
21
+
22
+ function makeEmbeddedChat(myIdent, dataAccess, situation) {
23
+ const config = {
24
+ rootMessageId: situation.gameId,
25
+ chatMessageType: 'chess_chat',
26
+ chatMessageField: 'msg',
27
+ chatboxEnabled: true,
28
+ previousChatId: situation.rematchFrom
29
+ };
30
+
31
+ if (situation.players[myIdent] != null) {
32
+ config.isPublic = false;
33
+ config.participants = Object.keys(situation.players);
34
+ } else {
35
+ config.isPublic = true;
36
+ }
37
+
38
+ config.publishPublic = dataAccess.publishPublicChessMessage.bind(dataAccess);
39
+ config.publishPrivate = dataAccess.publishPrivateChessMessage.bind(dataAccess);
40
+
41
+ config.getChatStream = (gameId, live) =>
42
+ pull(dataAccess.allGameMessages(gameId, live), pull.filter(msg => !msg.sync && msg.value && msg.value.content.type == "chess_chat"));
43
+
44
+ config.aboutSelfChangeStream = (since) => dataAccess.aboutSelfChangesUserIds(since);
45
+
46
+ config.getDisplayName = (id, cb) => dataAccess.getPlayerDisplayName(id, cb);
47
+
48
+ const chat = EmbeddedChat(config);
49
+
50
+ return chat;
51
+ }
@@ -7,328 +7,282 @@ const computed = require('mutant/computed');
7
7
  const when = require('mutant/when');
8
8
  const { Howl } = require('howler');
9
9
 
10
- const EmbeddedChat = require('ssb-embedded-chat');
11
10
  const ActionButtons = require('./gameActions');
12
11
  const GameHistory = require('./gameHistory');
13
12
  const PromotionBox = require('./promote');
13
+ const ChatBox = require('./chat')
14
14
 
15
15
  const PieceGraveyard = require('./PieceGraveyard');
16
16
  const NextPreviousButtons = require('./nextGameControl');
17
17
 
18
- const pull = require('pull-stream')
18
+ module.exports = (dataAccess, gameCtrl, observables, settings) => {
19
+ const myIdent = gameCtrl.getMyIdent();
19
20
 
20
- module.exports = (dataAccess, gameCtrl, observables, settings) => {
21
- const myIdent = gameCtrl.getMyIdent();
21
+ const { situationObservable, gamesWhereMyMove, observableGames } = observables;
22
22
 
23
- const { situationObservable, gamesWhereMyMove, observableGames } = observables;
23
+ let chessGround = null;
24
+ const chessGroundObservable = Value();
24
25
 
25
- let chessGround = null;
26
- const chessGroundObservable = Value();
26
+ const gameHistory = GameHistory(situationObservable, myIdent);
27
+ const actionButtons = ActionButtons(
28
+ gameCtrl.getMoveCtrl(),
29
+ gameCtrl.getInviteCtrl(),
30
+ myIdent,
31
+ situationObservable,
32
+ );
27
33
 
28
- const gameHistory = GameHistory(situationObservable, myIdent);
29
- const actionButtons = ActionButtons(
30
- gameCtrl.getMoveCtrl(),
31
- gameCtrl.getInviteCtrl(),
32
- myIdent,
33
- situationObservable,
34
- );
34
+ const gameHistoryObs = gameHistory.getMoveSelectedObservable();
35
35
 
36
- const gameHistoryObs = gameHistory.getMoveSelectedObservable();
36
+ const pieceGraveOpponent = PieceGraveyard(
37
+ chessGroundObservable,
38
+ situationObservable,
39
+ gameHistoryObs,
40
+ myIdent,
41
+ false,
42
+ );
37
43
 
38
- const pieceGraveOpponent = PieceGraveyard(
39
- chessGroundObservable,
40
- situationObservable,
41
- gameHistoryObs,
42
- myIdent,
43
- false,
44
- );
44
+ const pieceGraveMe = PieceGraveyard(
45
+ chessGroundObservable,
46
+ situationObservable,
47
+ gameHistoryObs,
48
+ myIdent,
49
+ true,
50
+ );
45
51
 
46
- const pieceGraveMe = PieceGraveyard(
47
- chessGroundObservable,
48
- situationObservable,
49
- gameHistoryObs,
50
- myIdent,
51
- true,
52
- );
52
+ const nextPrevButtons = NextPreviousButtons(situationObservable, arrowScrollsThroughGameCollection());
53
53
 
54
- const nextPrevButtons = NextPreviousButtons(situationObservable, arrowScrollsThroughGameCollection());
54
+ const rootDir = `${__dirname.replace('/ui/game', '')}/`;
55
55
 
56
- const rootDir = `${__dirname.replace('/ui/game', '')}/`;
56
+ const moveSound = new Howl({
57
+ src: [`${rootDir}assets/sounds/Move.mp3`],
58
+ });
57
59
 
58
- const moveSound = new Howl({
59
- src: [`${rootDir}assets/sounds/Move.mp3`],
60
- });
60
+ const captureSound = new Howl({
61
+ src: [`${rootDir}assets/sounds/Capture.mp3`],
62
+ });
61
63
 
62
- const captureSound = new Howl({
63
- src: [`${rootDir}assets/sounds/Capture.mp3`],
64
- });
64
+ function arrowScrollsThroughGameCollection() {
65
+ const playerIsPlaying = computed([situationObservable], (situation) => situation.currentPlayerIsInGame());
65
66
 
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
- });
67
+ return when(playerIsPlaying,
68
+ gamesWhereMyMove, observableGames
69
+ );
70
+ }
71
+
72
+ function plyToColourToPlay(ply) {
73
+ return ply % 2 === 0 ? 'white' : 'black';
74
+ }
75
+
76
+ function isPromotionMove(cg, dest) {
77
+ return (dest[1] === '8' || dest[1] === '1')
78
+ && cg.state.pieces[dest]
79
+ && (cg.state.pieces[dest].role === 'pawn');
80
+ }
81
+
82
+ function renderBoard(gameId) {
83
+ const chessDom = m('div', {
84
+ class: 'cg-wrap ssb-chess-board-large',
85
+ id: gameId,
86
+ });
89
87
 
90
- return m('div', { class: 'ssb-chess-board-area' }, [
91
- chessDom,
92
- m(nextPrevButtons)
93
- ]);
88
+ return m('div', { class: 'ssb-chess-board-area' }, [
89
+ chessDom,
90
+ m(nextPrevButtons)
91
+ ]);
92
+ }
93
+
94
+ function setNotMovable(conf) {
95
+ conf.movable = {};
96
+ conf.movable.color = null;
97
+ }
98
+
99
+ function watchForMoveConfirmation(situation, onConfirm, validMoves) {
100
+ if (!settings.getMoveConfirmation()) {
101
+ // If move confirmation is not enabled, perform the move immediately
102
+ onConfirm();
103
+ return;
94
104
  }
95
105
 
96
- function renderChat(gameId) {
97
- return m('div', {
98
- class: 'ssb-chess-chat',
99
- id: `chat-${gameId}`,
100
- });
101
- }
106
+ const confirmedObs = actionButtons.showMoveConfirmation();
107
+ m.redraw();
102
108
 
103
- function setNotMovable(conf) {
104
- conf.movable = {};
105
- conf.movable.color = null;
106
- }
109
+ const watches = computed(
110
+ [confirmedObs, gameHistory.getMoveSelectedObservable()],
111
+ (confirmed, moveSelected) => ({
112
+ moveConfirmed: confirmed,
113
+ moveSelected,
114
+ }),
115
+ );
107
116
 
108
- function watchForMoveConfirmation(situation, onConfirm, validMoves) {
109
- if (!settings.getMoveConfirmation()) {
110
- // If move confirmation is not enabled, perform the move immediately
117
+ const removeConfirmationListener = watches((value) => {
118
+ if (value.moveConfirmed.confirmed) {
111
119
  onConfirm();
112
- return;
113
- }
120
+ } else if (value.moveSelected !== 'live' || value.moveConfirmed.confirmed === false) {
121
+ const oldConfig = situationToChessgroundConfig(situation, 'live', validMoves);
114
122
 
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
- }
123
+ if (value.moveSelected === 'live') {
124
+ chessGround.set(oldConfig);
135
125
  }
126
+ }
136
127
 
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
- },
128
+ removeConfirmationListener();
129
+ actionButtons.hideMoveConfirmation();
130
+ });
131
+ }
132
+
133
+ function situationToChessgroundConfig(situation, moveSelected, validMoves) {
134
+ const playerColour = situation.players[myIdent] ? situation.players[myIdent].colour : 'white';
135
+
136
+ const colourToPlay = plyToColourToPlay(situation.ply);
137
+
138
+ const config = {
139
+ fen: situation.fen,
140
+ orientation: playerColour,
141
+ turnColor: colourToPlay,
142
+ ply: situation.ply,
143
+ check: situation.check,
144
+ movable: {
145
+ dests: validMoves,
146
+ free: false,
147
+ color: situation.toMove === myIdent ? playerColour : null,
148
+ events: {
149
+ after: (orig, dest) => {
150
+ if (isPromotionMove(chessGround, dest)) {
151
+ const chessboardDom = document.getElementsByClassName('cg-wrap')[0];
152
+
153
+ PromotionBox(chessboardDom, colourToPlay, dest[0],
154
+ (promotingToPiece) => {
155
+ const onConfirmMove = () => gameCtrl.getMoveCtrl().makeMove(
156
+ situation.gameId,
157
+ orig,
158
+ dest,
159
+ promotingToPiece,
160
+ );
161
+ watchForMoveConfirmation(situation, onConfirmMove);
162
+ }).renderPromotionOptionsOverlay();
163
+ } else {
164
+ const onConfirmMove = () => {
165
+ gameCtrl.getMoveCtrl().makeMove(situation.gameId, orig, dest);
185
166
  };
186
167
 
187
- chessGround.set(notMovable);
188
- },
189
- },
190
- },
191
- };
168
+ watchForMoveConfirmation(situation, onConfirmMove, validMoves);
169
+ }
192
170
 
193
- if (situation.lastMove) {
194
- config.lastMove = [situation.lastMove.orig, situation.lastMove.dest];
195
- }
171
+ const notMovable = {
172
+ check: false,
173
+ movable: {
174
+ color: null,
175
+ },
176
+ };
196
177
 
197
- if (moveSelected !== 'live') {
198
- resetConfigToOlderPosition(situation, config, moveSelected);
199
- }
178
+ chessGround.set(notMovable);
179
+ },
180
+ },
181
+ },
182
+ };
200
183
 
201
- return config;
184
+ if (situation.lastMove) {
185
+ config.lastMove = [situation.lastMove.orig, situation.lastMove.dest];
202
186
  }
203
187
 
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);
188
+ if (moveSelected !== 'live') {
189
+ resetConfigToOlderPosition(situation, config, moveSelected);
221
190
  }
222
191
 
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
- }
192
+ return config;
193
+ }
238
194
 
239
- config.publishPublic = dataAccess.publishPublicChessMessage.bind(dataAccess);
240
- config.publishPrivate = dataAccess.publishPrivateChessMessage.bind(dataAccess);
195
+ function resetConfigToOlderPosition(newSituation, newConfig, moveNumber) {
196
+ setNotMovable(newConfig);
197
+ newConfig.fen = newSituation.fenHistory[moveNumber];
241
198
 
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);
199
+ if (moveNumber > 0) {
200
+ newConfig.lastMove = [
201
+ newSituation.origDests[moveNumber - 1].orig,
202
+ newSituation.origDests[moveNumber - 1].dest,
203
+ ];
204
+ } else {
205
+ newConfig.lastMove = null;
206
+ }
248
207
 
249
- const chat = EmbeddedChat(config);
208
+ const colourToPlay = plyToColourToPlay(moveNumber);
209
+ newConfig.turnColor = colourToPlay;
250
210
 
251
- return chat;
252
- }
211
+ newConfig.check = newSituation.isCheckOnMoveNumber(moveNumber);
212
+ }
253
213
 
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];
214
+ function playMoveSound(situation, newConfig, cg, moveSelected) {
215
+ if (newConfig.fen !== cg.state.fen && moveSelected !== 0) {
216
+ const pgnMove = moveSelected === 'live' ? situation.pgnMoves[
217
+ situation.pgnMoves.length - 1] : situation.pgnMoves[moveSelected - 1];
258
218
 
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
- }
219
+ // Hacky way of determining if it's a capture move.
220
+ if (pgnMove.indexOf('x') !== -1) {
221
+ captureSound.play();
222
+ } else {
223
+ moveSound.play();
265
224
  }
266
225
  }
226
+ }
267
227
 
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);
228
+ const Chat = ChatBox(myIdent, dataAccess, situationObservable);
293
229
 
294
- this.embeddedChat = makeEmbeddedChat(originalSituation);
295
- chatDom.appendChild(this.embeddedChat.getChatboxElement());
230
+ return {
296
231
 
297
- const validMovesObservable = gameCtrl.getMovesFinderCtrl()
298
- .validMovesForSituationObs(situationObservable);
232
+ view(ctrl) {
233
+ const gameId = atob(ctrl.attrs.gameId);
299
234
 
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
- });
235
+ return m('div', {
236
+ class: 'ssb-chess-board-background-blue3 merida ssb-chess-game-layout',
237
+ }, [m(Chat), renderBoard(gameId),
238
+ m('div', { class: 'ssb-chess-history-area' }, [
239
+ m(pieceGraveOpponent),
240
+ m(gameHistory),
241
+ m(actionButtons),
242
+ m(pieceGraveMe)
243
+ ])]);
244
+ },
245
+ oncreate(vNode) {
246
+ const gameId = atob(vNode.attrs.gameId);
247
+ const boardDom = document.getElementById(gameId);
248
+
249
+ const originalSituation = situationObservable();
250
+
251
+ const config = situationToChessgroundConfig(originalSituation, 'live', {});
252
+ chessGround = Chessground(boardDom, config);
253
+ chessGroundObservable.set(chessGround);
254
+
255
+ const validMovesObservable = gameCtrl.getMovesFinderCtrl()
256
+ .validMovesForSituationObs(situationObservable);
257
+
258
+ this.removeWatches = watchAll([situationObservable,
259
+ gameHistory.getMoveSelectedObservable(), validMovesObservable],
260
+ (newSituation, moveSelected, validMoves) => {
261
+ const newConfig = situationToChessgroundConfig(newSituation, moveSelected, validMoves);
262
+
263
+ if (settings.getPlaySounds()) {
264
+ playMoveSound(newSituation, newConfig, chessGround, moveSelected);
265
+ }
311
266
 
312
- PubSub.publish('viewing_game', {
313
- gameId,
267
+ chessGround.set(newConfig);
314
268
  });
315
- },
316
- onremove() {
317
- if (this.removeWatches) {
318
- this.removeWatches();
319
- }
320
269
 
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
- }
270
+ PubSub.publish('viewing_game', {
271
+ gameId,
272
+ });
273
+ },
274
+ onremove() {
275
+ if (this.removeWatches) {
276
+ this.removeWatches();
277
+ }
326
278
 
327
- if (this.embeddedChat) {
328
- this.embeddedChat.destroy();
329
- }
279
+ if (chessGround) {
280
+ // Yuck. This has been null for people at this stage before. Perhaps onremove
281
+ // can be called before oncreate in edge cases?
282
+ chessGround.destroy();
283
+ }
330
284
 
331
- PubSub.publish('exited_game');
332
- },
333
- };
285
+ PubSub.publish('exited_game');
286
+ },
334
287
  };
288
+ };