@mideind/netskrafl-react 1.0.0-beta.1

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.
Files changed (68) hide show
  1. package/.eslintignore +8 -0
  2. package/.eslintrc.json +13 -0
  3. package/README.md +63 -0
  4. package/dist/cjs/index.css +6837 -0
  5. package/dist/cjs/index.js +3046 -0
  6. package/dist/cjs/index.js.map +1 -0
  7. package/dist/esm/index.css +6837 -0
  8. package/dist/esm/index.js +3046 -0
  9. package/dist/esm/index.js.map +1 -0
  10. package/dist/types.d.ts +41 -0
  11. package/package.json +63 -0
  12. package/rollup.config.js +60 -0
  13. package/src/components/index.ts +2 -0
  14. package/src/components/netskrafl/Netskrafl.stories.tsx +66 -0
  15. package/src/components/netskrafl/Netskrafl.tsx +135 -0
  16. package/src/components/netskrafl/Netskrafl.types.ts +7 -0
  17. package/src/components/netskrafl/index.ts +2 -0
  18. package/src/css/fonts.css +4 -0
  19. package/src/css/glyphs.css +224 -0
  20. package/src/css/skrafl-explo.css +6616 -0
  21. package/src/fonts/glyphicons-regular.eot +0 -0
  22. package/src/fonts/glyphicons-regular.ttf +0 -0
  23. package/src/fonts/glyphicons-regular.woff +0 -0
  24. package/src/index.ts +2 -0
  25. package/src/messages/messages.json +1576 -0
  26. package/src/mithril/actions.ts +319 -0
  27. package/src/mithril/bag.ts +65 -0
  28. package/src/mithril/bestdisplay.ts +74 -0
  29. package/src/mithril/blankdialog.ts +94 -0
  30. package/src/mithril/board.ts +336 -0
  31. package/src/mithril/buttons.ts +303 -0
  32. package/src/mithril/challengedialog.ts +186 -0
  33. package/src/mithril/channel.ts +162 -0
  34. package/src/mithril/chat.ts +228 -0
  35. package/src/mithril/components.ts +496 -0
  36. package/src/mithril/dragdrop.ts +219 -0
  37. package/src/mithril/elopage.ts +180 -0
  38. package/src/mithril/friend.ts +227 -0
  39. package/src/mithril/game.ts +1378 -0
  40. package/src/mithril/gameview.ts +111 -0
  41. package/src/mithril/globalstate.ts +33 -0
  42. package/src/mithril/i18n.ts +186 -0
  43. package/src/mithril/localstorage.ts +133 -0
  44. package/src/mithril/login.ts +122 -0
  45. package/src/mithril/logo.ts +270 -0
  46. package/src/mithril/main.ts +737 -0
  47. package/src/mithril/mithril.ts +29 -0
  48. package/src/mithril/model.ts +817 -0
  49. package/src/mithril/movelistitem.ts +226 -0
  50. package/src/mithril/page.ts +852 -0
  51. package/src/mithril/playername.ts +91 -0
  52. package/src/mithril/promodialog.ts +82 -0
  53. package/src/mithril/recentlist.ts +148 -0
  54. package/src/mithril/request.ts +52 -0
  55. package/src/mithril/review.ts +634 -0
  56. package/src/mithril/rightcolumn.ts +398 -0
  57. package/src/mithril/searchbutton.ts +118 -0
  58. package/src/mithril/statsdisplay.ts +109 -0
  59. package/src/mithril/tabs.ts +169 -0
  60. package/src/mithril/tile.ts +145 -0
  61. package/src/mithril/twoletter.ts +76 -0
  62. package/src/mithril/types.ts +379 -0
  63. package/src/mithril/userinfodialog.ts +171 -0
  64. package/src/mithril/util.ts +304 -0
  65. package/src/mithril/wait.ts +246 -0
  66. package/src/mithril/wordcheck.ts +102 -0
  67. package/tsconfig.json +28 -0
  68. package/vite.config.ts +12 -0
@@ -0,0 +1,634 @@
1
+ /*
2
+
3
+ Review.ts
4
+
5
+ Single page UI for Explo using the Mithril library
6
+
7
+ Copyright (C) 2025 Miðeind ehf.
8
+ Author: Vilhjálmur Þorsteinsson
9
+
10
+ The Creative Commons Attribution-NonCommercial 4.0
11
+ International Public License (CC-BY-NC 4.0) applies to this software.
12
+ For further information, see https://github.com/mideind/Netskrafl
13
+
14
+ */
15
+
16
+ import { ts } from "./i18n";
17
+ import { IView, Move, MoveInfo } from "./types";
18
+ import { glyph, coord, toVector, nbsp, buttonOver, buttonOut } from "./util";
19
+ import {
20
+ m, Vnode, VnodeChildren, VnodeAttrs,
21
+ ComponentFunc,
22
+ } from "./mithril";
23
+ import { ExploLogoOnly } from "./logo";
24
+ import { PlayerName } from "./playername";
25
+ import { makeButton } from "./buttons";
26
+ import { Info, LeftLogo } from "./components";
27
+ import { Board, Rack } from "./board";
28
+
29
+ export const vwReview = (view: IView): VnodeChildren => {
30
+ // A review of a finished game
31
+
32
+ const model = view.model;
33
+ if (!model.game) return undefined;
34
+ const game = model.game;
35
+ let moveIndex = model.reviewMove ?? 0;
36
+ let bestMoves = model.bestMoves || [];
37
+
38
+ function vwRightColumn(): Vnode {
39
+ // A container for the right-side header and area components
40
+
41
+ function vwRightHeading(): Vnode {
42
+ // The right-side heading on the game screen
43
+
44
+ const fairplay = game.fairplay;
45
+ const player = game.player;
46
+ let sc0 = "";
47
+ let sc1 = "";
48
+ if (moveIndex) {
49
+ let s0 = 0;
50
+ let s1 = 0;
51
+ for (let i = 0; i < moveIndex; i++) {
52
+ // Add up the scores until and including this move
53
+ let m = game.moves[i];
54
+ if (i % 2 === 0)
55
+ s0 += m[1][2];
56
+ else
57
+ s1 += m[1][2];
58
+ }
59
+ sc0 = s0.toString();
60
+ sc1 = s1.toString();
61
+ }
62
+ return m(".heading",
63
+ [
64
+ m(".logowrapper",
65
+ m(".header-logo",
66
+ m(m.route.Link,
67
+ {
68
+ href: "/page",
69
+ class: "backlink"
70
+ },
71
+ m(ExploLogoOnly)
72
+ )
73
+ )
74
+ ),
75
+ m(".playerwrapper", [
76
+ m(".leftplayer" + (player === 1 ? ".autoplayercolor" : ".humancolor"), [
77
+ m(".player", m(PlayerName, { view, side: "left" })),
78
+ m(".scorewrapper", m(".scoreleft", sc0)),
79
+ ]),
80
+ m(".rightplayer" + (player === 1 ? ".humancolor" : ".autoplayercolor"), [
81
+ m(".player", m(PlayerName, { view, side: "right" })),
82
+ m(".scorewrapper", m(".scoreright", sc1)),
83
+ ]),
84
+ m(".fairplay",
85
+ { style: { visibility: fairplay ? "visible" : "hidden" } },
86
+ m("span.fairplay-btn.large", { title: ts("Skraflað án hjálpartækja") }))
87
+ ])
88
+ ]
89
+ );
90
+ }
91
+
92
+ function vwRightArea(): Vnode {
93
+ // A container for the list of best possible moves
94
+ return m(".right-area", vwBestMoves(view, moveIndex, bestMoves));
95
+ }
96
+
97
+ return m(".rightcol", [vwRightHeading(), vwRightArea()]);
98
+ }
99
+
100
+ let r: VnodeChildren = [];
101
+ if (game) {
102
+ // Create a list of major elements that we're showing
103
+ r.push(vwRightColumn());
104
+ r.push(m(BoardReview, { view, moveIndex }));
105
+ if (model.reviewMove !== null && moveIndex === 0) {
106
+ // Only show the stats overlay if moveIndex is 0
107
+ const n = vwStatsReview(view);
108
+ n && r.push(n);
109
+ }
110
+ }
111
+ return m("div", // Removing this div messes up Mithril
112
+ [
113
+ m(".game-container", r),
114
+ m(LeftLogo), // Button to go back to main screen
115
+ m(Info) // Help button
116
+ ]
117
+ );
118
+ }
119
+
120
+ const vwBestMoves = (view: IView, moveIndex: number, bestMoves: Move[]): VnodeChildren => {
121
+ // List of best moves, in a game review
122
+
123
+ const model = view.model;
124
+ const game = model.game;
125
+
126
+ function bestHeader(co: string, tiles: string, score: number): Vnode {
127
+ // Generate the header of the best move list
128
+ let wrdclass = "wordmove";
129
+ let dispText: string | any[];
130
+ if (co.length > 0) {
131
+ // Regular move
132
+ dispText = [
133
+ m("i", tiles.split("?").join("")),
134
+ " (" + co + ")"
135
+ ];
136
+ }
137
+ else {
138
+ /* Not a regular tile move */
139
+ wrdclass = "othermove";
140
+ if (tiles == "PASS")
141
+ /* Pass move */
142
+ dispText = ts("Pass");
143
+ else
144
+ if (tiles.indexOf("EXCH") === 0) {
145
+ /* Exchange move - we don't show the actual tiles exchanged, only their count */
146
+ let numtiles = tiles.slice(5).length
147
+ const letters = ts(numtiles == 1 ? "letter" : "letters");
148
+ dispText = ts("exchanged", { numtiles: numtiles.toString(), letters: letters });
149
+ }
150
+ else
151
+ if (tiles == "RSGN")
152
+ /* Resigned from game */
153
+ dispText = ts("Gaf viðureign");
154
+ else
155
+ if (tiles == "CHALL")
156
+ /* Challenge issued */
157
+ dispText = ts("Véfengdi lögn");
158
+ else
159
+ if (tiles == "RESP") {
160
+ /* Challenge response */
161
+ if (score < 0)
162
+ dispText = ts("Óleyfileg lögn");
163
+ else
164
+ dispText = ts("Röng véfenging");
165
+ }
166
+ else
167
+ if (tiles == "TIME") {
168
+ /* Score adjustment for time */
169
+ dispText = ts("Umframtími");
170
+ }
171
+ else
172
+ if (tiles == "OVER") {
173
+ /* Game over */
174
+ dispText = ts("Viðureign lokið");
175
+ wrdclass = "gameover";
176
+ }
177
+ else {
178
+ // The rack leave at the end of the game (which is always in lowercase
179
+ // and thus cannot be confused with the above abbreviations)
180
+ wrdclass = "othermove";
181
+ if (tiles == "--")
182
+ dispText = ts("Stafaleif: (engin)");
183
+ else
184
+ dispText = [ts("Stafaleif: "), m("i.upper", tiles)];
185
+ }
186
+ }
187
+ return m(".reviewhdr",
188
+ [
189
+ m("span.movenumber", "#" + moveIndex),
190
+ m("span", { class: wrdclass }, dispText)
191
+ ]
192
+ );
193
+ }
194
+
195
+ function bestMoveList(): VnodeChildren {
196
+ let r: VnodeChildren = [];
197
+ // Use a 1-based index into the move list
198
+ // (We show the review summary if move==0)
199
+ if (!game || !moveIndex || moveIndex > game.moves.length)
200
+ return r;
201
+ // Prepend a header that describes the move being reviewed
202
+ const m = game.moves[moveIndex - 1];
203
+ const [co, tiles, score] = m[1];
204
+ r.push(bestHeader(co, tiles, score));
205
+ const mlist = bestMoves;
206
+ for (let i = 0; i < mlist.length; i++) {
207
+ const [player, [co, tiles, score]] = mlist[i];
208
+ const n = vwBestMove(view, moveIndex, i, mlist[i],
209
+ {
210
+ key: i.toString(),
211
+ player: player, co: co, tiles: tiles,
212
+ score: score, leftTotal: 0, rightTotal: 0
213
+ }
214
+ );
215
+ n && r.push(n);
216
+ }
217
+ return r;
218
+ }
219
+
220
+ return m(".movelist-container", [m(".movelist.bestmoves", bestMoveList())]);
221
+ }
222
+
223
+ const vwBestMove = (
224
+ view: IView,
225
+ moveIndex: number,
226
+ bestMoveIndex: number,
227
+ move: Move,
228
+ info: MoveInfo,
229
+ ): VnodeChildren => {
230
+ // Displays a move in a list of best available moves
231
+
232
+ const model = view.model;
233
+ if (!model.game) return undefined;
234
+ const game = model.game;
235
+ const { player, co, tiles, score } = info;
236
+
237
+ function highlightMove(co: string, tiles: string, playerColor: 0 | 1, show: boolean) {
238
+ /* Highlight a move's tiles when hovering over it in the best move list */
239
+ const vec = toVector(co);
240
+ let col = vec.col;
241
+ let row = vec.row;
242
+ let nextBlank = false;
243
+ // If we're highlighting a move, show all moves leading up to it on the board
244
+ if (show) {
245
+ model.highlightedMove = bestMoveIndex;
246
+ game.placeTiles(moveIndex - 1, true); // No highlight
247
+ }
248
+ for (let tile of tiles) {
249
+ if (tile === "?") {
250
+ nextBlank = true;
251
+ continue;
252
+ }
253
+ const sq = coord(row, col);
254
+ if (sq === null) continue; // Should not happen
255
+ const letter = tile;
256
+ if (nextBlank)
257
+ tile = '?';
258
+ const tscore = game.tilescore(tile);
259
+ if (show) {
260
+ if (!(sq in game.tiles)) {
261
+ // Showing a tile that was not already on the board
262
+ game.tiles[sq] = {
263
+ player,
264
+ tile,
265
+ letter,
266
+ score: tscore,
267
+ draggable: false,
268
+ freshtile: false,
269
+ index: 0,
270
+ xchg: false,
271
+ review: true, // Mark as a 'review tile'
272
+ highlight: playerColor
273
+ };
274
+ }
275
+ else {
276
+ // Highlighting a tile that was already on the board
277
+ game.tiles[sq].highlight = playerColor;
278
+ }
279
+ }
280
+ col += vec.dx;
281
+ row += vec.dy;
282
+ nextBlank = false;
283
+ }
284
+ if (!show) {
285
+ model.highlightedMove = null;
286
+ model.reviewMove !== null && game.placeTiles(model.reviewMove);
287
+ }
288
+ }
289
+
290
+ // Add a single move to the move list
291
+ // Normal tile move
292
+ const coParens = "(" + co + ")";
293
+ // Note: String.replace() will not work here since there may be two question marks in the string
294
+ const word = tiles.split("?").join(""); /* TBD: Display wildcard characters differently? */
295
+ // Normal game move
296
+ let title = "Smelltu til að fletta upp";
297
+ let playerColor: 0 | 1 = 0;
298
+ const lcp = game.player;
299
+ let cls: string;
300
+ if (player === lcp || (lcp === null && player === 0))
301
+ cls = "humangrad" + (player === 0 ? "_left" : "_right"); /* Local player */
302
+ else {
303
+ cls = "autoplayergrad" + (player === 0 ? "_left" : "_right"); /* Remote player */
304
+ playerColor = 1;
305
+ }
306
+ const attribs: VnodeAttrs = { title };
307
+ // Word lookup, if Icelandic game
308
+ if (game.locale === "is_IS")
309
+ attribs.onclick = () => { window.open('https://malid.is/leit/' + word, 'malid'); };
310
+ // Highlight the move on the board while hovering over it
311
+ attribs.onmouseover = () => {
312
+ move[2] = true; // highlighted
313
+ highlightMove(co, tiles, playerColor, true);
314
+ };
315
+ attribs.onmouseout = () => {
316
+ move[2] = false; // highlighted
317
+ highlightMove(co, tiles, playerColor, false);
318
+ };
319
+ if (player === 0) {
320
+ // Move by left side player
321
+ return m(".move.leftmove." + cls, attribs,
322
+ [
323
+ m("span.score" + (move[2] ? ".highlight" : ""), score),
324
+ m("span.wordmove", [m("i", word), nbsp(), coParens])
325
+ ]
326
+ );
327
+ }
328
+ else {
329
+ // Move by right side player
330
+ return m(".move.rightmove." + cls, attribs,
331
+ [
332
+ m("span.wordmove", [coParens, nbsp(), m("i", word)]),
333
+ m("span.score" + (move[2] ? ".highlight" : ""), score)
334
+ ]
335
+ );
336
+ }
337
+ }
338
+
339
+ const vwScoreReview = (view: IView, moveIndex: number): VnodeChildren => {
340
+ // Shows the score of the current move within a game review screen
341
+ const game = view.model.game;
342
+ if (!game) return undefined;
343
+ const mv = moveIndex ? game.moves[moveIndex - 1] : undefined;
344
+ if (mv === undefined)
345
+ return undefined;
346
+ const [_, [coord, tiles, score]] = mv;
347
+ if (score === undefined || (coord === "" && tiles === "OVER"))
348
+ // No score available, or this is a "game over" sentinel move: don't display
349
+ return undefined;
350
+ let sc = [".score"];
351
+ if (moveIndex > 0) {
352
+ if (moveIndex % 2 === (game.player ?? 0))
353
+ // Opponent's move
354
+ sc.push("opponent");
355
+ else
356
+ // Local player's move
357
+ sc.push("localplayer");
358
+ }
359
+ return m(sc.join("."), score.toString());
360
+ }
361
+
362
+ const vwScoreDiff = (view: IView, moveIndex: number): VnodeChildren => {
363
+ // Shows the score of the current move within a game review screen
364
+ const model = view.model;
365
+ const game = model.game;
366
+ if (!game) return undefined;
367
+ let sc = [".scorediff"];
368
+ const mv = moveIndex ? game.moves[moveIndex - 1] : undefined;
369
+ let score = mv ? mv[1][2] : undefined;
370
+ let diff = "";
371
+ if (score === undefined || model.bestMoves === null || model.highlightedMove === null) {
372
+ // Unable to display score difference
373
+ } else {
374
+ const bestScore = model.bestMoves[model.highlightedMove][1][2];
375
+ diff = (score - bestScore).toString();
376
+ if (diff[0] != "-" && diff[0] != "0")
377
+ diff = "+" + diff;
378
+ if (score >= bestScore)
379
+ sc.push("posdiff");
380
+ }
381
+ return m(sc.join("."), { style: { visibility: "visible" } }, diff);
382
+ }
383
+
384
+ const vwStatsReview = (view: IView): VnodeChildren => {
385
+ // Shows the game statistics overlay
386
+ if (view.model.game === null) return undefined;
387
+ const game = view.model.game;
388
+ if (game.stats === null)
389
+ // No stats yet loaded: do it now
390
+ game.loadStats();
391
+
392
+ function fmt(p: string, digits?: number, value?: string | number): string {
393
+ let txt = value;
394
+ if (txt === undefined && game.stats)
395
+ txt = game.stats[p];
396
+ if (txt === undefined)
397
+ return "";
398
+ if (typeof txt == "number") {
399
+ if (digits !== undefined && digits > 0)
400
+ txt = txt.toFixed(digits).replace(".", ","); // Convert decimal point to comma
401
+ else
402
+ txt = txt.toString();
403
+ }
404
+ return txt;
405
+ }
406
+
407
+ let leftPlayerColor: string, rightPlayerColor: string;
408
+
409
+ if (game.player === 1) {
410
+ rightPlayerColor = "humancolor";
411
+ leftPlayerColor = "autoplayercolor";
412
+ }
413
+ else {
414
+ leftPlayerColor = "humancolor";
415
+ rightPlayerColor = "autoplayercolor";
416
+ }
417
+
418
+ return m(
419
+ ".gamestats", { style: { visibility: "visible" } },
420
+ [
421
+ m("div", { style: { position: "relative", width: "100%" } },
422
+ [
423
+ m(".player", { class: leftPlayerColor, style: { width: "50%" } },
424
+ m(".robot-btn.left",
425
+ game.autoplayer[0] ?
426
+ [glyph("cog"), nbsp(), game.nickname[0]]
427
+ :
428
+ game.nickname[0]
429
+ )
430
+ ),
431
+ m(".player", { class: rightPlayerColor, style: { width: "50%", "text-align": "right" } },
432
+ m(".robot-btn.right",
433
+ game.autoplayer[1] ?
434
+ [glyph("cog"), nbsp(), game.nickname[1]]
435
+ :
436
+ game.nickname[1]
437
+ )
438
+ )
439
+ ]
440
+ ),
441
+ m("div", { id: "gamestarted" },
442
+ [
443
+ m("p",
444
+ [
445
+ "Viðureignin hófst ",
446
+ m("span", fmt("gamestart")), m("br"),
447
+ "og henni lauk ",
448
+ m("span", fmt("gameend"))
449
+ ]
450
+ ),
451
+ game.manual ? m("p", "Leikið var í keppnisham") : ""
452
+ ]
453
+ ),
454
+ m(".statscol", { style: { clear: "left" } },
455
+ [
456
+ m("p",
457
+ ["Fjöldi leikja: ", m("span", fmt("moves0"))]
458
+ ),
459
+ m("p",
460
+ [
461
+ "Fjöldi bingóa: ", m("span", fmt("bingoes0")),
462
+ " (bónus ",
463
+ m(
464
+ "span",
465
+ fmt("bingopoints0", 0, !game.stats ? 0 : game.stats.bingoes0 * 50)
466
+ ),
467
+ " stig)"
468
+ ]
469
+ ),
470
+ m("p",
471
+ [
472
+ "Stafir lagðir niður: ", m("span", fmt("tiles0")),
473
+ " (þar af ", m("span", fmt("blanks0")), " auðir)"
474
+ ]
475
+ ),
476
+ m("p", ["Meðalstig stafa (án auðra): ", m("span", fmt("average0", 2))]),
477
+ m("p", ["Samanlögð stafastig: ", m("span", fmt("letterscore0"))]),
478
+ m("p", ["Margföldun stafastiga: ", m("span", fmt("multiple0", 2))]),
479
+ m("p", ["Stig án stafaleifar í lok: ", m("span", fmt("cleantotal0"))]),
480
+ m("p", ["Meðalstig hvers leiks: ", m("span", fmt("avgmove0", 2))]),
481
+ game.manual ? m("p", ["Rangar véfengingar andstæðings x 10: ", m("span", fmt("wrongchall0"))]) : "",
482
+ m("p", ["Stafaleif og frádráttur í lok: ", m("span", fmt("remaining0"))]),
483
+ m("p", ["Umframtími: ", m("span", fmt("overtime0"))]),
484
+ m("p",
485
+ [
486
+ "Stig: ",
487
+ m(
488
+ "span",
489
+ fmt("total0", 0, !game.stats ? 0 : game.stats.scores[0])
490
+ ),
491
+ " (", m("span", fmt("ratio0", 1)), "%)"
492
+ ]
493
+ )
494
+ ]
495
+ ),
496
+ m(".statscol",
497
+ [
498
+ m("p",
499
+ ["Fjöldi leikja: ", m("span", fmt("moves1"))]
500
+ ),
501
+ m("p",
502
+ [
503
+ "Fjöldi bingóa: ", m("span", fmt("bingoes1")),
504
+ " (bónus ",
505
+ m(
506
+ "span",
507
+ fmt("bingopoints0", 0, !game.stats ? 0 : game.stats.bingoes1 * 50)
508
+ ),
509
+ " stig)"
510
+ ]
511
+ ),
512
+ m("p",
513
+ [
514
+ "Stafir lagðir niður: ", m("span", fmt("tiles1")),
515
+ " (þar af ", m("span", fmt("blanks1")), " auðir)"
516
+ ]
517
+ ),
518
+ m("p", ["Meðalstig stafa (án auðra): ", m("span", fmt("average1", 2))]),
519
+ m("p", ["Samanlögð stafastig: ", m("span", fmt("letterscore1"))]),
520
+ m("p", ["Margföldun stafastiga: ", m("span", fmt("multiple1", 2))]),
521
+ m("p", ["Stig án stafaleifar í lok: ", m("span", fmt("cleantotal1"))]),
522
+ m("p", ["Meðalstig hvers leiks: ", m("span", fmt("avgmove1", 2))]),
523
+ game.manual ? m("p", ["Rangar véfengingar andstæðings x 10: ", m("span", fmt("wrongchall1"))]) : "",
524
+ m("p", ["Stafaleif og frádráttur í lok: ", m("span", fmt("remaining1"))]),
525
+ m("p", ["Umframtími: ", m("span", fmt("overtime1"))]),
526
+ m("p",
527
+ [
528
+ "Stig: ",
529
+ m(
530
+ "span",
531
+ fmt("total1", 0, !game.stats ? 0 : game.stats.scores[1])
532
+ ),
533
+ " (", m("span", fmt("ratio1", 1)), "%)"
534
+ ]
535
+ )
536
+ ]
537
+ ),
538
+ m(".closebtn",
539
+ {
540
+ id: "review-close",
541
+ onclick: (ev: Event) => {
542
+ // Navigate to move #1
543
+ setTimeout(() => {
544
+ m.route.set("/review/" + game.uuid, { move: 1 });
545
+ });
546
+ ev.preventDefault();
547
+ },
548
+ onmouseover: buttonOver,
549
+ onmouseout: buttonOut
550
+ },
551
+ [glyph("play"), " Rekja"]
552
+ )
553
+ ]
554
+ );
555
+ }
556
+
557
+ const vwButtonsReview = (view: IView, moveIndex: number): VnodeChildren => {
558
+ // The navigation buttons below the board on the review screen
559
+ const model = view.model;
560
+ const game = model.game;
561
+ const numMoves = game?.moves.length ?? 0;
562
+ const gameUuid = game?.uuid ?? "";
563
+ let r: VnodeChildren = [];
564
+ if (!gameUuid) return r;
565
+ r.push(
566
+ makeButton(
567
+ "navbtn", !moveIndex, // Disabled if at moveIndex 0 (initial review dialog)
568
+ () => {
569
+ // Navigate to previous moveIndex
570
+ model.loadBestMoves(moveIndex ? moveIndex - 1 : 0);
571
+ },
572
+ "Sjá fyrri leik",
573
+ m("span",
574
+ { id: "nav-prev-visible" },
575
+ [glyph("chevron-left"), " Fyrri"]
576
+ ),
577
+ "navprev"
578
+ )
579
+ );
580
+ r.push(
581
+ makeButton(
582
+ "navbtn", (!moveIndex) || (moveIndex >= numMoves),
583
+ () => {
584
+ // Navigate to next moveIndex
585
+ model.loadBestMoves(moveIndex + 1);
586
+ },
587
+ "Sjá næsta leik",
588
+ m("span",
589
+ { id: "nav-next-visible" },
590
+ ["Næsti ", glyph("chevron-right")]
591
+ ),
592
+ "navnext"
593
+ )
594
+ );
595
+ // Show the score difference between an actual moveIndex and
596
+ // a particular moveIndex on the best moveIndex list
597
+ if (model.highlightedMove !== null) {
598
+ const n = vwScoreDiff(view, moveIndex);
599
+ if (n !== undefined)
600
+ r.push(n);
601
+ }
602
+ const n = vwScoreReview(view, moveIndex);
603
+ if (n !== undefined)
604
+ r.push(n);
605
+ return r;
606
+ }
607
+
608
+ interface IBoardReviewAttributes {
609
+ view: IView;
610
+ moveIndex: number;
611
+ }
612
+
613
+ export const BoardReview: ComponentFunc<IBoardReviewAttributes> = (initialVnode) => {
614
+ // The board area within a game review screen
615
+ const view = initialVnode.attrs.view;
616
+ const model = view.model;
617
+ return {
618
+ view: (vnode) => {
619
+ const game = model.game;
620
+ let r: VnodeChildren = [];
621
+ if (game) {
622
+ r = [
623
+ m(Board, { view, review: true }),
624
+ m(Rack, { view, review: true }),
625
+ ];
626
+ const moveIndex = vnode.attrs.moveIndex;
627
+ if (moveIndex !== null)
628
+ // Don't show navigation buttons if currently at overview (move==null)
629
+ r = r.concat(vwButtonsReview(view, moveIndex));
630
+ }
631
+ return m(".board-area", r);
632
+ }
633
+ };
634
+ }