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.