UNPKG

7.65 kBJavaScriptView Raw
1const computed = require('mutant/computed');
2const MutantUtils = require('./mutant_utils')();
3const MutantArray = require('mutant/array');
4const makeSituation = require('./model/situation');
5const Value = require('mutant/value');
6
7module.exports = (dataAccess, myIdent, backlinkUtils, socialCtrl) => {
8
9 function getPlayers(gameRootMessage) {
10 return new Promise((resolve, reject) => {
11 const authorId = gameRootMessage.author;
12 const invited = gameRootMessage.content.inviting;
13
14 const authorColour = gameRootMessage.content.myColor === 'white' ? gameRootMessage.content.myColor : 'black';
15 const players = {};
16
17 Promise.all([authorId, invited].map(socialCtrl.getDisplayName))
18 .then((names) => {
19 players[authorId] = {};
20 players[authorId].colour = authorColour;
21 [players[authorId].name] = names;
22 players[authorId].id = authorId;
23
24 players[invited] = {};
25 players[invited].colour = authorColour === 'white' ? 'black' : 'white';
26 [, players[invited].name] = names;
27 players[invited].id = invited;
28
29 resolve(players);
30 });
31 });
32 }
33
34 function situationToSummary(gameSituation) {
35 const summary = {
36 gameId: gameSituation.gameId,
37 fen: gameSituation.fen,
38 players: gameSituation.players,
39 toMove: gameSituation.toMove,
40 status: gameSituation.status,
41 lastMove: gameSituation.lastMove,
42 check: gameSituation.check,
43 lastUpdateTime: gameSituation.lastUpdateTime,
44 coloursToPlayer: gameSituation.coloursToPlayer
45 };
46
47 return summary;
48 }
49
50 /*
51 * Return just the FEN, players, and who's move it is.
52 * This might be used for a miniboard view of a game, for example.
53 */
54 function getSmallGameSummary(gameId) {
55 return MutantUtils.mutantToPromise(getSituationSummaryObservable(gameId));
56 }
57
58 /**
59 * Returns 'true' if the given scuttlebutt message is something that changes
60 * the chess board situation. For example, a move or invite accept. Returns false
61 * for messages that do not modify the situation (such as chat messages.)
62 */
63 function isSituationalChessMessage(msg) {
64 if (!msg.value || !msg.value.content) {
65 return false;
66 }
67
68 const relevantMessageTypes = [
69 'chess_invite_accept',
70 'chess_move',
71 'chess_game_end'];
72
73 const messageType = msg.value.content.type;
74 const isSituationMsg = relevantMessageTypes.find(m => m === messageType);
75 return isSituationMsg !== undefined;
76 }
77
78 function getSituationObservable(gameId) {
79
80 const gameMessages = backlinkUtils.getFilteredBackLinks(gameId, {
81 filter: isSituationalChessMessage,
82 });
83
84 const msgRoot = getRootMessage(gameId);
85
86 const players = computed([msgRoot], (msg) => {
87 // msg may be null if we don't have the root message yet.
88 if (!msg) return null;
89 else return MutantUtils.promiseToMutant(getPlayers(msg))
90 }
91 );
92
93 const rematchState = getRematchState(gameId, gameMessages);
94
95 return computed([msgRoot, myIdent, players, gameMessages, gameMessages.sync, rematchState], (
96 rootMessage, ident, p, gameMessagesBacklinks, isSynced, rematchInfo
97
98 ) => {
99 if (!rootMessage || !isSynced || !p) return null;
100 return makeSituation(gameId, rootMessage, ident, p, gameMessagesBacklinks, rematchInfo)
101 });
102 }
103
104 function getRootMessage(gameId) {
105 var result = Value();
106
107 dataAccess.getInviteMessage(gameId, (err, msg) => {
108 if (msg) {
109 result.set(msg);
110 } else {
111 console.log(err);
112
113 // It's possible to get the reply to the first invite message before the root
114 // message in rare cases, which can result in an error (because the message)
115 // isn't found - so we just return null and then return null for the whole
116 // observable
117 return null;
118 }
119 });
120
121 return result;
122 }
123
124 function getRematchState(gameId, gameMessagesObservable) {
125 return computed([gameMessagesObservable], (gameMessages) => {
126
127 if (!gameMessages) return [];
128
129 const rematchInvites = gameMessages.filter(msg => msg.value.content.type === "chess_invite" && msg.value.content.root === gameId);
130
131 const gameStates = rematchInvites.map(msg => {
132
133 var situation = getSituationSummaryObservable(msg.key);
134
135 return computed([situation], gameState => {
136
137 // This observable value is initially pending until we've fetched the necessary data to know whether the invite
138 // has been accepted or not
139 if (!gameState) return {
140 status: "pending"
141 };
142
143 return {
144 gameId: msg.key,
145 status: gameState.status.status === "invited" ? "invited" : "accepted",
146 isMyInvite: msg.value.author === myIdent
147 }
148 })
149
150 });
151
152 return MutantArray(gameStates);
153 });
154 }
155
156 function getSituationSummaryObservable(gameId) {
157 return computed([getSituationObservable(gameId)], (situation) => {
158 if (situation == null) {
159 return null;
160 }
161 return situationToSummary(situation);
162 });
163 }
164
165 function getSituation(gameId) {
166 return MutantUtils.mutantToPromise(getSituationObservable(gameId));
167 }
168
169 function makeMove(
170 gameId,
171 ply,
172 originSquare,
173 destinationSquare,
174 promotion,
175 pgnMove,
176 fen,
177 respondsTo,
178 ) {
179 const post = {
180 type: 'chess_move',
181 ply,
182 root: gameId,
183 orig: originSquare,
184 dest: destinationSquare,
185 pgnMove,
186 fen,
187 };
188
189 if (promotion) {
190 post.promotion = promotion;
191 }
192
193 if (respondsTo) {
194 post.branch = respondsTo;
195 }
196
197 return new Promise((resolve, reject) => {
198 dataAccess.publishPublicChessMessage(post, (err, msg) => {
199 if (err) {
200 reject(err);
201 } else {
202 resolve(msg);
203 }
204 });
205 });
206 }
207
208 function addPropertyIfNotEmpty(obj, key, value) {
209 if (value) {
210 obj[key] = value;
211 }
212 }
213
214 function resignGame(gameId, respondsTo) {
215 const post = {
216 type: 'chess_game_end',
217 status: 'resigned',
218 root: gameId,
219 };
220
221 addPropertyIfNotEmpty(post, 'branch', respondsTo);
222
223 return new Promise((resolve, reject) => {
224 dataAccess.publishPublicChessMessage(post, (err, msg) => {
225 if (err) {
226 reject(err);
227 } else {
228 resolve(msg);
229 }
230 });
231 });
232 }
233
234 function endGame(
235 gameId,
236 status,
237 winner,
238 fen,
239 ply,
240 originSquare,
241 destinationSquare,
242 pgnMove,
243 respondsTo,
244 ) {
245 return new Promise((resolve, reject) => {
246 const post = {
247 type: 'chess_game_end',
248 status,
249 ply,
250 fen,
251 root: gameId,
252 };
253
254 // If game aborted or agreed to draw / claimed draw, some of these
255 // properties might not be relevant
256 addPropertyIfNotEmpty(post, 'winner', winner);
257 addPropertyIfNotEmpty(post, 'ply', ply);
258 addPropertyIfNotEmpty(post, 'orig', originSquare);
259 addPropertyIfNotEmpty(post, 'dest', destinationSquare);
260 addPropertyIfNotEmpty(post, 'pgnMove', pgnMove);
261 addPropertyIfNotEmpty(post, 'branch', respondsTo);
262
263 dataAccess.publishPublicChessMessage(post, (err, msg) => {
264 if (err) {
265 reject(err);
266 } else {
267 resolve(msg);
268 }
269 });
270 });
271 }
272
273 return {
274 getPlayers,
275 getSituation,
276 getSituationObservable,
277 getSituationSummaryObservable,
278 getSmallGameSummary,
279 makeMove,
280 resignGame,
281 endGame,
282 };
283};