@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,398 @@
1
+ /*
2
+
3
+ Rightcolumn.ts
4
+
5
+ Right column of board view component
6
+
7
+ Copyright (C) 2024 Miðeind ehf.
8
+ Author: Vilhjalmur Thorsteinsson
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 { IGame, IView } from "./types";
17
+ import { ts } from "./i18n";
18
+ import { gameUrl, glyph, nbsp, scrollMovelistToBottom } from "./util";
19
+ import { VnodeChildren, ComponentFunc, m } from "./mithril";
20
+ import { ExploLogoOnly } from "./logo";
21
+ import { PlayerName } from "./playername";
22
+ import { TwoLetter } from "./twoletter";
23
+ import { Chat } from "./chat";
24
+ import { Bag } from "./bag";
25
+ import { MoveListItem } from "./movelistitem";
26
+
27
+ interface IAttributes {
28
+ view: IView;
29
+ }
30
+
31
+ const Movelist: ComponentFunc<IAttributes> = (initialVnode) => {
32
+ // The move list tab
33
+
34
+ const view = initialVnode.attrs.view;
35
+ const model = view.model;
36
+ const game = model.game;
37
+ const state = model.state;
38
+
39
+ function movelist(): VnodeChildren {
40
+ let mlist = game ? game.moves : []; // All moves made so far in the game
41
+ let r: VnodeChildren = [];
42
+ let leftTotal = 0;
43
+ let rightTotal = 0;
44
+ for (let i = 0; i < mlist.length; i++) {
45
+ let move = mlist[i];
46
+ let [player, [co, tiles, score]] = move;
47
+ if (player === 0)
48
+ leftTotal = Math.max(leftTotal + score, 0);
49
+ else
50
+ rightTotal = Math.max(rightTotal + score, 0);
51
+ const n = m(MoveListItem,
52
+ {
53
+ view,
54
+ move,
55
+ info: {
56
+ key: i.toString(),
57
+ leftTotal: leftTotal, rightTotal: rightTotal,
58
+ player: player, co: co, tiles: tiles, score: score
59
+ }
60
+ }
61
+ );
62
+ r.push(n);
63
+ }
64
+ return r;
65
+ }
66
+
67
+ const bag = game ? game.bag : "";
68
+ const newbag = game ? game.newbag : true;
69
+ return {
70
+ view: () => m(".movelist-container",
71
+ [
72
+ m(".movelist",
73
+ {
74
+ onupdate: () => { setTimeout(scrollMovelistToBottom); }
75
+ },
76
+ movelist()
77
+ ),
78
+ // Show the bag here on mobile
79
+ state?.uiFullscreen ? "" : m(Bag, { bag, newbag })
80
+ ]
81
+ )
82
+ };
83
+ };
84
+
85
+ const Games: ComponentFunc<IAttributes> = (initialVnode) => {
86
+ // The game list tab
87
+
88
+ const view = initialVnode.attrs.view;
89
+ const model = view.model;
90
+
91
+ function games(): VnodeChildren {
92
+ let r: VnodeChildren = [];
93
+ if (model.loadingGameList)
94
+ return r;
95
+ let gameList = model.gameList;
96
+ if (gameList === undefined)
97
+ // Game list is being loaded
98
+ return r;
99
+ if (gameList === null) {
100
+ // No games to show now, but we'll load them
101
+ // and they will be automatically refreshed when ready
102
+ model.loadGameList();
103
+ return r;
104
+ }
105
+ const game = model.game;
106
+ const gameId = game ? game.uuid : "";
107
+ for (let item of gameList) {
108
+ if (item.uuid == gameId)
109
+ continue; // Don't show this game
110
+ if (!item.my_turn && !item.zombie)
111
+ continue; // Only show pending games
112
+ let opp: VnodeChildren;
113
+ if (item.oppid === null)
114
+ // Mark robots with a cog icon
115
+ opp = [glyph("cog"), nbsp(), item.opp];
116
+ else
117
+ opp = [item.opp];
118
+ let winLose = item.sc0 < item.sc1 ? ".losing" : "";
119
+ let title = "Staðan er " + item.sc0 + ":" + item.sc1;
120
+ // Add the game-timed class if the game is a timed game.
121
+ // These will not be displayed in the mobile UI.
122
+ r.push(
123
+ m(".games-item" + (item.timed ? ".game-timed" : ""),
124
+ { key: item.uuid, title: title },
125
+ m(m.route.Link,
126
+ { href: gameUrl(item.url) },
127
+ [
128
+ m(".at-top-left", m(".tilecount", m(".oc", opp))),
129
+ m(".at-top-left",
130
+ m(".tilecount.trans",
131
+ m(".tc" + winLose, { style: { width: item.tile_count.toString() + "%" } }, opp)
132
+ )
133
+ )
134
+ ]
135
+ )
136
+ )
137
+ );
138
+ }
139
+ return r;
140
+ }
141
+
142
+ return {
143
+ view: () => m(".games", { style: "z-index: 6" }, games())
144
+ };
145
+ };
146
+
147
+ interface ITabAttributes {
148
+ game: IGame;
149
+ tabid: string;
150
+ title: string;
151
+ icon: string;
152
+ alert?: boolean;
153
+ funcSel?: () => void;
154
+ }
155
+
156
+ const Tab: ComponentFunc<ITabAttributes> = (initialVnode) => {
157
+ // A clickable tab for the right-side area content
158
+ const game = initialVnode.attrs.game;
159
+ return {
160
+ view: (vnode) => {
161
+ const { tabid, title, icon, alert, funcSel } = vnode.attrs;
162
+ const sel = game?.sel || "movelist";
163
+ return m(".right-tab" + (sel === tabid ? ".selected" : ""),
164
+ {
165
+ id: "tab-" + tabid,
166
+ className: alert ? "alert" : "",
167
+ title: title,
168
+ onclick: (ev: Event) => {
169
+ // Select this tab
170
+ if (game && game.showingDialog === null) {
171
+ if (game.setSelectedTab(tabid)) {
172
+ // A new tab was actually selected
173
+ funcSel && funcSel();
174
+ if (tabid === "movelist")
175
+ setTimeout(scrollMovelistToBottom);
176
+ }
177
+ }
178
+ ev.preventDefault();
179
+ }
180
+ },
181
+ glyph(icon)
182
+ );
183
+ }
184
+ };
185
+ };
186
+
187
+ const TabGroup: ComponentFunc<{ game: IGame }> = () => {
188
+ return {
189
+ view: (vnode) => {
190
+ // A group of clickable tabs for the right-side area content
191
+ const game = vnode.attrs.game;
192
+ let showchat = !(game.autoplayer[0] || game.autoplayer[1]);
193
+ const r: VnodeChildren = [
194
+ m(Tab, { game, tabid: "board", title: ts("Borðið"), icon: "grid" }),
195
+ m(Tab, { game, tabid: "movelist", title: ts("Leikir"), icon: "show-lines" }),
196
+ m(Tab, { game, tabid: "twoletter", title: ts("Tveggja stafa orð"), icon: "life-preserver" }),
197
+ m(Tab, { game, tabid: "games", title: ts("Viðureignir"), icon: "flag" }),
198
+ ];
199
+ if (showchat) {
200
+ // Add chat tab
201
+ r.push(
202
+ m(Tab, {
203
+ game,
204
+ tabid: "chat",
205
+ title: ts("Spjall"),
206
+ icon: "conversation",
207
+ funcSel: () => {
208
+ // The tab has been clicked
209
+ if (game.markChatShown())
210
+ // ...and now the user has seen all chat messages up until now
211
+ m.redraw();
212
+ },
213
+ // Show chat icon in red if any chat messages have not been seen
214
+ // and the chat tab is not already selected
215
+ alert: !game.chatSeen && game.sel != "chat"
216
+ })
217
+ );
218
+ }
219
+ return m.fragment({}, r);
220
+ }
221
+ };
222
+ };
223
+
224
+ export const RightColumn: ComponentFunc<IAttributes> = (initialVnode) => {
225
+ // A container for the right-side header and area components
226
+
227
+ const view = initialVnode.attrs.view;
228
+ const model = view.model;
229
+ const game = model.game;
230
+
231
+ function vwClock(): VnodeChildren {
232
+ // Show clock data if this is a timed game
233
+ if (!game || !game.showClock())
234
+ // Not a timed game, or a game that is completed
235
+ return m.fragment({}, []);
236
+
237
+ function vwClockFace(cls: string, txt: string, runningOut: boolean, blinking: boolean): VnodeChildren {
238
+ return m("h3." + cls
239
+ + (runningOut ? ".running-out" : "")
240
+ + (blinking ? ".blink" : ""),
241
+ txt
242
+ );
243
+ }
244
+
245
+ return m.fragment({}, [
246
+ vwClockFace("clockleft", game.clockText0, game.runningOut0, game.blinking0),
247
+ vwClockFace("clockright", game.clockText1, game.runningOut1, game.blinking1),
248
+ m(".clockface", glyph("time"))
249
+ ]);
250
+ }
251
+
252
+ function vwRightHeading(): VnodeChildren {
253
+ // The right-side heading on the game screen
254
+
255
+ const fairplay = game ? game.fairplay : false;
256
+ const player = game ? game.player : null;
257
+ const sc0 = game ? game.displayScore(0).toString() : "";
258
+ const sc1 = game ? game.displayScore(1).toString() : "";
259
+ return m(".heading",
260
+ [
261
+ // The header-logo is not displayed in fullscreen
262
+ m(".logowrapper",
263
+ m(".header-logo",
264
+ m(m.route.Link,
265
+ {
266
+ href: "/page",
267
+ class: "backlink"
268
+ },
269
+ m(ExploLogoOnly)
270
+ )
271
+ )
272
+ ),
273
+ m(".playerwrapper", [
274
+ m(".leftplayer" + (player === 1 ? ".autoplayercolor" : ".humancolor"), [
275
+ m(".player", m(PlayerName, { view, side: "left" })),
276
+ m(".scorewrapper", m(".scoreleft", sc0)),
277
+ ]),
278
+ m(".rightplayer" + (player === 1 ? ".humancolor" : ".autoplayercolor"), [
279
+ m(".player", m(PlayerName, { view, side: "right" })),
280
+ m(".scorewrapper", m(".scoreright", sc1)),
281
+ ]),
282
+ m(".fairplay",
283
+ { style: { visibility: fairplay ? "visible" : "hidden" } },
284
+ m("span.fairplay-btn.large", { title: ts("Skraflað án hjálpartækja") })
285
+ )
286
+ ]),
287
+ vwClock(),
288
+ ]
289
+ );
290
+ }
291
+
292
+ function vwRightArea(): VnodeChildren {
293
+ // A container for the tabbed right-side area components
294
+ if (!game) return undefined;
295
+ const sel = game.sel || "movelist";
296
+ // Show the chat tab unless the opponent is an autoplayer
297
+ let component: VnodeChildren;
298
+ switch (sel) {
299
+ case "movelist":
300
+ component = m(Movelist, { view });
301
+ break;
302
+ case "twoletter":
303
+ component = m(TwoLetter, { view });
304
+ break;
305
+ case "chat":
306
+ component = m(Chat, { view });
307
+ break;
308
+ case "games":
309
+ component = m(Games, { view });
310
+ break;
311
+ default:
312
+ break;
313
+ }
314
+ const tabgrp: VnodeChildren = m(TabGroup, { game });
315
+ return m(".right-area" + (game?.showClock() ? ".with-clock" : ""),
316
+ component ? [tabgrp, component] : [tabgrp]
317
+ );
318
+ }
319
+
320
+ function vwRightMessage(): VnodeChildren {
321
+ // Display a status message in the mobile UI
322
+ if (!game) return undefined;
323
+ const s = game.buttonState();
324
+ let msg: string | any[] = "";
325
+ const player = game.player;
326
+ const opp = player === null ? "" : game.nickname[1 - player];
327
+ const move = game.moves.length ? game.moves[game.moves.length - 1] : undefined;
328
+ const mtype = move ? move[1][1] : undefined;
329
+ if (s.congratulate) {
330
+ // This player won
331
+ if (mtype == "RSGN")
332
+ msg = [m("strong", [opp, " resigned!"]), " Congratulations."];
333
+ else
334
+ msg = [m("strong", ["You beat ", opp, "!"]), " Congratulations."];
335
+ } else if (s.gameOver) {
336
+ // This player lost
337
+ msg = "Game over!";
338
+ } else if (!s.localTurn) {
339
+ // It's the opponent's turn
340
+ msg = ["It's ", opp, "'s turn. Plan your next move!"];
341
+ } else if (s.tilesPlaced) {
342
+ if (game.currentScore === undefined) {
343
+ if (move === undefined)
344
+ msg = ["Your first move must cover the ", glyph("target"), " start square."];
345
+ else
346
+ msg = "Tiles must be consecutive.";
347
+ } else if (game.wordGood === false) {
348
+ msg = ["Move is not valid, but would score ", m("strong", game.currentScore.toString()), " points."];
349
+ } else {
350
+ msg = ["Valid move, score ", m("strong", game.currentScore.toString()), " points."];
351
+ }
352
+ } else if (move === undefined) {
353
+ // Initial move
354
+ msg = [m("strong", "You start!"), " Cover the ", glyph("star"), " asterisk with your move."];
355
+ }
356
+ else {
357
+ const co = move[1][0];
358
+ let tiles = mtype || "";
359
+ const score = move[1][2];
360
+ if (co === "") {
361
+ // Not a regular tile move
362
+ if (tiles === "PASS")
363
+ msg = [opp, " passed."];
364
+ else if (tiles.indexOf("EXCH") === 0) {
365
+ const numtiles = tiles.slice(5).length;
366
+ msg = [
367
+ opp, " exchanged ",
368
+ numtiles.toString(),
369
+ (numtiles === 1 ? " tile" : " tiles"),
370
+ "."
371
+ ];
372
+ } else if (tiles === "CHALL") {
373
+ msg = [opp, " challenged your move."];
374
+ } else if (tiles === "RESP") {
375
+ if (score < 0)
376
+ msg = [opp, " successfully challenged your move."];
377
+ else
378
+ msg = [opp, " unsuccessfully challenged your move and lost 10 points."];
379
+ }
380
+ }
381
+ else {
382
+ // Regular tile move
383
+ tiles = tiles.split("?").join(""); /* TBD: Display wildcard characters differently? */
384
+ msg = [opp, " played ", m("strong", tiles),
385
+ " for ", m("strong", score.toString()), " points"];
386
+ }
387
+ }
388
+ return m(".message", msg);
389
+ }
390
+
391
+ return {
392
+ view: () => m(".rightcol", [
393
+ vwRightHeading(),
394
+ vwRightArea(),
395
+ /* vwRightMessage(), */
396
+ ])
397
+ };
398
+ };
@@ -0,0 +1,118 @@
1
+ /*
2
+
3
+ Searchbutton.ts
4
+
5
+ Search button component
6
+
7
+ Copyright (C) 2024 Miðeind ehf.
8
+ Author: Vilhjalmur Thorsteinsson
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 { IView } from "./types";
17
+ import { ts } from "./i18n";
18
+ import { ComponentFunc, m } from "./mithril";
19
+ import { glyph, nbsp } from "./util";
20
+
21
+ interface IAttributes {
22
+ view: IView;
23
+ }
24
+
25
+ export const SearchButton: ComponentFunc<IAttributes> = (initialVnode) => {
26
+
27
+ // A combination of a button and pattern entry field
28
+ // for user search
29
+
30
+ const view = initialVnode.attrs.view;
31
+ const model = view.model;
32
+ let spec = ""; // The current search pattern
33
+ let promise: { result: boolean; p: Promise<boolean>; } | undefined = undefined;
34
+
35
+ function newSearch() {
36
+ // There may have been a change of search parameters: react
37
+ if (promise !== undefined) {
38
+ // There was a previous promise, now obsolete: make it
39
+ // resolve without action
40
+ promise.result = false;
41
+ promise = undefined;
42
+ }
43
+ let sel = model.userListCriteria?.query || "robots";
44
+ if (sel !== "search") {
45
+ // Not already in a search: load the user list immediately
46
+ model.loadUserList({ query: "search", spec: spec }, true);
47
+ return;
48
+ }
49
+ if (spec === model.userListCriteria?.spec)
50
+ // We're already looking at the same search spec: done
51
+ return;
52
+ // We're changing the search spec.
53
+ // In order to limit the number of search queries sent to
54
+ // the server while typing a new criteria, we keep an
55
+ // outstanding promise that resolves in 0.8 seconds,
56
+ // unless cancelled by a new keystroke/promise.
57
+ // Note: since a promise can't be directly cancelled, we use a
58
+ // convoluted route to associate a boolean result with it.
59
+ let newP = {
60
+ result: true,
61
+ p: new Promise<boolean>((resolve) => {
62
+ // After 800 milliseconds, resolve to whatever value the
63
+ // result property has at that time. It will be true
64
+ // unless the promise has been "cancelled" by setting
65
+ // its result property to false.
66
+ setTimeout(() => { resolve(newP.result); }, 800);
67
+ })
68
+ };
69
+ promise = newP;
70
+ promise.p.then((value: boolean) => {
71
+ if (value) {
72
+ // Successfully resolved, without cancellation:
73
+ // issue the search query to the server as it now stands
74
+ model.loadUserList({ query: "search", spec: spec }, true);
75
+ promise = undefined;
76
+ }
77
+ });
78
+ }
79
+
80
+ return {
81
+
82
+ view: () => {
83
+ const sel = model.userListCriteria?.query || "robots";
84
+ return m(".user-cat[id='user-search']",
85
+ [
86
+ glyph("search",
87
+ {
88
+ id: 'search',
89
+ className: (sel == "search" ? "shown" : ""),
90
+ onclick: () => {
91
+ // Reset the search pattern when clicking the search icon
92
+ spec = "";
93
+ newSearch();
94
+ document.getElementById("search-id")?.focus();
95
+ }
96
+ }
97
+ ),
98
+ nbsp(),
99
+ m("input.text.userid",
100
+ {
101
+ type: 'text',
102
+ id: 'search-id',
103
+ name: 'search-id',
104
+ maxlength: 16,
105
+ placeholder: ts('Einkenni eða nafn'),
106
+ value: spec,
107
+ onfocus: () => newSearch(),
108
+ oninput: (ev: Event) => {
109
+ spec = (ev.target as HTMLInputElement).value + "";
110
+ newSearch();
111
+ }
112
+ }
113
+ )
114
+ ]
115
+ );
116
+ }
117
+ };
118
+ };
@@ -0,0 +1,109 @@
1
+ /*
2
+
3
+ Statsdisplay.ts
4
+
5
+ User status display component
6
+
7
+ Copyright (C) 2024 Miðeind ehf.
8
+ Author: Vilhjalmur Thorsteinsson
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 { IView } from "./types";
17
+ import { ComponentFunc, m, VnodeChildren } from "./mithril";
18
+ import { ts } from "./i18n";
19
+ import { glyph, nbsp, valueOrK } from "./util";
20
+
21
+ interface IAttributes {
22
+ view: IView;
23
+ id: string;
24
+ ownStats: Record<string, any>;
25
+ }
26
+
27
+ export const StatsDisplay: ComponentFunc<IAttributes> = () => {
28
+ // Display key statistics, provided via the ownStats attribute
29
+
30
+ let sel = 1;
31
+
32
+ function vwStat(val?: number, icon?: string, suffix?: string): VnodeChildren {
33
+ // Display a user statistics figure, eventually with an icon
34
+ var txt = (val === undefined) ? "" : valueOrK(val);
35
+ if (suffix !== undefined)
36
+ txt += suffix;
37
+ return icon ? [glyph(icon), nbsp(), txt] : txt;
38
+ }
39
+
40
+ return {
41
+
42
+ view: (vnode) => {
43
+
44
+ // Display statistics about this user
45
+ var s = vnode.attrs.ownStats;
46
+ var winRatio = 0, winRatioHuman = 0;
47
+ var avgScore = 0, avgScoreHuman = 0;
48
+ if (s !== undefined && s.games !== undefined && s.human_games !== undefined) {
49
+ if (s.games > 0) {
50
+ winRatio = Math.round(100.0 * s.wins / s.games);
51
+ avgScore = Math.round(s.score / s.games);
52
+ }
53
+ if (s.human_games > 0) {
54
+ winRatioHuman = Math.round(100.0 * s.human_wins / s.human_games);
55
+ avgScoreHuman = Math.round(s.human_score / s.human_games);
56
+ }
57
+ }
58
+
59
+ return m("div", { id: vnode.attrs.id },
60
+ [
61
+ m(".toggler",
62
+ {
63
+ id: 'own-toggler',
64
+ title: ts("stats_choice"), // "With or without robot games"
65
+ onclick: (ev: Event) => { sel = 3 - sel; ev.preventDefault(); },
66
+ },
67
+ [
68
+ m(".option.small" + (sel === 1 ? ".selected" : ""),
69
+ { id: 'opt1' },
70
+ glyph("user")
71
+ ),
72
+ m(".option.small" + (sel === 2 ? ".selected" : ""),
73
+ { id: 'opt2' },
74
+ glyph("cog")
75
+ )
76
+ ]
77
+ ),
78
+ sel === 1 ? m("div",
79
+ { id: 'own-stats-human', className: 'stats-box', style: { display: "inline-block" } },
80
+ [
81
+ m(".stats-fig", { title: ts('Elo-stig') },
82
+ s ? vwStat(s.locale_elo?.human_elo, "crown") : ""),
83
+ m(".stats-fig.stats-games", { title: ts('Fjöldi viðureigna') },
84
+ s ? vwStat(s.human_games, "th") : ""),
85
+ m(".stats-fig.stats-win-ratio", { title: ts('Vinningshlutfall') },
86
+ vwStat(winRatioHuman, "bookmark", "%")),
87
+ m(".stats-fig.stats-avg-score", { title: ts('Meðalstigafjöldi') },
88
+ vwStat(avgScoreHuman, "dashboard"))
89
+ ]
90
+ ) : "",
91
+ sel === 2 ? m("div",
92
+ { id: 'own-stats-all', className: 'stats-box', style: { display: "inline-block" } },
93
+ [
94
+ m(".stats-fig", { title: ts("Elo-stig") },
95
+ s ? vwStat(s.locale_elo?.elo, "crown") : ""),
96
+ m(".stats-fig.stats-games", { title: ts('Fjöldi viðureigna') },
97
+ s ? vwStat(s.games, "th") : ""),
98
+ m(".stats-fig.stats-win-ratio", { title: ts('Vinningshlutfall') },
99
+ vwStat(winRatio, "bookmark", "%")),
100
+ m(".stats-fig.stats-avg-score", { title: ts('Meðalstigafjöldi') },
101
+ vwStat(avgScore, "dashboard"))
102
+ ]
103
+ ) : ""
104
+ ]
105
+ );
106
+ }
107
+
108
+ };
109
+ };