← Volver a código

Tic Tac Toe

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>Tic Tac Toe</title>
  7. <link rel="stylesheet" href="css/tictactoe.css">
  8. </head>
  9. <body>
  10. <!-- Estructura del juego (canvas, HUD, botones…) -->
  11. <script src="js/tictactoe.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. .ttt-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. align-items: center;
  64. padding: 18px 26px 24px;
  65. border-right: 1px solid rgba(255, 255, 255, 0.08);
  66. }
  67. .ttt-logo {
  68. display: flex;
  69. align-items: center;
  70. justify-content: center;
  71. gap: 12px;
  72. min-height: 66px;
  73. margin: 0 auto 6px;
  74. color: var(--purple-soft);
  75. font-size: clamp(28px, 3.4vw, 46px);
  76. font-weight: 1000;
  77. letter-spacing: 0.08em;
  78. text-transform: uppercase;
  79. text-shadow: 0 0 18px rgba(165, 92, 255, 0.5);
  80. }
  81. .ttt-logo .lg-dot { color: var(--cyan); }
  82. .ttt-turn {
  83. display: flex;
  84. align-items: center;
  85. justify-content: center;
  86. gap: 10px;
  87. min-height: 30px;
  88. margin-bottom: 16px;
  89. font-size: 13px;
  90. font-weight: 800;
  91. letter-spacing: 0.08em;
  92. text-transform: uppercase;
  93. color: var(--muted);
  94. }
  95. .ttt-turn b { color: var(--cyan); font-size: 18px; }
  96. .ttt-turn.o-turn b { color: var(--purple-soft); }
  97. /* ===== Tablero ===== */
  98. .ttt-wrap {
  99. position: relative;
  100. display: grid;
  101. place-items: center;
  102. flex: 1;
  103. width: 100%;
  104. }
  105. .ttt-board {
  106. display: grid;
  107. grid-template-columns: repeat(3, 1fr);
  108. grid-template-rows: repeat(3, 1fr);
  109. gap: 12px;
  110. width: min(440px, 78vw);
  111. aspect-ratio: 1 / 1;
  112. padding: 14px;
  113. border: 2px solid rgba(181, 108, 255, 0.5);
  114. border-radius: 20px;
  115. background:
  116. radial-gradient(120% 120% at 50% 0%, rgba(123, 92, 255, 0.12), transparent 60%),
  117. #03060c;
  118. box-shadow: 0 0 30px rgba(123, 92, 255, 0.18), inset 0 0 40px rgba(0, 0, 0, 0.7);
  119. }
  120. .cell {
  121. position: relative;
  122. display: grid;
  123. place-items: center;
  124. border: 1px solid rgba(255, 255, 255, 0.1);
  125. border-radius: 14px;
  126. background: rgba(255, 255, 255, 0.03);
  127. transition: background 0.15s ease, box-shadow 0.15s ease, border-color 0.15s ease;
  128. }
  129. .cell:not(.filled):hover {
  130. background: rgba(123, 92, 255, 0.12);
  131. border-color: rgba(181, 108, 255, 0.6);
  132. box-shadow: inset 0 0 20px rgba(123, 92, 255, 0.15);
  133. }
  134. .cell.disabled { pointer-events: none; }
  135. .cell svg { width: 62%; height: 62%; overflow: visible; }
  136. .mark-line {
  137. fill: none;
  138. stroke-width: 9;
  139. stroke-linecap: round;
  140. stroke-dasharray: 100;
  141. stroke-dashoffset: 100;
  142. animation: draw 0.28s ease forwards;
  143. }
  144. .mark-line.d2 { animation-delay: 0.18s; }
  145. @keyframes draw { to { stroke-dashoffset: 0; } }
  146. .cell.x .mark-line { stroke: var(--cyan); filter: drop-shadow(0 0 8px rgba(36, 246, 210, 0.6)); }
  147. .cell.o .mark-line { stroke: var(--purple-soft); filter: drop-shadow(0 0 8px rgba(181, 108, 255, 0.6)); }
  148. .cell.win {
  149. border-color: var(--green);
  150. box-shadow: 0 0 0 2px var(--green), 0 0 30px rgba(34, 197, 94, 0.45);
  151. animation: winpulse 0.9s ease infinite alternate;
  152. }
  153. @keyframes winpulse { to { box-shadow: 0 0 0 2px var(--green), 0 0 45px rgba(34, 197, 94, 0.7); } }
  154. /* Overlay resultado */
  155. .ttt-overlay {
  156. position: absolute;
  157. inset: 0;
  158. z-index: 5;
  159. display: none;
  160. place-items: center;
  161. border-radius: 20px;
  162. background: rgba(3, 6, 12, 0.72);
  163. backdrop-filter: blur(3px);
  164. text-align: center;
  165. }
  166. .ttt-overlay.show { display: grid; }
  167. .ov-title {
  168. font-size: clamp(30px, 5vw, 46px);
  169. font-weight: 1000;
  170. letter-spacing: 0.03em;
  171. }
  172. .ov-title.x { color: var(--cyan); }
  173. .ov-title.o { color: var(--purple-soft); }
  174. .ov-title.draw { color: var(--muted); }
  175. .ov-sub { margin-top: 8px; color: rgba(255, 255, 255, 0.85); font-size: 15px; }
  176. .ov-btn {
  177. margin-top: 16px;
  178. padding: 11px 22px;
  179. border: 2px solid var(--purple-soft);
  180. border-radius: 12px;
  181. color: var(--purple-soft);
  182. background: rgba(24, 14, 46, 0.9);
  183. font-weight: 900;
  184. letter-spacing: 0.05em;
  185. text-transform: uppercase;
  186. }
  187. .ov-btn:hover { color: #fff; box-shadow: 0 0 22px rgba(165, 92, 255, 0.4); }
  188. /* ===== Columna control ===== */
  189. .control-column {
  190. display: flex;
  191. min-width: 0;
  192. flex-direction: column;
  193. padding: 24px 28px 22px;
  194. }
  195. .mode-switch {
  196. display: grid;
  197. grid-template-columns: 1fr 1fr;
  198. gap: 8px;
  199. padding: 6px;
  200. margin-bottom: 20px;
  201. border: 1px solid rgba(255, 255, 255, 0.12);
  202. border-radius: 14px;
  203. background: rgba(255, 255, 255, 0.03);
  204. }
  205. .mode-btn {
  206. min-height: 42px;
  207. border: 0;
  208. border-radius: 10px;
  209. color: var(--muted);
  210. background: transparent;
  211. font-size: 12px;
  212. font-weight: 800;
  213. letter-spacing: 0.05em;
  214. text-transform: uppercase;
  215. transition: 0.15s ease;
  216. }
  217. .mode-btn.active {
  218. color: #fff;
  219. background: rgba(123, 92, 255, 0.25);
  220. box-shadow: 0 0 16px rgba(165, 92, 255, 0.3);
  221. }
  222. .desktop-stats {
  223. display: grid;
  224. grid-template-columns: repeat(3, 1fr);
  225. gap: 12px;
  226. }
  227. .stat-card {
  228. display: flex;
  229. flex-direction: column;
  230. align-items: center;
  231. gap: 4px;
  232. padding: 14px 8px;
  233. border: 1px solid rgba(255, 255, 255, 0.14);
  234. border-radius: 16px;
  235. background: rgba(255, 255, 255, 0.035);
  236. font-size: 10px;
  237. font-weight: 800;
  238. letter-spacing: 0.06em;
  239. text-transform: uppercase;
  240. color: var(--muted);
  241. }
  242. .stat-card b { font-size: 24px; line-height: 1; color: var(--yellow); }
  243. .stat-card.x b { color: var(--cyan); }
  244. .stat-card.o b { color: var(--purple-soft); }
  245. .rules-block { margin: 22px 0; }
  246. .rules-block h2 {
  247. margin-bottom: 16px;
  248. color: var(--purple);
  249. font-size: 18px;
  250. font-weight: 900;
  251. letter-spacing: 0.045em;
  252. }
  253. .rules-block p { margin: 12px 0; font-size: 15px; line-height: 1.6; color: rgba(255, 255, 255, 0.82); }
  254. .rules-block strong { color: var(--cyan); }
  255. .action-row {
  256. display: grid;
  257. grid-template-columns: 1fr;
  258. gap: 12px;
  259. margin-top: auto;
  260. }
  261. .arcade-action {
  262. min-height: 46px;
  263. border: 1px solid rgba(181, 108, 255, 0.45);
  264. border-radius: 13px;
  265. color: var(--purple);
  266. background: rgba(123, 92, 255, 0.08);
  267. font-size: 12px;
  268. font-weight: 900;
  269. letter-spacing: 0.04em;
  270. }
  271. .arcade-action:hover { color: #fff; background: rgba(123, 92, 255, 0.19); }
  272. .btn-start-ttt {
  273. display: flex;
  274. align-items: center;
  275. justify-content: center;
  276. gap: 12px;
  277. width: 100%;
  278. min-height: 58px;
  279. border: 2px solid var(--purple-soft);
  280. border-radius: 12px;
  281. color: var(--purple-soft);
  282. background: rgba(24, 14, 46, 0.9);
  283. font-size: 18px;
  284. font-weight: 900;
  285. letter-spacing: 0.06em;
  286. text-transform: uppercase;
  287. box-shadow: 0 0 18px rgba(165, 92, 255, 0.25);
  288. }
  289. .btn-start-ttt:hover { color: #fff; box-shadow: 0 0 28px rgba(165, 92, 255, 0.45); }
  290. /* ===== Responsive ===== */
  291. @media (max-width: 1050px) {
  292. .arcade-shell { grid-template-columns: minmax(0, 1.4fr) minmax(300px, 0.85fr); }
  293. }
  294. @media (max-width: 820px) {
  295. .ttt-page { width: calc(100% - 24px); }
  296. .arcade-shell { display: block; min-height: 0; border-radius: 22px; }
  297. .board-column { padding: 18px 16px 18px; border-right: 0; }
  298. .control-column { padding: 4px 16px 20px; border-top: 1px solid rgba(255, 255, 255, 0.08); }
  299. .action-row { margin-top: 18px; }
  300. }
  301. @media (max-width: 480px) {
  302. .ttt-page { width: calc(100% - 16px); }
  303. .desktop-stats { gap: 8px; }
  304. .stat-card b { font-size: 20px; }
  305. }

JS

  1. (function () {
  2. 'use strict';
  3. const $ = (id) => document.getElementById(id);
  4. const boardEl = $('board');
  5. const turnWrap = $('turnWrap');
  6. const turnMark = $('turnMark');
  7. const overlay = $('overlay');
  8. const ovTitle = $('overlayTitle');
  9. const ovSub = $('overlaySub');
  10. const playAgainBtn = $('playAgainBtn');
  11. const newGameBtn = $('newGameBtn');
  12. const resetScoreBtn = $('resetScoreBtn');
  13. const modeAI = $('modeAI');
  14. const mode2P = $('mode2P');
  15. const scoreXEl = $('scoreX');
  16. const scoreOEl = $('scoreO');
  17. const scoreDEl = $('scoreD');
  18. const STORE_KEY = 'ttt_neon_v1';
  19. const es = {
  20. turn: 'Turno', win: '¡Gana', draw: '¡Empate!', again: '¿Otra partida?',
  21. youWin: '¡Ganas!', aiWin: 'Gana la IA', reset: 'Marcador reseteado'
  22. };
  23. const en = {
  24. turn: 'Turn', win: 'Wins', draw: 'Draw!', again: 'Another game?',
  25. youWin: 'You win!', aiWin: 'AI wins', reset: 'Score reset'
  26. };
  27. const T = (window.APP_LOCALE === 'en') ? en : es;
  28. const WINS = [
  29. [0, 1, 2], [3, 4, 5], [6, 7, 8],
  30. [0, 3, 6], [1, 4, 7], [2, 5, 8],
  31. [0, 4, 8], [2, 4, 6]
  32. ];
  33. let board, current, over, mode, scores;
  34. scores = load();
  35. renderScores();
  36. buildBoard();
  37. newGame();
  38. modeAI.addEventListener('click', () => setMode('ai'));
  39. mode2P.addEventListener('click', () => setMode('2p'));
  40. playAgainBtn.addEventListener('click', newGame);
  41. newGameBtn.addEventListener('click', newGame);
  42. resetScoreBtn.addEventListener('click', () => {
  43. scores = { x: 0, o: 0, d: 0 };
  44. save();
  45. renderScores();
  46. });
  47. function setMode(m) {
  48. mode = m;
  49. modeAI.classList.toggle('active', m === 'ai');
  50. mode2P.classList.toggle('active', m === '2p');
  51. newGame();
  52. }
  53. function buildBoard() {
  54. boardEl.innerHTML = '';
  55. for (let i = 0; i < 9; i++) {
  56. const c = document.createElement('button');
  57. c.className = 'cell';
  58. c.type = 'button';
  59. c.dataset.i = i;
  60. c.addEventListener('click', () => onCell(i));
  61. boardEl.appendChild(c);
  62. }
  63. }
  64. function newGame() {
  65. board = Array(9).fill('');
  66. current = 'X';
  67. over = false;
  68. if (mode === undefined) mode = 'ai';
  69. overlay.classList.remove('show');
  70. Array.from(boardEl.children).forEach((c) => {
  71. c.className = 'cell';
  72. c.innerHTML = '';
  73. });
  74. updateTurn();
  75. }
  76. function onCell(i) {
  77. if (over || board[i]) return;
  78. if (mode === 'ai' && current !== 'X') return;
  79. move(i, current);
  80. if (over) return;
  81. if (mode === 'ai' && current === 'O') {
  82. lockBoard(true);
  83. setTimeout(() => {
  84. const best = bestMove();
  85. if (best > -1) move(best, 'O');
  86. lockBoard(false);
  87. }, 380);
  88. }
  89. }
  90. function move(i, player) {
  91. board[i] = player;
  92. paint(i, player);
  93. const line = winningLine(board, player);
  94. if (line) return finish(player, line);
  95. if (board.every((v) => v)) return finish('D', null);
  96. current = player === 'X' ? 'O' : 'X';
  97. updateTurn();
  98. }
  99. function paint(i, player) {
  100. const cell = boardEl.children[i];
  101. cell.classList.add('filled', player.toLowerCase());
  102. cell.innerHTML = player === 'X'
  103. ? '<svg viewBox="0 0 100 100"><line class="mark-line" pathLength="100" x1="20" y1="20" x2="80" y2="80"/><line class="mark-line d2" pathLength="100" x1="80" y1="20" x2="20" y2="80"/></svg>'
  104. : '<svg viewBox="0 0 100 100"><circle class="mark-line" pathLength="100" cx="50" cy="50" r="32"/></svg>';
  105. }
  106. function finish(winner, line) {
  107. over = true;
  108. if (winner === 'D') {
  109. scores.d++;
  110. show(T.draw, T.again, 'draw');
  111. } else {
  112. scores[winner.toLowerCase()]++;
  113. if (line) line.forEach((idx) => boardEl.children[idx].classList.add('win'));
  114. let title;
  115. if (mode === 'ai') title = winner === 'X' ? T.youWin : T.aiWin;
  116. else title = (T.win === 'Wins' ? winner + ' ' + T.win : T.win + ' ' + winner + '!');
  117. show(title, T.again, winner.toLowerCase());
  118. }
  119. save();
  120. renderScores();
  121. }
  122. function show(title, sub, cls) {
  123. ovTitle.textContent = title;
  124. ovTitle.className = 'ov-title ' + cls;
  125. ovSub.textContent = sub;
  126. setTimeout(() => overlay.classList.add('show'), 500);
  127. }
  128. function updateTurn() {
  129. turnMark.textContent = current;
  130. turnWrap.classList.toggle('o-turn', current === 'O');
  131. }
  132. function lockBoard(v) {
  133. Array.from(boardEl.children).forEach((c) => c.classList.toggle('disabled', v));
  134. }
  135. function winningLine(b, p) {
  136. for (const w of WINS) {
  137. if (b[w[0]] === p && b[w[1]] === p && b[w[2]] === p) return w;
  138. }
  139. return null;
  140. }
  141. /* IA minimax (O maximiza) */
  142. function bestMove() {
  143. let bestScore = -Infinity;
  144. let best = -1;
  145. for (let i = 0; i < 9; i++) {
  146. if (!board[i]) {
  147. board[i] = 'O';
  148. const s = minimax(board, 0, false);
  149. board[i] = '';
  150. if (s > bestScore) { bestScore = s; best = i; }
  151. }
  152. }
  153. return best;
  154. }
  155. function minimax(b, depth, isMax) {
  156. if (winningLine(b, 'O')) return 10 - depth;
  157. if (winningLine(b, 'X')) return depth - 10;
  158. if (b.every((v) => v)) return 0;
  159. if (isMax) {
  160. let best = -Infinity;
  161. for (let i = 0; i < 9; i++) {
  162. if (!b[i]) { b[i] = 'O'; best = Math.max(best, minimax(b, depth + 1, false)); b[i] = ''; }
  163. }
  164. return best;
  165. }
  166. let best = Infinity;
  167. for (let i = 0; i < 9; i++) {
  168. if (!b[i]) { b[i] = 'X'; best = Math.min(best, minimax(b, depth + 1, true)); b[i] = ''; }
  169. }
  170. return best;
  171. }
  172. function renderScores() {
  173. scoreXEl.textContent = scores.x;
  174. scoreOEl.textContent = scores.o;
  175. scoreDEl.textContent = scores.d;
  176. }
  177. function save() { try { localStorage.setItem(STORE_KEY, JSON.stringify(scores)); } catch (e) {} }
  178. function load() {
  179. try {
  180. const raw = localStorage.getItem(STORE_KEY);
  181. if (!raw) return { x: 0, o: 0, d: 0 };
  182. const p = JSON.parse(raw);
  183. return { x: +p.x || 0, o: +p.o || 0, d: +p.d || 0 };
  184. } catch (e) { return { x: 0, o: 0, d: 0 }; }
  185. }
  186. })();