1 const db
= require("../utils/database");
2 const UserModel
= require("./User");
5 * Structure table Games:
7 * vid: integer (variant id)
8 * fenStart: varchar (initial position)
9 * fen: varchar (current position)
13 * score: varchar (result)
14 * scoreMsg: varchar ("Time", "Mutual agreement"...)
16 * drawOffer: char ('w','b' or '' for none)
17 * rematchOffer: char (similar to drawOffer)
19 * deletedByWhite: boolean
20 * deletedByBlack: boolean
21 * chatReadWhite: datetime
22 * chatReadBlack: datetime
24 * Structure table Moves:
26 * squares: varchar (description)
30 * Structure table Chats:
39 checkGameInfo: function(g
) {
41 g
.vid
.toString().match(/^[0-9]+$/) &&
42 g
.cadence
.match(/^[0-9dhms
+]+$/) &&
43 g
.fen
.match(/^[a-zA-Z0-9, /-]*$/) &&
44 g
.players
.length
== 2 &&
45 g
.players
.every(p
=> p
.id
.toString().match(/^[0-9]+$/))
49 incrementCounter: function(vid
, cb
) {
50 db
.serialize(function() {
53 "SET total = total + 1 " +
59 create: function(vid
, fen
, options
, cadence
, players
, cb
) {
60 db
.serialize(function() {
62 "INSERT INTO Games " +
64 "vid, fenStart, fen, options, " +
70 vid
+ ",'" + fen
+ "','" + fen
+ "',?," +
71 players
[0].id
+ "," + players
[1].id
+ "," +
72 "'" + cadence
+ "'," + Date
.now() +
74 db
.run(query
, options
, function(err
) {
75 cb(err
, { id: this.lastID
});
80 // TODO: some queries here could be async
81 getOne: function(id
, cb
) {
82 // NOTE: ignoring errors (shouldn't happen at this stage)
83 db
.serialize(function() {
86 "id, vid, fen, fenStart, cadence, created, " +
87 "white, black, options, score, scoreMsg, " +
88 "chatReadWhite, chatReadBlack, drawOffer, rematchOffer " +
91 db
.get(query
, (err
, gameInfo
) => {
93 cb(err
|| { errmsg: "Game not found" }, undefined);
99 "WHERE id IN (" + gameInfo
.white
+ "," + gameInfo
.black
+ ")";
100 db
.all(query
, (err2
, players
) => {
101 if (players
[0].id
== gameInfo
.black
) players
= players
.reverse();
102 // The original players' IDs info isn't required anymore
103 delete gameInfo
["white"];
104 delete gameInfo
["black"];
106 "SELECT squares, played, idx " +
109 db
.all(query
, (err3
, moves
) => {
111 "SELECT msg, name, added " +
114 db
.all(query
, (err4
, chats
) => {
115 const game
= Object
.assign(
131 // For display on Hall: no need for moves or chats
132 getObserved: function(uid
, cursor
, cb
) {
133 db
.serialize(function() {
135 "SELECT id, vid, cadence, options, created, score, white, black " +
137 "WHERE created < " + cursor
+ " ";
140 " AND white <> " + uid
+ " " +
141 " AND black <> " + uid
+ " ";
144 "ORDER BY created DESC " +
145 "LIMIT 20"; //TODO: 20 hard-coded...
146 db
.all(query
, (err
, games
) => {
147 // Query players names
150 if (!pids
[g
.white
]) pids
[g
.white
] = true;
151 if (!pids
[g
.black
]) pids
[g
.black
] = true;
153 UserModel
.getByIds(Object
.keys(pids
), (err2
, users
) => {
155 users
.forEach(u
=> { names
[u
.id
] = u
.name
; });
168 { id: g
.white
, name: names
[g
.white
] },
169 { id: g
.black
, name: names
[g
.black
] }
180 // For display on MyGames: registered user only
181 getRunning: function(uid
, cb
) {
182 db
.serialize(function() {
184 "SELECT id, vid, cadence, options, created, white, black " +
186 "WHERE score = '*' AND (white = " + uid
+ " OR black = " + uid
+ ")";
187 db
.all(query
, (err
, games
) => {
188 // Get movesCount (could be done in // with next query)
190 "SELECT gid, COUNT(*) AS nbMoves " +
192 "WHERE gid IN " + "(" + games
.map(g
=> g
.id
).join(",") + ") " +
194 db
.all(query
, (err
, mstats
) => {
195 let movesCounts
= {};
196 mstats
.forEach(ms
=> { movesCounts
[ms
.gid
] = ms
.nbMoves
; });
197 // Query player names
200 if (!pids
[g
.white
]) pids
[g
.white
] = true;
201 if (!pids
[g
.black
]) pids
[g
.black
] = true;
203 UserModel
.getByIds(Object
.keys(pids
), (err2
, users
) => {
205 users
.forEach(u
=> { names
[u
.id
] = u
.name
; });
217 movesCount: movesCounts
[g
.id
] || 0,
219 { id: g
.white
, name: names
[g
.white
] },
220 { id: g
.black
, name: names
[g
.black
] }
232 // These games could be deleted on some side. movesCount not required
233 getCompleted: function(uid
, cursor
, cb
) {
234 db
.serialize(function() {
236 "SELECT id, vid, cadence, options, created, score, scoreMsg, " +
237 "white, black, deletedByWhite, deletedByBlack " +
240 " score <> '*' AND" +
241 " created < " + cursor
+ " AND" +
244 " white = " + uid
+ " AND" +
245 " (deletedByWhite IS NULL OR NOT deletedByWhite)" +
249 " black = " + uid
+ " AND" +
250 " (deletedByBlack IS NULL OR NOT deletedByBlack)" +
254 "ORDER BY created DESC " +
256 db
.all(query
, (err
, games
) => {
257 // Query player names
260 if (!pids
[g
.white
]) pids
[g
.white
] = true;
261 if (!pids
[g
.black
]) pids
[g
.black
] = true;
263 UserModel
.getByIds(Object
.keys(pids
), (err2
, users
) => {
265 users
.forEach(u
=> { names
[u
.id
] = u
.name
; });
277 scoreMsg: g
.scoreMsg
,
279 { id: g
.white
, name: names
[g
.white
] },
280 { id: g
.black
, name: names
[g
.black
] }
282 deletedByWhite: g
.deletedByWhite
,
283 deletedByBlack: g
.deletedByBlack
293 getPlayers: function(id
, cb
) {
294 db
.serialize(function() {
296 "SELECT white, black " +
299 db
.get(query
, (err
, players
) => {
300 return cb(err
, players
);
305 checkGameUpdate: function(obj
) {
306 // Check all that is possible (required) in obj:
309 !obj
.move || !!(obj
.move.idx
.toString().match(/^[0-9]+$/))
311 !obj
.drawOffer
|| !!(obj
.drawOffer
.match(/^[wbtn]$/))
313 !obj
.rematchOffer
|| !!(obj
.rematchOffer
.match(/^[wbn]$/))
315 !obj
.fen
|| !!(obj
.fen
.match(/^[a-zA-Z0-9,.:{}\[\]" /-]*$/))
317 !obj
.score
|| !!(obj
.score
.match(/^[012?*\/-]+$/))
319 !obj
.chatRead
|| ['w','b'].includes(obj
.chatRead
)
321 !obj
.scoreMsg
|| !!(obj
.scoreMsg
.match(/^[a
-zA
-Z
]+$/))
323 !obj
.chat
|| UserModel
.checkNameEmail({name: obj
.chat
.name
})
328 // obj can have fields move, chat, fen, drawOffer and/or score + message
329 update: function(id
, obj
, cb
) {
330 db
.parallelize(function() {
335 // NOTE: if drawOffer is set, we should check that it's player's turn
336 // A bit overcomplicated. Let's trust the client on that for now...
337 if (!!obj
.drawOffer
) {
338 if (obj
.drawOffer
== "n")
339 // Special "None" update
341 modifs
+= "drawOffer = '" + obj
.drawOffer
+ "',";
343 if (!!obj
.rematchOffer
) {
344 if (obj
.rematchOffer
== "n")
345 // Special "None" update
346 obj
.rematchOffer
= "";
347 modifs
+= "rematchOffer = '" + obj
.rematchOffer
+ "',";
349 if (!!obj
.fen
) modifs
+= "fen = '" + obj
.fen
+ "',";
350 if (!!obj
.deletedBy
) {
351 const myColor
= obj
.deletedBy
== 'w' ? "White" : "Black";
352 modifs
+= "deletedBy" + myColor
+ " = true,";
354 if (!!obj
.chatRead
) {
355 const myColor
= obj
.chatRead
== 'w' ? "White" : "Black";
356 modifs
+= "chatRead" + myColor
+ " = " + Date
.now() + ",";
359 modifs
+= "score = '" + obj
.score
+ "'," +
360 "scoreMsg = '" + obj
.scoreMsg
+ "',";
362 const finishAndSendQuery
= () => {
363 modifs
= modifs
.slice(0, -1); //remove last comma
364 if (modifs
.length
> 0) {
365 updateQuery
+= modifs
+ " WHERE id = " + id
;
370 if (!!obj
.move || (!!obj
.score
&& obj
.scoreMsg
== "Time")) {
371 // Security: only update moves if index is right,
372 // and score with scoreMsg "Time" if really lost on time.
374 "SELECT MAX(idx) AS maxIdx, MAX(played) AS lastPlayed " +
377 db
.get(query
, (err
, ret
) => {
379 if (!ret
.maxIdx
|| ret
.maxIdx
+ 1 == obj
.move.idx
) {
381 "INSERT INTO Moves (gid, squares, played, idx) VALUES " +
382 "(" + id
+ ",?," + Date
.now() + "," + obj
.move.idx
+ ")";
383 db
.run(query
, JSON
.stringify(obj
.move.squares
));
384 finishAndSendQuery();
386 else cb({ errmsg: "Wrong move index" });
389 if (ret
.maxIdx
< 2) cb({ errmsg: "Time not over" });
391 // We also need the game cadence
396 db
.get(query
, (err2
, ret2
) => {
397 const daysTc
= parseInt(ret2
.cadence
.match(/^[0-9]+/)[0]);
398 if (Date
.now() - ret
.lastPlayed
> daysTc
* 24 * 3600 * 1000)
399 finishAndSendQuery();
400 else cb({ errmsg: "Time not over" });
406 else finishAndSendQuery();
407 // NOTE: chat and delchat are mutually exclusive
410 "INSERT INTO Chats (gid, msg, name, added) VALUES ("
411 + id
+ ",?,'" + obj
.chat
.name
+ "'," + Date
.now() + ")";
412 db
.run(query
, obj
.chat
.msg
);
414 else if (obj
.delchat
) {
421 if (!!obj
.deletedBy
) {
422 // Did my opponent delete it too?
425 (obj
.deletedBy
== 'w' ? "Black" : "White") +
428 "SELECT " + selection
+ " " +
431 db
.get(query
, (err
,ret
) => {
432 // If yes: just remove game
433 if (!!ret
.deletedByOpp
) GameModel
.remove(id
);
439 remove: function(id_s
) {
442 ? " IN (" + id_s
.join(",") + ")"
444 db
.parallelize(function() {
446 "DELETE FROM Games " +
447 "WHERE id " + suffix
;
450 "DELETE FROM Moves " +
451 "WHERE gid " + suffix
;
454 "DELETE FROM Chats " +
455 "WHERE gid " + suffix
;
460 cleanGamesDb: function() {
461 const tsNow
= Date
.now();
462 // 86400000 = 24 hours in milliseconds
463 const day
= 86400000;
464 db
.serialize(function() {
466 "SELECT id, created, cadence, score " +
468 db
.all(query
, (err
, games
) => {
470 "SELECT gid, count(*) AS nbMoves, MAX(played) AS lastMaj " +
473 db
.all(query
, (err2
, mstats
) => {
474 // Reorganize moves data to avoid too many array lookups:
475 let movesGroups
= {};
476 mstats
.forEach(ms
=> {
477 movesGroups
[ms
.gid
] = {
482 // Remove games still not really started,
483 // with no action in the last 2 weeks, or result != '*':
485 let lostOnTime
= [ [], [] ];
489 !movesGroups
[g
.id
] &&
490 (g
.score
!= '*' || tsNow
- g
.created
> 14*day
)
494 !!movesGroups
[g
.id
] &&
495 movesGroups
[g
.id
].nbMoves
== 1 &&
496 (g
.score
!= '*' || tsNow
- movesGroups
[g
.id
].lastMaj
> 14*day
)
501 // Set score if lost on time and >= 2 moves:
504 !!movesGroups
[g
.id
] &&
505 movesGroups
[g
.id
].nbMoves
>= 2 &&
506 tsNow
- movesGroups
[g
.id
].lastMaj
>
507 // cadence in days * nb seconds per day:
508 parseInt(g
.cadence
.slice(0, -1), 10) * day
510 lostOnTime
[movesGroups
[g
.id
].nbMoves
% 2].push(g
.id
);
513 if (toRemove
.length
> 0) GameModel
.remove(toRemove
);
514 if (lostOnTime
.some(l
=> l
.length
> 0)) {
515 db
.parallelize(function() {
516 for (let i
of [0, 1]) {
517 if (lostOnTime
[i
].length
> 0) {
518 const score
= (i
== 0 ? "0-1" : "1-0");
521 "SET score = '" + score
+ "', scoreMsg = 'Time' " +
522 "WHERE id IN (" + lostOnTime
[i
].join(',') + ")";
535 module
.exports
= GameModel
;