UNPKG

7.56 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 = (sbot, 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. 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',
70 'chess_invite_accept',
71 'chess_move',
72 'chess_game_end'];
73
74 const messageType = msg.value.content.type;
75 const isSituationMsg = relevantMessageTypes.find(m => m === messageType);
76 return isSituationMsg !== undefined;
77 }
78
79 function getSituationObservable(gameId) {
80
81 const gameMessages = backlinkUtils.getFilteredBackLinks(gameId, {
82 filter: isSituationalChessMessage,
83 });
84
85 const msgRoot = getRootMessage(gameId);
86
87 const players = computed([msgRoot], (msg) => {
88 // msg may be null if we don't have the root message yet.
89 if (!msg) return null;
90 else return MutantUtils.promiseToMutant(getPlayers(msg))
91 }
92 );
93
94 const rematchState = getRematchState(gameId, gameMessages);
95
96 return computed([msgRoot, myIdent, players, gameMessages, gameMessages.sync, rematchState], (
97 rootMessage, ident, p, gameMessagesBacklinks, isSynced, rematchInfo
98
99 ) => {
100 if (!rootMessage || !isSynced || !p) return null;
101 return makeSituation(gameId, rootMessage, ident, p, gameMessagesBacklinks, rematchInfo)
102 });
103 }
104
105 function getRootMessage(gameId) {
106 var result = Value();
107
108 sbot.get(gameId, (err, msg) => {
109 if (msg) {
110 result.set(msg);
111 } else {
112 console.log(err);
113
114 // It's possible to get the reply to the first invite message before the root
115 // message in rare cases, which can result in an error (because the message)
116 // isn't found - so we just return null and then return null for the whole
117 // observable
118 return null;
119 }
120 });
121
122 return result;
123 }
124
125 function getRematchState(gameId, gameMessagesObservable) {
126 return computed([gameMessagesObservable], (gameMessages) => {
127
128 if (!gameMessages) return [];
129
130 const rematchInvites = gameMessages.filter(msg => msg.value.content.type === "chess_invite" && msg.value.content.root === gameId);
131
132 const gameStates = rematchInvites.map(msg => {
133
134 var situation = getSituationSummaryObservable(msg.key);
135
136 return computed([situation], gameState => {
137
138 // This observable value is initially pending until we've fetched the necessary data to know whether the invite
139 // has been accepted or not
140 if (!gameState) return {
141 status: "pending"
142 };
143
144 return {
145 gameId: msg.key,
146 status: gameState.status.status === "invited" ? "invited" : "accepted",
147 isMyInvite: msg.value.author === myIdent
148 }
149 })
150
151 });
152
153 return MutantArray(gameStates);
154 });
155 }
156
157 function getSituationSummaryObservable(gameId) {
158 return computed([getSituationObservable(gameId)], (situation) => {
159 if (situation == null) {
160 return null;
161 }
162 return situationToSummary(situation);
163 });
164 }
165
166 function getSituation(gameId) {
167 return MutantUtils.mutantToPromise(getSituationObservable(gameId));
168 }
169
170 function makeMove(
171 gameId,
172 ply,
173 originSquare,
174 destinationSquare,
175 promotion,
176 pgnMove,
177 fen,
178 respondsTo,
179 ) {
180 const post = {
181 type: 'chess_move',
182 ply,
183 root: gameId,
184 orig: originSquare,
185 dest: destinationSquare,
186 pgnMove,
187 fen,
188 };
189
190 if (promotion) {
191 post.promotion = promotion;
192 }
193
194 if (respondsTo) {
195 post.branch = respondsTo;
196 }
197
198 return new Promise((resolve, reject) => {
199 sbot.publish(post, (err, msg) => {
200 if (err) {
201 reject(err);
202 } else {
203 resolve(msg);
204 }
205 });
206 });
207 }
208
209 function addPropertyIfNotEmpty(obj, key, value) {
210 if (value) {
211 obj[key] = value;
212 }
213 }
214
215 function resignGame(gameId, respondsTo) {
216 const post = {
217 type: 'chess_game_end',
218 status: 'resigned',
219 root: gameId,
220 };
221
222 addPropertyIfNotEmpty(post, 'branch', respondsTo);
223
224 return new Promise((resolve, reject) => {
225 sbot.publish(post, (err, msg) => {
226 if (err) {
227 reject(err);
228 } else {
229 resolve(msg);
230 }
231 });
232 });
233 }
234
235 function endGame(
236 gameId,
237 status,
238 winner,
239 fen,
240 ply,
241 originSquare,
242 destinationSquare,
243 pgnMove,
244 respondsTo,
245 ) {
246 return new Promise((resolve, reject) => {
247 const post = {
248 type: 'chess_game_end',
249 status,
250 ply,
251 fen,
252 root: gameId,
253 };
254
255 // If game aborted or agreed to draw / claimed draw, some of these
256 // properties might not be relevant
257 addPropertyIfNotEmpty(post, 'winner', winner);
258 addPropertyIfNotEmpty(post, 'ply', ply);
259 addPropertyIfNotEmpty(post, 'orig', originSquare);
260 addPropertyIfNotEmpty(post, 'dest', destinationSquare);
261 addPropertyIfNotEmpty(post, 'pgnMove', pgnMove);
262 addPropertyIfNotEmpty(post, 'branch', respondsTo);
263
264 sbot.publish(post, (err, msg) => {
265 if (err) {
266 reject(err);
267 } else {
268 resolve(msg);
269 }
270 });
271 });
272 }
273
274 return {
275 getPlayers,
276 getSituation,
277 getSituationObservable,
278 getSituationSummaryObservable,
279 getSmallGameSummary,
280 makeMove,
281 resignGame,
282 endGame,
283 };
284};