Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
name: Deploy to GitHub Pages

on:
push:
branches:
- main
- claude/3d-cachipun-game-d23abx
workflow_dispatch:

permissions:
contents: read
pages: write
id-token: write

concurrency:
group: pages
cancel-in-progress: true

jobs:
deploy:
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Configure Pages
uses: actions/configure-pages@v5

- name: Upload site artifact
uses: actions/upload-pages-artifact@v3
with:
# The whole repo is the static site (no build step needed).
path: .

- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
node_modules/
.DS_Store
*.log
Empty file added .nojekyll
Empty file.
72 changes: 71 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,71 @@
# cachipun
# 🇨🇱 Cachipún 3D

Un **tres en línea (gato / "cachipún") a la chilena** con visuales impresionantes,
hecho 100% con [Three.js](https://threejs.org/) y sin paso de compilación.

Inspirado en los juegos virales de Three.js que circularon hace poco (estética
*liquid glass*, bloom, sombras suaves y partículas), pero llevado a un clásico
de patio.

## ✨ Qué tiene

- **Tablero de vidrio refractivo** (`MeshPhysicalMaterial` con `transmission`) y
reflejos realistas vía entorno PMREM.
- **Piezas de neón** (X cian, O magenta) que brillan gracias al post-proceso
**UnrealBloom**.
- **Sombras PCF suaves**, luces de relleno con color (rim lights) y un fondo
con degradado por shader + niebla.
- **Animaciones con easing** (rebote al colocar, resorte al ganar) mediante un
mini motor de tweens propio, sin dependencias extra.
- **Cámara que respira** con parallax según el mouse y barrido al celebrar.
- **Confeti** y línea ganadora iluminada.
- **Dos modos**: contra una **CPU imbatible** (minimax) o **2 jugadores**.
- **Marcador persistente** y partida en curso guardada en `localStorage`:
cierra la pestaña y vuelve justo donde quedaste.
- **Sonido** sintetizado con WebAudio (se puede silenciar).
- Responsive y jugable con mouse o touch.

## 🎮 Jugar

Abre la versión publicada en GitHub Pages o, en local, sirve la carpeta con
cualquier servidor estático (se usan módulos ES, así que `file://` no funciona):

```bash
# opción 1
npx serve .
# opción 2
python3 -m http.server 8000
```

Luego entra a `http://localhost:8000`.

## 🗂️ Estructura

```
index.html · markup + import map de Three.js (desde CDN)
styles.css · UI glassmorphism (HUD, marcador, banner)
src/
main.js · une lógica, escena, sonido y persistencia
scene.js · toda la escena 3D, efectos y animaciones
game.js · reglas, detección de ganador, CPU minimax, localStorage
tween.js · motor de animación con easing, sin dependencias
```

## 🚀 Despliegue en GitHub Pages

El repo incluye un workflow en `.github/workflows/deploy.yml` que publica el
sitio automáticamente. Solo hay que activar Pages una vez:

1. **Settings → Pages → Build and deployment → Source: GitHub Actions**.
2. Haz push a la rama configurada (`main` o `claude/3d-cachipun-game-d23abx`).
3. El workflow sube todo el repositorio tal cual (no hay build) y queda en línea.

> Nota: el entorno `github-pages` puede estar restringido por defecto a la rama
> por defecto. Si despliegas desde otra rama, permite esa rama en
> *Settings → Environments → github-pages → Deployment branches*.

## 📝 Nota cultural

En Chile el *cachipún* es estrictamente el "piedra, papel o tijera", mientras que
a esto se le suele decir *gato* o *tres en línea*. Aquí seguimos el nombre que
pidió el proyecto. 😄
88 changes: 88 additions & 0 deletions index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
<!DOCTYPE html>
<html lang="es-CL">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
<meta name="theme-color" content="#120a2a" />
<title>Cachipún 3D · Tres en línea a la chilena</title>
<meta name="description" content="Cachipún 3D — un tres en línea (gato) chileno con visuales impresionantes hechos en Three.js: vidrio refractivo, bloom y partículas. Juega contra la CPU o un amigo." />
<link rel="preconnect" href="https://cdn.jsdelivr.net" />
<link rel="stylesheet" href="./styles.css" />
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'%3E%3Ctext y='.9em' font-size='90'%3E%E2%9C%8A%3C/text%3E%3C/svg%3E" />

<!-- Three.js is vendored under /vendor so the game has no runtime CDN
dependency: it works offline and survives any CDN hiccup. -->
<script type="importmap">
{
"imports": {
"three": "./vendor/three.module.js",
"three/addons/": "./vendor/jsm/"
}
}
</script>
</head>
<body>
<!-- Loading veil -->
<div id="loader" class="loader">
<div class="loader__spinner"></div>
<div class="loader__text">cargando el cachipún…</div>
</div>

<!-- 3D canvas mounts here -->
<canvas id="scene"></canvas>

<!-- Glass UI overlay -->
<div id="ui" class="ui">
<header class="topbar">
<div class="brand">
<h1 class="brand__title">CACHIPÚN <span>3D</span></h1>
<p class="brand__sub">tres en línea a la chilena</p>
</div>
<div class="topbar__actions">
<button id="modeBtn" class="chip" title="Cambiar modo de juego">
<span class="chip__icon">🤖</span><span id="modeLabel">vs CPU</span>
</button>
<button id="soundBtn" class="chip" title="Activar / silenciar sonido">
<span id="soundIcon">🔊</span>
</button>
</div>
</header>

<section class="scoreboard">
<div class="score score--x">
<span class="score__tag" id="tagX">Tú</span>
<span class="score__val" id="scoreX">0</span>
</div>
<div class="score score--draw">
<span class="score__tag">Empates</span>
<span class="score__val" id="scoreD">0</span>
</div>
<div class="score score--o">
<span class="score__tag" id="tagO">CPU</span>
<span class="score__val" id="scoreO">0</span>
</div>
</section>

<div class="status" id="status">Turno de <b>X</b></div>

<footer class="bottombar">
<button id="resetBtn" class="btn">Nueva mano</button>
<button id="clearBtn" class="btn btn--ghost">Reiniciar marcador</button>
</footer>

<p class="hint">Hecho con Three.js · el estado se guarda solo en tu navegador</p>
</div>

<!-- Win banner -->
<div id="banner" class="banner">
<div class="banner__card">
<div class="banner__emoji" id="bannerEmoji">🎉</div>
<div class="banner__title" id="bannerTitle">¡Ganaste!</div>
<div class="banner__sub" id="bannerSub">Pura pinta nomás</div>
<button id="bannerBtn" class="btn">Otra vez</button>
</div>
</div>

<script type="module" src="./src/main.js"></script>
</body>
</html>
110 changes: 110 additions & 0 deletions src/game.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
// Pure game logic for Cachipún (tres en línea / tic-tac-toe).
// No rendering here — just state, rules, the CPU brain and persistence.

const STORAGE_KEY = "cachipun3d:v1";

export const LINES = [
[0, 1, 2], [3, 4, 5], [6, 7, 8], // rows
[0, 3, 6], [1, 4, 7], [2, 5, 8], // cols
[0, 4, 8], [2, 4, 6], // diagonals
];

export function emptyBoard() {
return Array(9).fill(null);
}

export function winningLine(board) {
for (const line of LINES) {
const [a, b, c] = line;
if (board[a] && board[a] === board[b] && board[a] === board[c]) {
return { player: board[a], line };
}
}
return null;
}

export function isFull(board) {
return board.every((c) => c !== null);
}

export function availableMoves(board) {
const moves = [];
for (let i = 0; i < 9; i++) if (!board[i]) moves.push(i);
return moves;
}

// Minimax CPU. `level` lets us add a little human-beatable randomness on easy.
export function cpuMove(board, cpu, human, level = "hard") {
const moves = availableMoves(board);
if (moves.length === 0) return -1;

// First move: grab a corner or center for a livelier opening.
if (moves.length === 9) {
const openers = [0, 2, 4, 6, 8];
return openers[Math.floor(Math.random() * openers.length)];
}

// Easy / medium: sometimes play a random legal move instead of the optimum.
const slipChance = level === "easy" ? 0.5 : level === "medium" ? 0.2 : 0;
if (Math.random() < slipChance) {
return moves[Math.floor(Math.random() * moves.length)];
}

let bestScore = -Infinity;
let best = moves[0];
for (const m of moves) {
board[m] = cpu;
const score = minimax(board, 0, false, cpu, human);
board[m] = null;
if (score > bestScore) {
bestScore = score;
best = m;
}
}
return best;
}

function minimax(board, depth, maximizing, cpu, human) {
const result = winningLine(board);
if (result) return result.player === cpu ? 10 - depth : depth - 10;
if (isFull(board)) return 0;

if (maximizing) {
let best = -Infinity;
for (const m of availableMoves(board)) {
board[m] = cpu;
best = Math.max(best, minimax(board, depth + 1, false, cpu, human));
board[m] = null;
}
return best;
} else {
let best = Infinity;
for (const m of availableMoves(board)) {
board[m] = human;
best = Math.min(best, minimax(board, depth + 1, true, cpu, human));
board[m] = null;
}
return best;
}
}

// ---------- persistence ----------
export function loadState() {
try {
const raw = localStorage.getItem(STORAGE_KEY);
if (!raw) return null;
const data = JSON.parse(raw);
if (!Array.isArray(data.board) || data.board.length !== 9) return null;
return data;
} catch {
return null;
}
}

export function saveState(state) {
try {
localStorage.setItem(STORAGE_KEY, JSON.stringify(state));
} catch {
/* storage full or unavailable — fail silently */
}
}
Loading
Loading