← Volver a código

Piedra Papel Tijera

Explora el código (HTML / CSS / JS) del juego

HTML

  1. <!doctype html>
  2. <html lang="es">
  3. <head>
  4. <meta charset="utf-8">
  5. <meta name="viewport" content="width=device-width, initial-scale=1">
  6. <title>Piedra Papel Tijera</title>
  7. <link rel="stylesheet" href="css/piedra.css">
  8. </head>
  9. <body>
  10. <!-- Estructura del juego (canvas, HUD, botones…) -->
  11. <script src="js/piedra.js"></script>
  12. </body>
  13. </html>

CSS

  1. * { box-sizing: border-box; margin: 0; padding: 0; }
  2. :root {
  3. --bg: #06050c;
  4. --purple: #a55cff;
  5. --purple-soft: #b56cff;
  6. --cyan: #24f6d2;
  7. --yellow: #ffd719;
  8. --green: #22c55e;
  9. --red: #ff4d6a;
  10. --text: #ffffff;
  11. --muted: rgba(255, 255, 255, 0.64);
  12. }
  13. html, body { width: 100%; min-height: 100%; overflow-x: hidden; }
  14. body {
  15. min-width: 320px;
  16. color: var(--text);
  17. background-color: var(--bg);
  18. background-position: center;
  19. background-size: cover;
  20. background-attachment: fixed;
  21. font-family: "Segoe UI", system-ui, -apple-system, Arial, sans-serif;
  22. }
  23. button, a { font: inherit; -webkit-tap-highlight-color: transparent; }
  24. button { cursor: pointer; }
  25. /* ===== Página / shell ===== */
  26. .ppt-page {
  27. width: min(1400px, 94vw);
  28. margin: 0 auto;
  29. padding-bottom: 26px;
  30. }
  31. .game-toolbar {
  32. display: flex;
  33. align-items: center;
  34. justify-content: space-between;
  35. min-height: 56px;
  36. padding: 0 4px;
  37. }
  38. .toolbar-link {
  39. color: var(--purple);
  40. text-decoration: none;
  41. font-size: 13px;
  42. font-weight: 850;
  43. letter-spacing: 0.06em;
  44. background: none;
  45. border: 0;
  46. }
  47. .toolbar-link:hover { color: #d3a7ff; }
  48. .arcade-shell {
  49. display: grid;
  50. grid-template-columns: minmax(0, 1.55fr) minmax(330px, 0.95fr);
  51. min-height: calc(100svh - 160px);
  52. overflow: hidden;
  53. border: 1px solid rgba(181, 108, 255, 0.48);
  54. border-radius: 28px;
  55. background: linear-gradient(135deg, rgba(11, 8, 25, 0.94), rgba(5, 4, 14, 0.92));
  56. box-shadow: 0 35px 100px rgba(0, 0, 0, 0.62);
  57. backdrop-filter: blur(9px);
  58. }
  59. .board-column {
  60. display: flex;
  61. min-width: 0;
  62. flex-direction: column;
  63. padding: 18px 26px 22px;
  64. border-right: 1px solid rgba(255, 255, 255, 0.08);
  65. }
  66. .ppt-logo {
  67. display: flex;
  68. align-items: center;
  69. justify-content: center;
  70. gap: 14px;
  71. min-height: 70px;
  72. margin: 0 auto 10px;
  73. color: var(--purple-soft);
  74. font-size: clamp(28px, 3.4vw, 46px);
  75. font-weight: 1000;
  76. line-height: 1;
  77. letter-spacing: 0.06em;
  78. text-transform: uppercase;
  79. text-shadow: 0 0 18px rgba(165, 92, 255, 0.55);
  80. }
  81. .ppt-logo .lg-dot { color: var(--cyan); }
  82. /* ===== Arena ===== */
  83. .ppt-arena {
  84. position: relative;
  85. flex: 1;
  86. display: grid;
  87. grid-template-columns: 1fr auto 1fr;
  88. align-items: center;
  89. gap: clamp(10px, 2.5vw, 36px);
  90. min-height: 360px;
  91. padding: clamp(18px, 4vh, 44px) clamp(12px, 2vw, 26px);
  92. overflow: hidden;
  93. border: 2px solid rgba(181, 108, 255, 0.5);
  94. border-radius: 18px;
  95. background:
  96. radial-gradient(120% 90% at 50% 0%, rgba(123, 92, 255, 0.12), transparent 60%),
  97. #03060c;
  98. box-shadow: 0 0 30px rgba(123, 92, 255, 0.18), inset 0 0 40px rgba(0, 0, 0, 0.7);
  99. }
  100. .ppt-side {
  101. display: grid;
  102. gap: 12px;
  103. justify-items: center;
  104. }
  105. .ppt-tag {
  106. font-size: 12px;
  107. font-weight: 800;
  108. letter-spacing: 0.14em;
  109. text-transform: uppercase;
  110. color: rgba(255, 255, 255, 0.72);
  111. }
  112. .ppt-side.you .ppt-tag { color: var(--cyan); }
  113. .ppt-side.ai .ppt-tag { color: var(--red); }
  114. /* Carta 3D */
  115. .flip-card {
  116. width: min(210px, 30vw);
  117. aspect-ratio: 3 / 4;
  118. perspective: 1200px;
  119. }
  120. .flip-inner {
  121. position: relative;
  122. width: 100%;
  123. height: 100%;
  124. transform-style: preserve-3d;
  125. transition: transform 0.6s cubic-bezier(0.6, 0.2, 0.2, 1);
  126. }
  127. .flip-card.flipped .flip-inner { transform: rotateY(180deg); }
  128. .flip-face {
  129. position: absolute;
  130. inset: 0;
  131. display: grid;
  132. place-items: center;
  133. border-radius: 18px;
  134. backface-visibility: hidden;
  135. -webkit-backface-visibility: hidden;
  136. }
  137. .flip-front {
  138. border: 2px solid rgba(181, 108, 255, 0.55);
  139. background:
  140. repeating-linear-gradient(45deg, rgba(123, 92, 255, 0.10) 0 10px, transparent 10px 20px),
  141. rgba(12, 9, 26, 0.96);
  142. color: rgba(181, 108, 255, 0.85);
  143. font-size: clamp(48px, 8vw, 78px);
  144. font-weight: 900;
  145. }
  146. .flip-back {
  147. transform: rotateY(180deg);
  148. border: 2px solid rgba(255, 255, 255, 0.14);
  149. background: rgba(255, 255, 255, 0.05);
  150. color: #fff;
  151. font-size: clamp(54px, 9vw, 96px);
  152. }
  153. .flip-back i { filter: drop-shadow(0 6px 18px rgba(0, 0, 0, 0.5)); }
  154. /* Resultado por carta */
  155. .flip-card.win .flip-back {
  156. border-color: var(--green);
  157. box-shadow: 0 0 0 2px var(--green), 0 0 45px rgba(34, 197, 94, 0.5);
  158. }
  159. .flip-card.win .flip-back i { color: var(--green); }
  160. .flip-card.lose .flip-inner { opacity: 0.45; filter: saturate(0.5); }
  161. .flip-card.tie .flip-back {
  162. border-color: var(--purple-soft);
  163. box-shadow: 0 0 0 2px rgba(181, 108, 255, 0.7), 0 0 40px rgba(123, 92, 255, 0.4);
  164. }
  165. .flip-card.tie .flip-back i { color: var(--purple-soft); }
  166. .flip-card.pulse { animation: ppt-pop 0.4s cubic-bezier(0.2, 0.9, 0.3, 1.5); }
  167. @keyframes ppt-pop {
  168. 0% { transform: scale(0.8); }
  169. 70% { transform: scale(1.06); }
  170. 100% { transform: scale(1); }
  171. }
  172. /* VS central */
  173. .ppt-vs {
  174. display: grid;
  175. place-items: center;
  176. width: clamp(54px, 7vw, 78px);
  177. height: clamp(54px, 7vw, 78px);
  178. border-radius: 50%;
  179. border: 2px solid rgba(255, 255, 255, 0.2);
  180. background: rgba(10, 8, 22, 0.9);
  181. color: #fff;
  182. font-size: clamp(18px, 2vw, 26px);
  183. font-weight: 1000;
  184. letter-spacing: 0.05em;
  185. box-shadow: 0 0 24px rgba(123, 92, 255, 0.35);
  186. }
  187. .ppt-vs.clash { animation: vs-clash 0.5s ease; }
  188. @keyframes vs-clash {
  189. 0% { transform: scale(1); }
  190. 40% { transform: scale(1.4); box-shadow: 0 0 60px rgba(181, 108, 255, 0.9); }
  191. 100% { transform: scale(1); }
  192. }
  193. /* Cuenta atrás + destello */
  194. .ppt-count {
  195. position: absolute;
  196. inset: 0;
  197. display: grid;
  198. place-items: center;
  199. z-index: 5;
  200. color: #fff;
  201. font-size: clamp(70px, 12vw, 130px);
  202. font-weight: 1000;
  203. pointer-events: none;
  204. opacity: 0;
  205. text-shadow: 0 0 30px rgba(123, 92, 255, 0.8);
  206. }
  207. .ppt-count.tick { animation: count-tick 0.45s ease; }
  208. @keyframes count-tick {
  209. 0% { opacity: 0; transform: scale(1.6); }
  210. 40% { opacity: 1; transform: scale(1); }
  211. 100% { opacity: 0; transform: scale(0.8); }
  212. }
  213. .ppt-flash {
  214. position: absolute;
  215. inset: 0;
  216. z-index: 4;
  217. pointer-events: none;
  218. opacity: 0;
  219. background: radial-gradient(circle at 50% 50%, rgba(255, 255, 255, 0.85), transparent 60%);
  220. }
  221. .ppt-flash.go { animation: flash-go 0.45s ease; }
  222. @keyframes flash-go { 0% { opacity: 0; } 30% { opacity: 0.9; } 100% { opacity: 0; } }
  223. /* Partículas del choque */
  224. .spark {
  225. position: absolute;
  226. z-index: 6;
  227. width: 10px;
  228. height: 10px;
  229. border-radius: 50%;
  230. pointer-events: none;
  231. will-change: transform, opacity;
  232. }
  233. /* Estado / resultado */
  234. .ppt-status {
  235. display: grid;
  236. gap: 4px;
  237. margin-top: 16px;
  238. text-align: center;
  239. }
  240. .ppt-result {
  241. min-height: 26px;
  242. font-size: clamp(18px, 2vw, 24px);
  243. font-weight: 900;
  244. letter-spacing: 0.03em;
  245. }
  246. .ppt-result.win { color: var(--green); text-shadow: 0 0 18px rgba(34, 197, 94, 0.5); }
  247. .ppt-result.lose { color: var(--red); text-shadow: 0 0 18px rgba(255, 77, 106, 0.5); }
  248. .ppt-result.tie { color: var(--purple-soft); }
  249. .ppt-detail { font-size: 13px; color: var(--muted); }
  250. /* Elección */
  251. .ppt-choices {
  252. display: grid;
  253. grid-template-columns: repeat(3, 1fr);
  254. gap: 12px;
  255. margin-top: 18px;
  256. }
  257. .ppt-choice {
  258. display: grid;
  259. place-items: center;
  260. gap: 8px;
  261. padding: 16px 10px;
  262. border: 1px solid rgba(181, 108, 255, 0.35);
  263. border-radius: 16px;
  264. background: rgba(123, 92, 255, 0.07);
  265. color: #fff;
  266. transition: transform 0.15s ease, background 0.15s ease, box-shadow 0.15s ease, border-color 0.15s ease;
  267. }
  268. .ppt-choice .ic { font-size: 34px; color: var(--purple-soft); }
  269. .ppt-choice .lb {
  270. font-size: 12px;
  271. font-weight: 800;
  272. letter-spacing: 0.06em;
  273. text-transform: uppercase;
  274. color: rgba(255, 255, 255, 0.82);
  275. }
  276. .ppt-choice:hover {
  277. transform: translateY(-4px);
  278. border-color: var(--purple-soft);
  279. background: rgba(123, 92, 255, 0.18);
  280. box-shadow: 0 14px 34px rgba(0, 0, 0, 0.4), 0 0 20px rgba(165, 92, 255, 0.3);
  281. }
  282. .ppt-choice:hover .ic { color: #fff; }
  283. .ppt-choice:disabled { opacity: 0.45; cursor: not-allowed; transform: none; box-shadow: none; }
  284. /* ===== Columna control ===== */
  285. .control-column {
  286. display: flex;
  287. min-width: 0;
  288. flex-direction: column;
  289. padding: 24px 28px 22px;
  290. }
  291. .desktop-stats {
  292. display: grid;
  293. grid-template-columns: repeat(3, 1fr);
  294. gap: 12px;
  295. }
  296. .stat-card {
  297. display: flex;
  298. flex-direction: column;
  299. align-items: center;
  300. gap: 4px;
  301. padding: 14px 8px;
  302. border: 1px solid rgba(255, 255, 255, 0.14);
  303. border-radius: 16px;
  304. background: rgba(255, 255, 255, 0.035);
  305. font-size: 10px;
  306. font-weight: 800;
  307. letter-spacing: 0.06em;
  308. text-transform: uppercase;
  309. color: var(--muted);
  310. }
  311. .stat-card b { font-size: 24px; line-height: 1; color: var(--yellow); }
  312. .stat-card.you b { color: var(--cyan); }
  313. .stat-card.ai b { color: var(--red); }
  314. .streak-badge {
  315. display: flex;
  316. align-items: center;
  317. justify-content: center;
  318. gap: 8px;
  319. margin-top: 14px;
  320. padding: 10px;
  321. border: 1px solid rgba(181, 108, 255, 0.35);
  322. border-radius: 13px;
  323. background: rgba(123, 92, 255, 0.08);
  324. font-size: 12px;
  325. font-weight: 800;
  326. letter-spacing: 0.05em;
  327. text-transform: uppercase;
  328. color: var(--muted);
  329. }
  330. .streak-badge b { color: var(--purple-soft); font-size: 16px; }
  331. .rules-block { margin: 22px 0; }
  332. .rules-block h2 {
  333. margin-bottom: 16px;
  334. color: var(--purple);
  335. font-size: 18px;
  336. font-weight: 900;
  337. letter-spacing: 0.045em;
  338. }
  339. .rules-block p {
  340. margin: 12px 0;
  341. font-size: 15px;
  342. line-height: 1.6;
  343. color: rgba(255, 255, 255, 0.82);
  344. }
  345. .rules-block strong { color: var(--cyan); }
  346. .action-row {
  347. display: grid;
  348. grid-template-columns: 1fr;
  349. gap: 12px;
  350. margin-top: auto;
  351. }
  352. .arcade-action {
  353. min-height: 46px;
  354. border: 1px solid rgba(181, 108, 255, 0.45);
  355. border-radius: 13px;
  356. color: var(--purple);
  357. background: rgba(123, 92, 255, 0.08);
  358. font-size: 12px;
  359. font-weight: 900;
  360. letter-spacing: 0.04em;
  361. }
  362. .arcade-action:hover { color: #fff; background: rgba(123, 92, 255, 0.19); }
  363. .btn-start-ppt {
  364. display: flex;
  365. align-items: center;
  366. justify-content: center;
  367. gap: 12px;
  368. width: 100%;
  369. min-height: 58px;
  370. border: 2px solid var(--purple-soft);
  371. border-radius: 12px;
  372. color: var(--purple-soft);
  373. background: rgba(24, 14, 46, 0.9);
  374. font-size: 18px;
  375. font-weight: 900;
  376. letter-spacing: 0.06em;
  377. text-transform: uppercase;
  378. box-shadow: 0 0 18px rgba(165, 92, 255, 0.25);
  379. }
  380. .btn-start-ppt:hover { color: #fff; box-shadow: 0 0 28px rgba(165, 92, 255, 0.45); }
  381. /* ===== Responsive ===== */
  382. .mobile-choices-note { display: none; }
  383. @media (max-width: 1050px) {
  384. .arcade-shell { grid-template-columns: minmax(0, 1.4fr) minmax(300px, 0.85fr); }
  385. }
  386. @media (max-width: 820px) {
  387. .ppt-page { width: calc(100% - 24px); }
  388. .arcade-shell { display: block; min-height: 0; border-radius: 22px; }
  389. .board-column { padding: 18px 16px 16px; border-right: 0; }
  390. .control-column {
  391. padding: 4px 16px 20px;
  392. border-top: 1px solid rgba(255, 255, 255, 0.08);
  393. }
  394. .action-row { margin-top: 18px; }
  395. }
  396. @media (max-width: 480px) {
  397. .ppt-page { width: calc(100% - 16px); }
  398. .flip-card { width: 40vw; }
  399. .ppt-choice { padding: 12px 6px; }
  400. .ppt-choice .ic { font-size: 28px; }
  401. .desktop-stats { gap: 8px; }
  402. .stat-card b { font-size: 20px; }
  403. }

JS

  1. (function () {
  2. 'use strict';
  3. const $ = (id) => document.getElementById(id);
  4. const cardYou = $('cardYou');
  5. const cardAI = $('cardAI');
  6. const backYou = $('backYou');
  7. const backAI = $('backAI');
  8. const vsBadge = $('vsBadge');
  9. const countEl = $('countEl');
  10. const flashEl = $('flashEl');
  11. const arenaEl = $('arena');
  12. const resultEl = $('resultLine');
  13. const detailEl = $('detailLine');
  14. const choiceButtons = Array.from(document.querySelectorAll('.ppt-choice'));
  15. const scoreYouEl = $('scoreYou');
  16. const scoreAIEl = $('scoreAI');
  17. const scoreTieEl = $('scoreTie');
  18. const streakEl = $('streakValue');
  19. const resetScoreBtn = $('resetScoreBtn');
  20. const playBtn = $('playBtn');
  21. const STORE_KEY = 'rps_neon_v1';
  22. const es = {
  23. rock: 'Piedra', paper: 'Papel', scissors: 'Tijera',
  24. win: '¡GANAS!', lose: '¡PIERDES!', tie: 'EMPATE',
  25. beats: 'vence a', ties: 'empata con',
  26. choose: 'Elige tu carta', go: '¡YA!',
  27. winStreak: 'racha', reset: 'Marcador reseteado'
  28. };
  29. const en = {
  30. rock: 'Rock', paper: 'Paper', scissors: 'Scissors',
  31. win: 'YOU WIN!', lose: 'YOU LOSE!', tie: 'DRAW',
  32. beats: 'beats', ties: 'ties with',
  33. choose: 'Pick your card', go: 'GO!',
  34. winStreak: 'streak', reset: 'Score reset'
  35. };
  36. const T = (window.APP_LOCALE === 'en') ? en : es;
  37. const ICON = { rock: 'fa-hand-back-fist', paper: 'fa-hand', scissors: 'fa-hand-scissors' };
  38. const NAME = { rock: T.rock, paper: T.paper, scissors: T.scissors };
  39. const iconHTML = (k) => '<i class="fa-solid ' + ICON[k] + '"></i>';
  40. let locked = false;
  41. let scores = load();
  42. let streak = 0;
  43. render();
  44. choiceButtons.forEach((btn) => {
  45. btn.addEventListener('click', () => {
  46. if (locked) return;
  47. play(btn.dataset.move);
  48. });
  49. });
  50. playBtn.addEventListener('click', resetRound);
  51. resetScoreBtn.addEventListener('click', () => {
  52. scores = { you: 0, ai: 0, tie: 0 };
  53. streak = 0;
  54. save();
  55. render();
  56. detailEl.textContent = T.reset;
  57. });
  58. function play(youKey) {
  59. locked = true;
  60. disable(true);
  61. clearResult();
  62. const aiKey = randomKey();
  63. resultEl.textContent = '';
  64. detailEl.textContent = '';
  65. // Cuenta atrás 3 · 2 · 1
  66. const seq = ['3', '2', '1'];
  67. let i = 0;
  68. tick(seq[i]);
  69. const iv = setInterval(() => {
  70. i++;
  71. if (i < seq.length) { tick(seq[i]); }
  72. }, 520);
  73. setTimeout(() => {
  74. clearInterval(iv);
  75. reveal(youKey, aiKey);
  76. }, 520 * seq.length + 120);
  77. }
  78. function tick(txt) {
  79. countEl.textContent = txt;
  80. countEl.classList.remove('tick');
  81. void countEl.offsetWidth;
  82. countEl.classList.add('tick');
  83. }
  84. function reveal(youKey, aiKey) {
  85. // Destello + choque
  86. flashEl.classList.remove('go');
  87. void flashEl.offsetWidth;
  88. flashEl.classList.add('go');
  89. countEl.textContent = T.go;
  90. countEl.classList.remove('tick');
  91. void countEl.offsetWidth;
  92. countEl.classList.add('tick');
  93. vsBadge.classList.remove('clash');
  94. void vsBadge.offsetWidth;
  95. vsBadge.classList.add('clash');
  96. backYou.innerHTML = iconHTML(youKey);
  97. backAI.innerHTML = iconHTML(aiKey);
  98. cardYou.classList.add('flipped', 'pulse');
  99. cardAI.classList.add('flipped', 'pulse');
  100. setTimeout(() => { cardYou.classList.remove('pulse'); cardAI.classList.remove('pulse'); }, 420);
  101. const r = result(youKey, aiKey);
  102. burst(r);
  103. if (r === 'win') {
  104. scores.you++;
  105. streak = streak >= 0 ? streak + 1 : 1;
  106. cardYou.classList.add('win');
  107. cardAI.classList.add('lose');
  108. resultEl.textContent = T.win;
  109. resultEl.classList.add('win');
  110. detailEl.textContent = NAME[youKey] + ' ' + T.beats + ' ' + NAME[aiKey];
  111. } else if (r === 'lose') {
  112. scores.ai++;
  113. streak = streak <= 0 ? streak - 1 : -1;
  114. cardAI.classList.add('win');
  115. cardYou.classList.add('lose');
  116. resultEl.textContent = T.lose;
  117. resultEl.classList.add('lose');
  118. detailEl.textContent = NAME[aiKey] + ' ' + T.beats + ' ' + NAME[youKey];
  119. } else {
  120. scores.tie++;
  121. streak = 0;
  122. cardYou.classList.add('tie');
  123. cardAI.classList.add('tie');
  124. resultEl.textContent = T.tie;
  125. resultEl.classList.add('tie');
  126. detailEl.textContent = NAME[youKey] + ' ' + T.ties + ' ' + NAME[aiKey];
  127. }
  128. save();
  129. render();
  130. locked = false;
  131. disable(false);
  132. }
  133. function burst(kind) {
  134. const rect = arenaEl.getBoundingClientRect();
  135. const cx = rect.width / 2;
  136. const cy = rect.height / 2;
  137. const color = kind === 'win' ? '#22c55e' : kind === 'lose' ? '#ff4d6a' : '#b56cff';
  138. const n = 22;
  139. for (let k = 0; k < n; k++) {
  140. const s = document.createElement('span');
  141. s.className = 'spark';
  142. s.style.left = cx + 'px';
  143. s.style.top = cy + 'px';
  144. s.style.background = color;
  145. s.style.boxShadow = '0 0 12px ' + color;
  146. arenaEl.appendChild(s);
  147. const ang = (Math.PI * 2 * k) / n + Math.random() * 0.5;
  148. const dist = 70 + Math.random() * 130;
  149. const dx = Math.cos(ang) * dist;
  150. const dy = Math.sin(ang) * dist;
  151. requestAnimationFrame(() => {
  152. s.style.transition = 'transform 0.6s cubic-bezier(0.2,0.7,0.2,1), opacity 0.6s ease';
  153. s.style.transform = 'translate(' + dx + 'px,' + dy + 'px) scale(' + (0.3 + Math.random()) + ')';
  154. s.style.opacity = '0';
  155. });
  156. setTimeout(() => s.remove(), 650);
  157. }
  158. }
  159. function resetRound() {
  160. clearResult();
  161. cardYou.classList.remove('flipped');
  162. cardAI.classList.remove('flipped');
  163. resultEl.textContent = '';
  164. detailEl.textContent = T.choose;
  165. disable(false);
  166. locked = false;
  167. }
  168. function clearResult() {
  169. ['win', 'lose', 'tie', 'pulse'].forEach((c) => {
  170. cardYou.classList.remove(c);
  171. cardAI.classList.remove(c);
  172. });
  173. resultEl.classList.remove('win', 'lose', 'tie');
  174. }
  175. function disable(v) { choiceButtons.forEach((b) => (b.disabled = v)); }
  176. function randomKey() {
  177. const keys = ['rock', 'paper', 'scissors'];
  178. return keys[Math.floor(Math.random() * keys.length)];
  179. }
  180. function result(you, ai) {
  181. if (you === ai) return 'tie';
  182. if ((you === 'rock' && ai === 'scissors') ||
  183. (you === 'paper' && ai === 'rock') ||
  184. (you === 'scissors' && ai === 'paper')) return 'win';
  185. return 'lose';
  186. }
  187. function render() {
  188. scoreYouEl.textContent = scores.you;
  189. scoreAIEl.textContent = scores.ai;
  190. scoreTieEl.textContent = scores.tie;
  191. if (streakEl) {
  192. const a = Math.abs(streak);
  193. streakEl.textContent = streak === 0 ? '—' : (streak > 0 ? '+' + a : '-' + a);
  194. }
  195. }
  196. function save() { try { localStorage.setItem(STORE_KEY, JSON.stringify(scores)); } catch (e) {} }
  197. function load() {
  198. try {
  199. const raw = localStorage.getItem(STORE_KEY);
  200. if (!raw) return { you: 0, ai: 0, tie: 0 };
  201. const p = JSON.parse(raw);
  202. return { you: +p.you || 0, ai: +p.ai || 0, tie: +p.tie || 0 };
  203. } catch (e) { return { you: 0, ai: 0, tie: 0 }; }
  204. }
  205. })();