PageSourceSearch

https://cardgames.ai/shared/game-stats.js

js cardgames.ai collected 2026-10-05 15:29:04 UTC 8,563 bytes, 257 lines download raw bytes

1/* CardGames.ai — Game Stats Utility */
2var GameStats = (function () {
3  'use strict';
4
5  var STORAGE_KEY = 'cardgames-stats';
6
7  function loadAll() {
8    try {
9      return JSON.parse(localStorage.getItem(STORAGE_KEY)) || {};
10    } catch (e) {
11      return {};
12    }
13  }
14
15  function saveAll(data) {
16    try {
17      localStorage.setItem(STORAGE_KEY, JSON.stringify(data));
18    } catch (e) { /* quota exceeded — silently fail */ }
19  }
20
21  function getDefault() {
22    return {
23      played: 0,
24      won: 0,
25      lost: 0,
26      draws: 0,
27      bestScore: 0,
28      bestTime: 0,
29      currentStreak: 0,
30      bestStreak: 0,
31      worstStreak: 0,
32      firstPlayed: 0,
33      lastPlayed: 0
34    };
35  }
36
37  function get(gameId) {
38    var all = loadAll();
39    return Object.assign(getDefault(), all[gameId] || {});
40  }
41
42  function update(gameId, fn) {
43    var all = loadAll();
44    var stats = Object.assign(getDefault(), all[gameId] || {});
45    var now = Date.now();
46    if (!stats.firstPlayed) stats.firstPlayed = now;
47    stats.lastPlayed = now;
48    fn(stats);
49    all[gameId] = stats;
50    saveAll(all);
51    return stats;
52  }
53
54  function recordWin(gameId, opts) {
55    opts = opts || {};
56    return update(gameId, function (s) {
57      s.played++;
58      s.won++;
59      s.currentStreak = Math.max(1, s.currentStreak + 1);
60      if (s.currentStreak > s.bestStreak) s.bestStreak = s.currentStreak;
61      if (opts.score && opts.score > s.bestScore) s.bestScore = opts.score;
62      if (opts.time && (s.bestTime === 0 || opts.time < s.bestTime)) s.bestTime = opts.time;
63    });
64  }
65
66  function recordLoss(gameId, opts) {
67    opts = opts || {};
68    return update(gameId, function (s) {
69      s.played++;
70      s.lost++;
71      s.currentStreak = Math.min(-1, s.currentStreak - 1);
72      if (s.currentStreak < s.worstStreak) s.worstStreak = s.currentStreak;
73      if (opts.score && opts.score > s.bestScore) s.bestScore = opts.score;
74    });
75  }
76
77  function recordDraw(gameId) {
78    return update(gameId, function (s) {
79      s.played++;
80      s.draws++;
81      s.currentStreak = 0;
82    });
83  }
84
85  function formatTime(seconds) {
86    if (!seconds) return '--';
87    var m = Math.floor(seconds / 60);
88    var s = seconds % 60;
89    return m + ':' + (s < 10 ? '0' : '') + s;
90  }
91
92  function formatDate(ts) {
93    if (!ts) return '--';
94    var d = new Date(ts);
95    return d.toLocaleDateString();
96  }
97
98  function daysSince(ts) {
99    if (!ts) return 0;
100    return Math.floor((Date.now() - ts) / 86400000);
101  }
102
103  function render(gameId, containerId, opts) {
104    opts = opts || {};
105    var stats = get(gameId);
106    var el = document.getElementById(containerId);
107    if (!el) return;
108
109    var winPct = stats.played > 0 ? Math.round((stats.won / stats.played) * 100) : 0;
110    var lossPct = stats.played > 0 ? Math.round((stats.lost / stats.played) * 100) : 0;
111    var streak = stats.currentStreak;
112    var streakLabel = streak > 0 ? streak + ' wins' : streak < 0 ? Math.abs(streak) + ' losses' : '--';
113
114    var rows = [];
115
116    if (stats.firstPlayed) {
117      var days = daysSince(stats.firstPlayed);
118      rows.push(['Tracking since', formatDate(stats.firstPlayed) + ' (' + days + ' days)']);
119    }
120
121    rows.push(['Games played', stats.played]);
122
123    if (!opts.hideWinPct) {
124      rows.push(['Won', stats.won + '/' + stats.played + ' (' + winPct + '%)']);
125      rows.push(['Lost', stats.lost + '/' + stats.played + ' (' + lossPct + '%)']);
126    }
127
128    if (opts.showDraws && stats.draws > 0) {
129      var drawPct = stats.played > 0 ? Math.round((stats.draws / stats.played) * 100) : 0;
130      rows.push(['Draws', stats.draws + '/' + stats.played + ' (' + drawPct + '%)']);
131    }
132
133    if (opts.showBestScore) {
134      rows.push(['Best score', stats.bestScore || '--']);
135    }
136
137    if (opts.showBestTime) {
138      rows.push(['Best time', formatTime(stats.bestTime)]);
139    }
140
141    if (!opts.hideStreak) {
142      rows.push(['Current streak', streakLabel]);
143      rows.push(['Longest win streak', stats.bestStreak ? stats.bestStreak : '--']);
144      if (stats.worstStreak) {
145        rows.push(['Longest loss streak', Math.abs(stats.worstStreak)]);
146      }
147    }
148
149    var html = '<table class="stats-table">';
150    html += '<thead><tr><th colspan="2">Your Statistics</th></tr></thead>';
151    html += '<tbody>';
152    for (var i = 0; i < rows.length; i++) {
153      html += '<tr><td>' + rows[i][0] + '</td><td>' + rows[i][1] + '</td></tr>';
154    }
155    html += '</tbody></table>';
156    el.innerHTML = html;
157  }
158
159  // Solitaire stats bridge — reads from the existing "Solitaire Games" localStorage
160  function renderSolitaire(gameName, containerId) {
161    var el = document.getElementById(containerId);
162    if (!el) return;
163
164    var rows = [];
165    try {
166      var gameState = JSON.parse(localStorage.getItem('Solitaire Games')) || {};
167      var s = gameState[gameName] || {};
168
169      var totalGames = s.totalGames || 0;
170      var gamesWon = s.gamesWon || 0;
171      var totalMoves = s.totalMoves || 0;
172      var winPct = totalGames > 0 ? Math.round((gamesWon / totalGames) * 100) : 0;
173      var gamesLost = totalGames - gamesWon;
174      var lossPct = totalGames > 0 ? Math.round((gamesLost / totalGames) * 100) : 0;
175      var avgMoves = gamesWon > 0 ? Math.round(totalMoves / gamesWon) : '--';
176
177      rows.push(['Games played', totalGames]);
178      rows.push(['Won', gamesWon + '/' + totalGames + ' (' + winPct + '%)']);
179      rows.push(['Lost', gamesLost + '/' + totalGames + ' (' + lossPct + '%)']);
180      rows.push(['Total moves', totalMoves]);
181      rows.push(['Avg. moves per win', avgMoves]);
182
183      var streak = s.currStreak || 0;
184      var streakLabel = streak > 0 ? streak + ' wins' : streak < 0 ? Math.abs(streak) + ' losses' : '--';
185      rows.push(['Current streak', streakLabel]);
186      rows.push(['Longest win streak', s.bestStreak || '--']);
187      if (s.worstStreak) {
188        rows.push(['Longest loss streak', Math.abs(s.worstStreak)]);
189      }
190      rows.push(['Best completion %', (s.bestPercent || 0) + '%']);
191    } catch (e) {
192      rows.push(['No statistics yet', '--']);
193    }
194
195    var html = '<table class="stats-table">';
196    html += '<thead><tr><th colspan="2">Your Statistics</th></tr></thead>';
197    html += '<tbody>';
198    for (var i = 0; i < rows.length; i++) {
199      html += '<tr><td>' + rows[i][0] + '</td><td>' + rows[i][1] + '</td></tr>';
200    }
201    html += '</tbody></table>';
202    el.innerHTML = html;
203  }
204
205  function renderMulti(baseId, levels, containerId, opts) {
206    opts = opts || {};
207    var el = document.getElementById(containerId);
208    if (!el) return;
209
210    var html = '<div class="stats-level-tabs">';
211    for (var i = 0; i < levels.length; i++) {
212      var label = levels[i].charAt(0).toUpperCase() + levels[i].slice(1);
213      html += '<button class="stats-level-tab' + (i === 0 ? ' active' : '') + '" data-stats-level="' + levels[i] + '">' + label + '</button>';
214    }
215    html += '</div>';
216    html += '<div class="stats-level-content" id="' + containerId + '-content"></div>';
217    el.innerHTML = html;
218
219    // render first tab
220    render(baseId + '-' + levels[0], containerId + '-content', opts);
221
222    // tab click handlers
223    var tabs = el.querySelectorAll('.stats-level-tab');
224    for (var j = 0; j < tabs.length; j++) {
225      tabs[j].addEventListener('click', (function(lvl) {
226        return function() {
227          var allTabs = el.querySelectorAll('.stats-level-tab');
228          for (var k = 0; k < allTabs.length; k++) allTabs[k].classList.remove('active');
229          this.classList.add('active');
230          render(baseId + '-' + lvl, containerId + '-content', opts);
231        };
232      })(tabs[j].dataset.statsLevel));
233    }
234  }
235
236  // Auto-init: toggle button and initial render
237  document.addEventListener('DOMContentLoaded', function () {
238    var toggle = document.getElementById('stats-toggle');
239    var container = document.getElementById('stats-container');
240    if (toggle && container) {
241      toggle.addEventListener('click', function () {
242        container.classList.toggle('hidden');
243        toggle.textContent = container.classList.contains('hidden') ? 'Stats' : 'Hide Stats';
244      });
245    }
246  });
247
248  return {
249    get: get,
250    recordWin: recordWin,
251    recordLoss: recordLoss,
252    recordDraw: recordDraw,
253    render: render,
254    renderMulti: renderMulti,
255    renderSolitaire: renderSolitaire
256  };
257})();

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.