diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml
new file mode 100644
index 0000000..dcec89a
--- /dev/null
+++ b/.github/workflows/deploy.yml
@@ -0,0 +1,63 @@
+name: CI / Deploy to GitHub Pages
+
+on:
+ push:
+ branches: [main]
+ pull_request:
+ branches: [main]
+
+permissions:
+ contents: read
+
+jobs:
+ # ── Validate ─────────────────────────────────────────────────
+ validate:
+ name: Validate HTML & JS
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+
+ - name: Setup Node.js
+ uses: actions/setup-node@v4
+ with:
+ node-version: '20'
+
+ - name: Install validators
+ run: npm install -g html-validate
+
+ - name: Validate HTML
+ run: html-validate index.html
+
+ # ── Deploy (only on push to main) ────────────────────────────
+ deploy:
+ name: Deploy to GitHub Pages
+ needs: validate
+ if: github.event_name == 'push' && github.ref == 'refs/heads/main'
+ runs-on: ubuntu-latest
+
+ permissions:
+ contents: read
+ pages: write
+ id-token: write
+
+ environment:
+ name: github-pages
+ url: ${{ steps.deployment.outputs.page_url }}
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+
+ - name: Configure GitHub Pages
+ uses: actions/configure-pages@v4
+
+ - name: Upload Pages artifact
+ uses: actions/upload-pages-artifact@v3
+ with:
+ # Deploy everything in the repo root
+ path: '.'
+
+ - name: Deploy to GitHub Pages
+ id: deployment
+ uses: actions/deploy-pages@v4
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..e918b09
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,16 @@
+# Dependencies installed locally (vendored copy in /vendor is committed)
+node_modules/
+package-lock.json
+
+# OS artefacts
+.DS_Store
+Thumbs.db
+
+# Editor / IDE
+.vscode/
+.idea/
+*.swp
+*.swo
+
+# Logs
+*.log
diff --git a/.htmlvalidate.json b/.htmlvalidate.json
new file mode 100644
index 0000000..695b527
--- /dev/null
+++ b/.htmlvalidate.json
@@ -0,0 +1,8 @@
+{
+ "extends": ["html-validate:recommended"],
+ "rules": {
+ "no-inline-style": "off",
+ "svg-focusable": "off",
+ "long-title": "off"
+ }
+}
diff --git a/README.md b/README.md
index 6ac7c90..22f0f0c 100644
--- a/README.md
+++ b/README.md
@@ -1 +1,67 @@
-# SQL_Chess
\ No newline at end of file
+# ♟ SQL Chess
+
+> Play chess — and watch every move translated into real SQL queries in real time.
+
+[](https://github.com/Devn913/SQL_Chess/actions/workflows/deploy.yml)
+
+**Live demo:** https://devn913.github.io/SQL_Chess/
+
+---
+
+## Features
+
+| Feature | Details |
+|---|---|
+| ♟ **Fully playable chess** | All rules enforced via [chess.js](https://github.com/jhlywa/chess.js) — en passant, castling, pawn promotion, check/checkmate/stalemate |
+| ⬡ **SQL Panel** | Every move generates real `INSERT`, `UPDATE`, `DELETE` SQL statements with syntax highlighting |
+| □ **Toggle SQL** | Hide the SQL panel to play as a traditional chess board |
+| 👤 **Guest mode** | No login required — just open the page and play |
+| ⇗ **Invite link** | Click "Invite" to generate a shareable URL that encodes the full game state — anyone who opens it continues the same game |
+| ⇅ **Flip board** | Swap perspective between white and black |
+| ↩ **Undo** | Take back the last move |
+
+## How the SQL works
+
+Each game gets its own `game_id`. Three tables are used:
+
+```sql
+chess_game -- one row per game (id, players, status, winner)
+chess_piece -- one row per piece (position updated on every move)
+chess_move -- one row per move (full audit log)
+```
+
+Example — white pawn e2 → e4:
+
+```sql
+INSERT INTO chess_move (game_id, move_number, color, piece_type, from_square, to_square, san)
+VALUES ('a1b2-c3d4-e5f6-g7h8', 1, 'white', 'pawn', 'e2', 'e4', 'e4');
+
+UPDATE chess_piece
+SET position = 'e4'
+WHERE game_id = 'a1b2-c3d4-e5f6-g7h8'
+ AND position = 'e2'
+ AND color = 'white';
+```
+
+## CI/CD
+
+The repository uses **GitHub Actions** (`.github/workflows/deploy.yml`):
+
+1. **Validate** — runs `html-validate` on every push/PR to `main`
+2. **Deploy** — automatically publishes the site to **GitHub Pages** on every push to `main`
+
+## Local development
+
+No build step required — it's a plain HTML/CSS/JS site.
+
+```bash
+# Clone and open
+git clone https://github.com/Devn913/SQL_Chess.git
+cd SQL_Chess
+# Open index.html in your browser, or serve with any static server:
+npx serve .
+```
+
+## License
+
+[MIT](LICENSE)
diff --git a/css/style.css b/css/style.css
new file mode 100644
index 0000000..b21e856
--- /dev/null
+++ b/css/style.css
@@ -0,0 +1,760 @@
+/* ============================================================
+ SQL Chess — Global Styles
+ Dark theme, responsive layout
+ ============================================================ */
+
+/* ── Custom Properties ─────────────────────────────────────── */
+:root {
+ --bg-base: #0d1117;
+ --bg-surface: #161b22;
+ --bg-elevated: #21262d;
+ --bg-border: #30363d;
+
+ --text-primary: #e6edf3;
+ --text-secondary: #8b949e;
+ --text-muted: #484f58;
+
+ --accent-blue: #58a6ff;
+ --accent-green: #3fb950;
+ --accent-orange: #d29922;
+ --accent-red: #f85149;
+ --accent-purple: #bc8cff;
+
+ --board-light: #f0d9b5;
+ --board-dark: #b58863;
+ --board-select: #7fc97f;
+ --board-move: rgba(127, 201, 127, 0.45);
+ --board-check: rgba(248, 81, 73, 0.75);
+ --board-last-from: rgba(155, 199, 232, 0.4);
+ --board-last-to: rgba(155, 199, 232, 0.55);
+
+ --sql-bg: #0d1117;
+ --sql-keyword: #ff7b72;
+ --sql-string: #a5d6ff;
+ --sql-comment: #8b949e;
+ --sql-number: #79c0ff;
+ --sql-function: #d2a8ff;
+
+ --radius: 6px;
+ --radius-lg: 12px;
+
+ --font-mono: 'JetBrains Mono', 'Fira Code', 'Cascadia Code', 'Courier New', monospace;
+ --font-sans: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
+
+ --shadow-sm: 0 1px 4px rgba(0,0,0,.4);
+ --shadow-md: 0 4px 16px rgba(0,0,0,.5);
+ --shadow-lg: 0 8px 32px rgba(0,0,0,.6);
+}
+
+/* ── Reset ─────────────────────────────────────────────────── */
+*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
+html, body { height: 100%; }
+body {
+ font-family: var(--font-sans);
+ background: var(--bg-base);
+ color: var(--text-primary);
+ line-height: 1.5;
+ overflow-x: hidden;
+}
+
+button { cursor: pointer; font-family: inherit; }
+input { font-family: inherit; }
+a { color: var(--accent-blue); }
+
+/* ── App Shell ─────────────────────────────────────────────── */
+#app {
+ display: flex;
+ flex-direction: column;
+ min-height: 100vh;
+}
+
+/* ── Header ────────────────────────────────────────────────── */
+.app-header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 0 1.5rem;
+ height: 56px;
+ background: var(--bg-surface);
+ border-bottom: 1px solid var(--bg-border);
+ position: sticky;
+ top: 0;
+ z-index: 100;
+ flex-shrink: 0;
+}
+
+.header-logo {
+ display: flex;
+ align-items: center;
+ gap: .5rem;
+ font-size: 1.2rem;
+ font-weight: 700;
+ user-select: none;
+}
+.logo-icon { font-size: 1.5rem; }
+.logo-accent { color: var(--accent-blue); }
+
+.header-controls {
+ display: flex;
+ align-items: center;
+ gap: .5rem;
+}
+
+/* ── Buttons ────────────────────────────────────────────────── */
+.btn {
+ display: inline-flex;
+ align-items: center;
+ gap: .3rem;
+ padding: .4rem .85rem;
+ border-radius: var(--radius);
+ border: 1px solid var(--bg-border);
+ background: var(--bg-elevated);
+ color: var(--text-primary);
+ font-size: .875rem;
+ font-weight: 500;
+ transition: background .15s, border-color .15s, color .15s, box-shadow .15s;
+ white-space: nowrap;
+}
+.btn:hover { background: #2d333b; border-color: #8b949e; }
+.btn:active { transform: translateY(1px); }
+
+.btn-primary {
+ background: var(--accent-blue);
+ border-color: var(--accent-blue);
+ color: #0d1117;
+}
+.btn-primary:hover { background: #79bcff; border-color: #79bcff; }
+
+.btn-secondary {
+ background: var(--bg-elevated);
+ border-color: var(--accent-purple);
+ color: var(--accent-purple);
+}
+.btn-secondary:hover { background: #2d2152; }
+
+.btn-outline {
+ background: transparent;
+ border-color: var(--bg-border);
+ color: var(--text-secondary);
+}
+.btn-outline:hover { background: var(--bg-elevated); color: var(--text-primary); }
+
+.btn-sm { padding: .3rem .65rem; font-size: .8rem; }
+.btn-xs { padding: .2rem .5rem; font-size: .75rem; }
+.btn-wide { width: 100%; justify-content: center; }
+
+/* ── Main Layout ────────────────────────────────────────────── */
+.app-main {
+ display: flex;
+ flex: 1;
+ gap: 0;
+ overflow: hidden;
+}
+
+/* ── Chess Panel ────────────────────────────────────────────── */
+.chess-panel {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ padding: 1rem;
+ gap: .6rem;
+ flex-shrink: 0;
+ min-width: 0;
+ overflow-y: auto;
+}
+
+/* Player bars */
+.player-bar {
+ display: flex;
+ align-items: center;
+ gap: .6rem;
+ width: 100%;
+ max-width: 480px;
+ padding: .4rem .6rem;
+ background: var(--bg-surface);
+ border: 1px solid var(--bg-border);
+ border-radius: var(--radius);
+}
+
+.player-avatar {
+ font-size: 1.4rem;
+ width: 2rem;
+ text-align: center;
+ flex-shrink: 0;
+}
+.white-avatar { filter: drop-shadow(0 0 3px rgba(255,255,255,.4)); }
+.black-avatar { filter: drop-shadow(0 0 3px rgba(0,0,0,.6)); }
+
+.player-details {
+ display: flex;
+ flex-direction: column;
+ flex: 1;
+ min-width: 0;
+}
+.player-name {
+ font-weight: 600;
+ font-size: .9rem;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+.player-badge {
+ font-size: .75rem;
+ color: var(--accent-green);
+ font-weight: 500;
+}
+
+.captured-strip {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 1px;
+ font-size: .95rem;
+ min-height: 1.2rem;
+ flex: 1;
+ justify-content: flex-end;
+}
+
+/* ── Board ──────────────────────────────────────────────────── */
+.board-wrapper {
+ position: relative;
+ display: grid;
+ grid-template-columns: 1.2rem auto;
+ grid-template-rows: auto 1.2rem;
+ gap: 2px;
+}
+
+.rank-labels {
+ grid-column: 1;
+ grid-row: 1;
+ display: flex;
+ flex-direction: column;
+ justify-content: space-around;
+}
+.rank-label {
+ font-size: .7rem;
+ color: var(--text-secondary);
+ text-align: center;
+ line-height: 1;
+}
+
+.file-labels {
+ grid-column: 2;
+ grid-row: 2;
+ display: flex;
+ justify-content: space-around;
+}
+.file-label {
+ font-size: .7rem;
+ color: var(--text-secondary);
+ text-align: center;
+ flex: 1;
+ line-height: 1;
+}
+
+.board-grid {
+ grid-column: 2;
+ grid-row: 1;
+ display: grid;
+ grid-template-columns: repeat(8, 1fr);
+ grid-template-rows: repeat(8, 1fr);
+ border: 2px solid #555;
+ border-radius: 3px;
+ overflow: hidden;
+ box-shadow: var(--shadow-lg);
+ /* Size is set dynamically in JS but default here */
+ width: min(57vw, 460px);
+ height: min(57vw, 460px);
+}
+
+/* Individual squares */
+.sq {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ position: relative;
+ cursor: pointer;
+ user-select: none;
+ transition: background .1s;
+}
+.sq.light { background: var(--board-light); }
+.sq.dark { background: var(--board-dark); }
+
+.sq:hover { filter: brightness(1.12); }
+
+/* Highlights */
+.sq.selected { background: var(--board-select) !important; }
+.sq.valid-move::after {
+ content: '';
+ position: absolute;
+ width: 32%;
+ height: 32%;
+ border-radius: 50%;
+ background: rgba(0,0,0,.2);
+ pointer-events: none;
+}
+.sq.valid-capture::after {
+ content: '';
+ position: absolute;
+ inset: 0;
+ border-radius: 50%;
+ border: 4px solid rgba(0,0,0,.2);
+ pointer-events: none;
+ width: auto;
+ height: auto;
+}
+.sq.last-from { background: var(--board-last-from) !important; }
+.sq.last-to { background: var(--board-last-to) !important; }
+.sq.in-check { background: var(--board-check) !important; }
+
+/* Pieces */
+.piece {
+ font-size: calc(min(57vw, 460px) / 10);
+ line-height: 1;
+ pointer-events: none;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ text-shadow: 1px 1px 2px rgba(0,0,0,.5);
+ transition: transform .1s;
+ position: relative;
+ z-index: 1;
+}
+.sq:hover .piece { transform: scale(1.05); }
+
+/* ── Game Controls ──────────────────────────────────────────── */
+.game-controls {
+ display: flex;
+ align-items: center;
+ gap: .5rem;
+ width: 100%;
+ max-width: 480px;
+}
+
+.game-status-text {
+ margin-left: auto;
+ font-size: .85rem;
+ color: var(--text-secondary);
+ font-weight: 500;
+}
+
+/* ── Move History ───────────────────────────────────────────── */
+.move-history-box {
+ width: 100%;
+ max-width: 480px;
+ background: var(--bg-surface);
+ border: 1px solid var(--bg-border);
+ border-radius: var(--radius);
+ overflow: hidden;
+ flex-shrink: 0;
+}
+.move-history-header {
+ padding: .4rem .8rem;
+ font-size: .8rem;
+ font-weight: 600;
+ text-transform: uppercase;
+ letter-spacing: .05em;
+ color: var(--text-secondary);
+ border-bottom: 1px solid var(--bg-border);
+ background: var(--bg-elevated);
+}
+.moves-list {
+ display: flex;
+ flex-wrap: wrap;
+ gap: .25rem;
+ padding: .5rem .6rem;
+ min-height: 2.5rem;
+ max-height: 120px;
+ overflow-y: auto;
+ font-family: var(--font-mono);
+ font-size: .8rem;
+}
+.move-pair {
+ display: flex;
+ align-items: center;
+ gap: .2rem;
+ color: var(--text-secondary);
+}
+.move-num { color: var(--text-muted); margin-right: .1rem; }
+.move-san {
+ color: var(--text-primary);
+ padding: .1rem .3rem;
+ border-radius: 3px;
+ cursor: pointer;
+ transition: background .1s;
+}
+.move-san:hover { background: var(--bg-elevated); }
+.move-san.active { background: var(--accent-blue); color: #0d1117; }
+
+/* ── SQL Panel ──────────────────────────────────────────────── */
+.sql-panel {
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ border-left: 1px solid var(--bg-border);
+ background: var(--sql-bg);
+ overflow: hidden;
+ min-width: 0;
+ transition: width .25s ease, flex .25s ease, opacity .2s;
+}
+.sql-panel.hidden-panel {
+ flex: 0;
+ width: 0;
+ opacity: 0;
+ pointer-events: none;
+ border-left: none;
+}
+
+.sql-panel-header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: .6rem 1rem;
+ border-bottom: 1px solid var(--bg-border);
+ background: var(--bg-surface);
+ flex-shrink: 0;
+}
+.sql-title {
+ font-size: .95rem;
+ font-weight: 700;
+ display: flex;
+ align-items: center;
+ gap: .4rem;
+ color: var(--accent-blue);
+}
+.sql-icon { font-size: .9rem; }
+.sql-actions {
+ display: flex;
+ align-items: center;
+ gap: .5rem;
+}
+.autoscroll-label {
+ display: flex;
+ align-items: center;
+ gap: .3rem;
+ font-size: .75rem;
+ color: var(--text-secondary);
+ cursor: pointer;
+ user-select: none;
+}
+
+/* SQL content area */
+.sql-content {
+ flex: 1;
+ overflow-y: auto;
+ padding: .75rem 1rem;
+ display: flex;
+ flex-direction: column;
+ gap: .75rem;
+ scroll-behavior: smooth;
+}
+
+.sql-placeholder {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ gap: .75rem;
+ flex: 1;
+ color: var(--text-muted);
+ text-align: center;
+ padding: 2rem;
+}
+.placeholder-icon { font-size: 2.5rem; opacity: .3; }
+.sql-placeholder p { font-size: .9rem; }
+.placeholder-hint { font-size: .8rem; line-height: 1.6; color: var(--text-muted); opacity: .7; }
+
+/* SQL blocks */
+.sql-block {
+ background: var(--bg-elevated);
+ border: 1px solid var(--bg-border);
+ border-radius: var(--radius);
+ overflow: hidden;
+ animation: fadeSlideIn .2s ease;
+}
+@keyframes fadeSlideIn {
+ from { opacity: 0; transform: translateY(6px); }
+ to { opacity: 1; transform: translateY(0); }
+}
+
+.sql-block-label {
+ display: flex;
+ align-items: center;
+ gap: .4rem;
+ padding: .35rem .75rem;
+ background: var(--bg-surface);
+ border-bottom: 1px solid var(--bg-border);
+ font-size: .7rem;
+ font-weight: 600;
+ text-transform: uppercase;
+ letter-spacing: .05em;
+ color: var(--text-secondary);
+}
+.sql-block-badge {
+ background: var(--accent-blue);
+ color: #0d1117;
+ padding: .05rem .35rem;
+ border-radius: 99px;
+ font-size: .65rem;
+}
+
+.sql-code {
+ padding: .75rem;
+ font-family: var(--font-mono);
+ font-size: .78rem;
+ line-height: 1.65;
+ white-space: pre;
+ overflow-x: auto;
+ color: var(--text-primary);
+}
+/* SQL syntax colouring */
+.sql-kw { color: var(--sql-keyword); font-weight: 700; }
+.sql-str { color: var(--sql-string); }
+.sql-cmt { color: var(--sql-comment); font-style: italic; }
+.sql-num { color: var(--sql-number); }
+.sql-fn { color: var(--sql-function); }
+
+/* ── Modals ─────────────────────────────────────────────────── */
+.modal-overlay {
+ position: fixed;
+ inset: 0;
+ background: rgba(0,0,0,.7);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ z-index: 1000;
+ backdrop-filter: blur(4px);
+ padding: 1rem;
+}
+.modal-overlay.hidden { display: none; }
+
+.modal {
+ background: var(--bg-surface);
+ border: 1px solid var(--bg-border);
+ border-radius: var(--radius-lg);
+ width: 100%;
+ max-width: 440px;
+ box-shadow: var(--shadow-lg);
+ animation: modalIn .2s ease;
+}
+@keyframes modalIn {
+ from { opacity: 0; transform: scale(.96) translateY(10px); }
+ to { opacity: 1; transform: scale(1) translateY(0); }
+}
+.modal-sm { max-width: 320px; }
+
+.modal-header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 1rem 1.25rem .75rem;
+ border-bottom: 1px solid var(--bg-border);
+}
+.modal-header h2 { font-size: 1rem; font-weight: 700; }
+
+.modal-close-btn {
+ background: none;
+ border: none;
+ color: var(--text-secondary);
+ font-size: 1.1rem;
+ line-height: 1;
+ padding: .2rem .4rem;
+ border-radius: var(--radius);
+ transition: background .15s, color .15s;
+}
+.modal-close-btn:hover { background: var(--bg-elevated); color: var(--text-primary); }
+
+.modal-body {
+ padding: 1rem 1.25rem;
+ display: flex;
+ flex-direction: column;
+ gap: .85rem;
+}
+.modal-footer {
+ display: flex;
+ gap: .6rem;
+ padding: .75rem 1.25rem 1rem;
+ flex-direction: column;
+}
+
+.form-row {
+ display: flex;
+ flex-direction: column;
+ gap: .3rem;
+}
+.form-row label {
+ font-size: .82rem;
+ font-weight: 600;
+ color: var(--text-secondary);
+ text-transform: uppercase;
+ letter-spacing: .04em;
+}
+.form-row input[type="text"] {
+ background: var(--bg-elevated);
+ border: 1px solid var(--bg-border);
+ border-radius: var(--radius);
+ color: var(--text-primary);
+ padding: .5rem .7rem;
+ font-size: .9rem;
+ outline: none;
+ transition: border-color .15s;
+}
+.form-row input[type="text"]:focus { border-color: var(--accent-blue); }
+
+.toggle-row {
+ flex-direction: row;
+ align-items: center;
+ justify-content: space-between;
+}
+
+/* Toggle switch */
+.switch { position: relative; display: inline-block; width: 40px; height: 22px; }
+.switch input { opacity: 0; width: 0; height: 0; }
+.slider {
+ position: absolute;
+ inset: 0;
+ background: var(--bg-border);
+ border-radius: 99px;
+ transition: background .2s;
+ cursor: pointer;
+}
+.slider::before {
+ content: '';
+ position: absolute;
+ height: 16px;
+ width: 16px;
+ left: 3px;
+ bottom: 3px;
+ background: white;
+ border-radius: 50%;
+ transition: transform .2s;
+}
+input:checked + .slider { background: var(--accent-blue); }
+input:checked + .slider::before { transform: translateX(18px); }
+
+/* Invite modal */
+.invite-info { font-size: .875rem; color: var(--text-secondary); }
+.invite-url-row {
+ display: flex;
+ gap: .5rem;
+}
+.invite-url-row input[type="text"] {
+ flex: 1;
+ background: var(--bg-elevated);
+ border: 1px solid var(--bg-border);
+ border-radius: var(--radius);
+ color: var(--text-primary);
+ padding: .45rem .65rem;
+ font-family: var(--font-mono);
+ font-size: .78rem;
+ outline: none;
+ cursor: text;
+ min-width: 0;
+}
+.invite-meta {
+ display: flex;
+ gap: 1rem;
+ font-size: .82rem;
+ color: var(--text-secondary);
+}
+.invite-meta strong { color: var(--text-primary); }
+
+.copy-feedback {
+ color: var(--accent-green);
+ font-size: .82rem;
+ font-weight: 600;
+}
+
+/* Promotion */
+.promotion-choices {
+ display: flex;
+ justify-content: center;
+ gap: .5rem;
+ padding: .5rem 0;
+}
+.promotion-btn {
+ width: 64px;
+ height: 64px;
+ font-size: 2.2rem;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ background: var(--bg-elevated);
+ border: 2px solid var(--bg-border);
+ border-radius: var(--radius);
+ cursor: pointer;
+ color: var(--text-primary);
+ transition: border-color .15s, background .15s;
+}
+.promotion-btn:hover { border-color: var(--accent-blue); background: #1c2431; }
+
+/* ── Toast ──────────────────────────────────────────────────── */
+.toast {
+ position: fixed;
+ bottom: 1.5rem;
+ left: 50%;
+ transform: translateX(-50%);
+ background: var(--bg-elevated);
+ border: 1px solid var(--bg-border);
+ color: var(--text-primary);
+ padding: .6rem 1.25rem;
+ border-radius: 99px;
+ font-size: .875rem;
+ font-weight: 500;
+ box-shadow: var(--shadow-md);
+ z-index: 9999;
+ white-space: nowrap;
+ animation: toastIn .2s ease;
+}
+.toast.hidden { display: none; }
+@keyframes toastIn {
+ from { opacity: 0; transform: translateX(-50%) translateY(10px); }
+ to { opacity: 1; transform: translateX(-50%) translateY(0); }
+}
+
+/* ── Scrollbar ──────────────────────────────────────────────── */
+::-webkit-scrollbar { width: 6px; height: 6px; }
+::-webkit-scrollbar-track { background: transparent; }
+::-webkit-scrollbar-thumb { background: var(--bg-border); border-radius: 3px; }
+::-webkit-scrollbar-thumb:hover { background: #555; }
+
+/* ── Utility ────────────────────────────────────────────────── */
+.hidden { display: none !important; }
+
+/* ── Responsive ─────────────────────────────────────────────── */
+@media (max-width: 900px) {
+ .app-main { flex-direction: column; }
+
+ .sql-panel {
+ border-left: none;
+ border-top: 1px solid var(--bg-border);
+ max-height: 40vh;
+ }
+ .sql-panel.hidden-panel {
+ max-height: 0;
+ border-top: none;
+ }
+
+ .board-grid {
+ width: min(90vw, 420px);
+ height: min(90vw, 420px);
+ }
+
+ .piece {
+ font-size: calc(min(90vw, 420px) / 10);
+ }
+}
+
+@media (max-width: 480px) {
+ .app-header { padding: 0 .75rem; }
+ .header-logo .logo-text { display: none; }
+ .chess-panel { padding: .5rem; }
+ .header-controls { gap: .3rem; }
+
+ .board-grid {
+ width: min(92vw, 360px);
+ height: min(92vw, 360px);
+ }
+ .piece { font-size: calc(min(92vw, 360px) / 10); }
+
+ #btnToggleSQL #sqlToggleLabel { display: none; }
+}
diff --git a/index.html b/index.html
new file mode 100644
index 0000000..b0f057f
--- /dev/null
+++ b/index.html
@@ -0,0 +1,178 @@
+
+
+
+
+
+ SQL Chess — Play Chess with SQL
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
♚
+
+ Guest Black
+
+
+
+
+
+
+
+
+
+
+
♔
+
+ Guest White
+ Your turn
+
+
+
+
+
+
+
+
+ White to move
+
+
+
+
+
+
+
+
+
+
+
+
+
⬡
+
SQL queries will appear here as you play.
+
Each chess move is translated into
real SQL statements in real time.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Share this link to let someone join and continue this game:
+
+
+
+
+
✓ Copied to clipboard!
+
+ Current turn: White
+ Moves played: 0
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/js/app.js b/js/app.js
new file mode 100644
index 0000000..00d8d61
--- /dev/null
+++ b/js/app.js
@@ -0,0 +1,904 @@
+/**
+ * SQL Chess — Main Application
+ * A chess game where every move is translated into SQL queries in real time.
+ *
+ * Features:
+ * - Fully playable chess (chess.js engine)
+ * - SQL query panel showing CREATE/INSERT/UPDATE/DELETE for every move
+ * - Toggle SQL panel on/off
+ * - Play as Guest (no login required)
+ * - Invite system: generates a shareable URL with full game state
+ * - Pawn promotion dialog
+ * - Undo, flip board
+ * - Load game from invite URL on page load
+ */
+
+'use strict';
+
+/* ─── Configuration ──────────────────────────────────────────── */
+const PIECE_UNICODE = {
+ wK: '♔', wQ: '♕', wR: '♖', wB: '♗', wN: '♘', wP: '♙',
+ bK: '♚', bQ: '♛', bR: '♜', bB: '♝', bN: '♞', bP: '♟',
+};
+
+const PIECE_NAMES = { k: 'king', q: 'queen', r: 'rook', b: 'bishop', n: 'knight', p: 'pawn' };
+
+const FILES = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'];
+const RANKS = ['8', '7', '6', '5', '4', '3', '2', '1']; // top→bottom visual
+
+/* ─── State ───────────────────────────────────────────────────── */
+const state = {
+ chess: null,
+ gameId: null,
+ selectedSquare: null,
+ validMoves: [],
+ flipped: false,
+ showSQL: true,
+ sqlBlocks: [],
+ moveCount: 0,
+ whitePlayer: 'Guest White',
+ blackPlayer: 'Guest Black',
+ lastMove: null,
+ pendingPromotion: null, // { from, to } while waiting for user to pick
+ capturedByWhite: [], // pieces taken by white (black pieces lost)
+ capturedByBlack: [], // pieces taken by black (white pieces lost)
+};
+
+/* ─── Helpers ─────────────────────────────────────────────────── */
+function generateId() {
+ return Array.from({ length: 4 }, () =>
+ Math.floor(Math.random() * 0x10000).toString(16).padStart(4, '0')
+ ).join('-');
+}
+
+function pieceName(type) { return PIECE_NAMES[type] || type; }
+
+function pieceSymbol(color, type) {
+ return PIECE_UNICODE[(color === 'w' ? 'w' : 'b') + type.toUpperCase()] || '';
+}
+
+function capitalize(s) { return s.charAt(0).toUpperCase() + s.slice(1); }
+
+function showToast(msg, duration = 2500) {
+ const el = document.getElementById('toast');
+ el.textContent = msg;
+ el.classList.remove('hidden');
+ clearTimeout(el._timer);
+ el._timer = setTimeout(() => el.classList.add('hidden'), duration);
+}
+
+/* ─── Board Rendering ─────────────────────────────────────────── */
+function buildBoard() {
+ const board = document.getElementById('board');
+ board.innerHTML = '';
+ const rankLabels = document.getElementById('rankLabels');
+ const fileLabels = document.getElementById('fileLabels');
+ rankLabels.innerHTML = '';
+ fileLabels.innerHTML = '';
+
+ const ranks = state.flipped ? ['1','2','3','4','5','6','7','8'] : ['8','7','6','5','4','3','2','1'];
+ const files = state.flipped ? ['h','g','f','e','d','c','b','a'] : ['a','b','c','d','e','f','g','h'];
+
+ ranks.forEach(rank => {
+ const lbl = document.createElement('div');
+ lbl.className = 'rank-label';
+ lbl.textContent = rank;
+ rankLabels.appendChild(lbl);
+ });
+ files.forEach(file => {
+ const lbl = document.createElement('div');
+ lbl.className = 'file-label';
+ lbl.textContent = file;
+ fileLabels.appendChild(lbl);
+ });
+
+ ranks.forEach(rank => {
+ files.forEach(file => {
+ const sq = document.createElement('div');
+ const sqName = file + rank;
+ const isDark = (file.charCodeAt(0) + parseInt(rank)) % 2 === 0;
+ sq.className = 'sq ' + (isDark ? 'dark' : 'light');
+ sq.dataset.square = sqName;
+ sq.addEventListener('click', () => onSquareClick(sqName));
+ board.appendChild(sq);
+ });
+ });
+}
+
+function renderPieces() {
+ const boardState = state.chess.board();
+ document.querySelectorAll('.sq').forEach(sqEl => {
+ sqEl.innerHTML = '';
+ sqEl.classList.remove('selected', 'valid-move', 'valid-capture', 'last-from', 'last-to', 'in-check');
+
+ const sqName = sqEl.dataset.square;
+ const file = sqName.charCodeAt(0) - 97;
+ const rank = parseInt(sqName[1]) - 1;
+ const piece = boardState[7 - rank][file];
+
+ if (piece) {
+ const span = document.createElement('span');
+ span.className = 'piece';
+ span.textContent = pieceSymbol(piece.color, piece.type);
+ sqEl.appendChild(span);
+ }
+
+ // Highlight last move
+ if (state.lastMove) {
+ if (sqName === state.lastMove.from) sqEl.classList.add('last-from');
+ if (sqName === state.lastMove.to) sqEl.classList.add('last-to');
+ }
+ });
+
+ // Highlight selected + valid moves
+ if (state.selectedSquare) {
+ const sel = document.querySelector(`.sq[data-square="${state.selectedSquare}"]`);
+ if (sel) sel.classList.add('selected');
+ state.validMoves.forEach(mv => {
+ const target = document.querySelector(`.sq[data-square="${mv.to}"]`);
+ if (target) {
+ target.classList.add(mv.captured ? 'valid-capture' : 'valid-move');
+ }
+ });
+ }
+
+ // Highlight check
+ if (state.chess.in_check()) {
+ const turn = state.chess.turn();
+ const kingType = 'k';
+ state.chess.board().forEach((row, ri) => {
+ row.forEach((cell, fi) => {
+ if (cell && cell.color === turn && cell.type === kingType) {
+ const sqName = FILES[fi] + (8 - ri);
+ const sqEl = document.querySelector(`.sq[data-square="${sqName}"]`);
+ if (sqEl) sqEl.classList.add('in-check');
+ }
+ });
+ });
+ }
+}
+
+function updateCapturedStrips() {
+ const fmt = (arr) => arr.map(p => pieceSymbol(p.color, p.type)).join('');
+ document.getElementById('whiteCapturedStrip').textContent = fmt(state.capturedByWhite);
+ document.getElementById('blackCapturedStrip').textContent = fmt(state.capturedByBlack);
+}
+
+function updatePlayerBars() {
+ const turn = state.chess.turn();
+ document.getElementById('whiteBadge').textContent = turn === 'w' ? 'Your turn' : '';
+ document.getElementById('blackBadge').textContent = turn === 'b' ? 'Your turn' : '';
+ document.getElementById('whiteBar').style.opacity = turn === 'w' ? '1' : '.65';
+ document.getElementById('blackBar').style.opacity = turn === 'b' ? '1' : '.65';
+}
+
+function updateStatus() {
+ const el = document.getElementById('gameStatusText');
+ const chess = state.chess;
+ if (chess.in_checkmate()) {
+ const winner = chess.turn() === 'w' ? state.blackPlayer : state.whitePlayer;
+ el.textContent = `♛ Checkmate — ${winner} wins!`;
+ el.style.color = 'var(--accent-red)';
+ } else if (chess.in_stalemate()) {
+ el.textContent = '½-½ Stalemate';
+ el.style.color = 'var(--accent-orange)';
+ } else if (chess.in_draw()) {
+ el.textContent = '½-½ Draw';
+ el.style.color = 'var(--accent-orange)';
+ } else if (chess.in_check()) {
+ const who = chess.turn() === 'w' ? state.whitePlayer : state.blackPlayer;
+ el.textContent = `⚠ Check — ${who}`;
+ el.style.color = 'var(--accent-red)';
+ } else {
+ const who = chess.turn() === 'w' ? state.whitePlayer : state.blackPlayer;
+ el.textContent = `${who} to move`;
+ el.style.color = 'var(--text-secondary)';
+ }
+}
+
+function buildMoveHistory() {
+ const list = document.getElementById('movesList');
+ const pgn = state.chess.pgn({ max_width: 80, newline_char: ' ' });
+ const moves = state.chess.history();
+ list.innerHTML = '';
+ for (let i = 0; i < moves.length; i += 2) {
+ const num = Math.floor(i / 2) + 1;
+ const pair = document.createElement('div');
+ pair.className = 'move-pair';
+ pair.innerHTML =
+ `${num}.` +
+ `${moves[i]}` +
+ (moves[i + 1] ? `${moves[i + 1]}` : '');
+ list.appendChild(pair);
+ }
+ list.scrollTop = list.scrollHeight;
+}
+
+function renderAll() {
+ renderPieces();
+ updateCapturedStrips();
+ updatePlayerBars();
+ updateStatus();
+ buildMoveHistory();
+}
+
+/* ─── Square Click Logic ──────────────────────────────────────── */
+function onSquareClick(sqName) {
+ // Ignore clicks if game over or awaiting promotion
+ if (state.chess.game_over()) return;
+ if (state.pendingPromotion) return;
+
+ const piece = state.chess.get(sqName);
+
+ // If no square selected yet
+ if (!state.selectedSquare) {
+ if (!piece) return;
+ if (piece.color !== state.chess.turn()) return; // not your piece
+ state.selectedSquare = sqName;
+ state.validMoves = state.chess.moves({ square: sqName, verbose: true });
+ renderPieces();
+ return;
+ }
+
+ // Already have a selected square
+ if (sqName === state.selectedSquare) {
+ // Deselect
+ state.selectedSquare = null;
+ state.validMoves = [];
+ renderPieces();
+ return;
+ }
+
+ // Re-select if clicking own piece
+ if (piece && piece.color === state.chess.turn()) {
+ state.selectedSquare = sqName;
+ state.validMoves = state.chess.moves({ square: sqName, verbose: true });
+ renderPieces();
+ return;
+ }
+
+ // Attempt a move
+ const moveObj = state.validMoves.find(m => m.to === sqName);
+ if (!moveObj) {
+ // Invalid target — deselect
+ state.selectedSquare = null;
+ state.validMoves = [];
+ renderPieces();
+ return;
+ }
+
+ // Pawn promotion?
+ if (moveObj.flags.includes('p')) {
+ state.pendingPromotion = { from: state.selectedSquare, to: sqName };
+ state.selectedSquare = null;
+ state.validMoves = [];
+ openPromotionDialog(state.chess.turn());
+ return;
+ }
+
+ executeMove(state.selectedSquare, sqName, null);
+}
+
+function executeMove(from, to, promotion) {
+ const moveResult = state.chess.move({ from, to, promotion: promotion || 'q' });
+ if (!moveResult) return;
+
+ // Track captures
+ if (moveResult.captured) {
+ const capturedPiece = { color: moveResult.color === 'w' ? 'b' : 'w', type: moveResult.captured };
+ if (moveResult.color === 'w') {
+ state.capturedByWhite.push(capturedPiece);
+ } else {
+ state.capturedByBlack.push(capturedPiece);
+ }
+ }
+
+ state.lastMove = { from, to };
+ state.selectedSquare = null;
+ state.validMoves = [];
+ state.moveCount++;
+
+ // Generate SQL
+ if (state.showSQL) {
+ const sql = SQLGen.move(moveResult, state.moveCount, state.gameId);
+ appendSQL(sql, `Move ${Math.ceil(state.moveCount / 2)} — ${capitalize(pieceName(moveResult.piece))} ${from}→${to}`, state.moveCount);
+
+ // Game-end SQL
+ if (state.chess.in_checkmate() || state.chess.in_stalemate() || state.chess.in_draw()) {
+ const endSQL = SQLGen.gameEnd(state.chess, state.gameId);
+ appendSQL(endSQL, 'Game Over', null);
+ } else if (state.chess.in_check()) {
+ const checkSQL = SQLGen.check(state.chess.turn(), state.gameId);
+ appendSQL(checkSQL, '⚠ Check', null);
+ }
+ }
+
+ renderAll();
+
+ // Show game-end toast
+ if (state.chess.in_checkmate()) {
+ const winner = state.chess.turn() === 'w' ? state.blackPlayer : state.whitePlayer;
+ showToast(`♛ Checkmate! ${winner} wins!`, 5000);
+ } else if (state.chess.in_stalemate()) {
+ showToast('½-½ Stalemate!', 4000);
+ } else if (state.chess.in_draw()) {
+ showToast('½-½ Draw!', 4000);
+ }
+}
+
+/* ─── Promotion Dialog ────────────────────────────────────────── */
+function openPromotionDialog(turn) {
+ const overlay = document.getElementById('promotionOverlay');
+ const choices = document.getElementById('promotionChoices');
+ choices.innerHTML = '';
+ const pieces = ['q', 'r', 'b', 'n'];
+ const names = { q: 'Queen', r: 'Rook', b: 'Bishop', n: 'Knight' };
+ pieces.forEach(p => {
+ const btn = document.createElement('button');
+ btn.className = 'promotion-btn';
+ btn.title = names[p];
+ btn.textContent = pieceSymbol(turn, p);
+ btn.addEventListener('click', () => {
+ overlay.classList.add('hidden');
+ const prom = state.pendingPromotion;
+ state.pendingPromotion = null;
+ executeMove(prom.from, prom.to, p);
+ });
+ choices.appendChild(btn);
+ });
+ overlay.classList.remove('hidden');
+}
+
+/* ─── SQL Generation ──────────────────────────────────────────── */
+const SQLGen = {
+ gameStart(gameId, whiteName, blackName) {
+ return [
+ `-- ══════════════════════════════════════════`,
+ `-- SQL Chess · New Game`,
+ `-- Game ID : ${gameId}`,
+ `-- ══════════════════════════════════════════`,
+ ``,
+ `-- Schema (run once)`,
+ `CREATE TABLE IF NOT EXISTS chess_game (`,
+ ` id CHAR(36) NOT NULL,`,
+ ` white_player VARCHAR(100) NOT NULL,`,
+ ` black_player VARCHAR(100) NOT NULL,`,
+ ` status VARCHAR(20) NOT NULL DEFAULT 'active',`,
+ ` winner VARCHAR(10) DEFAULT NULL,`,
+ ` created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,`,
+ ` PRIMARY KEY (id)`,
+ `);`,
+ ``,
+ `CREATE TABLE IF NOT EXISTS chess_piece (`,
+ ` piece_id VARCHAR(15) NOT NULL,`,
+ ` game_id CHAR(36) NOT NULL,`,
+ ` color VARCHAR(5) NOT NULL,`,
+ ` type VARCHAR(10) NOT NULL,`,
+ ` position CHAR(2) DEFAULT NULL,`,
+ ` is_captured BOOLEAN NOT NULL DEFAULT FALSE,`,
+ ` PRIMARY KEY (piece_id, game_id),`,
+ ` FOREIGN KEY (game_id) REFERENCES chess_game(id)`,
+ `);`,
+ ``,
+ `CREATE TABLE IF NOT EXISTS chess_move (`,
+ ` id INT NOT NULL AUTO_INCREMENT,`,
+ ` game_id CHAR(36) NOT NULL,`,
+ ` move_number INT NOT NULL,`,
+ ` color VARCHAR(5) NOT NULL,`,
+ ` piece_type VARCHAR(10) NOT NULL,`,
+ ` from_square CHAR(2) NOT NULL,`,
+ ` to_square CHAR(2) NOT NULL,`,
+ ` captured VARCHAR(10) DEFAULT NULL,`,
+ ` special VARCHAR(20) DEFAULT NULL,`,
+ ` san VARCHAR(10) NOT NULL,`,
+ ` created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,`,
+ ` PRIMARY KEY (id),`,
+ ` FOREIGN KEY (game_id) REFERENCES chess_game(id)`,
+ `);`,
+ ``,
+ `-- Start new game`,
+ `INSERT INTO chess_game (id, white_player, black_player)`,
+ `VALUES ('${gameId}', '${whiteName}', '${blackName}');`,
+ ``,
+ `-- Set up all 32 pieces`,
+ `INSERT INTO chess_piece (piece_id, game_id, color, type, position) VALUES`,
+ ` ('wR_a1','${gameId}','white','rook', 'a1'),`,
+ ` ('wN_b1','${gameId}','white','knight','b1'),`,
+ ` ('wB_c1','${gameId}','white','bishop','c1'),`,
+ ` ('wQ_d1','${gameId}','white','queen', 'd1'),`,
+ ` ('wK_e1','${gameId}','white','king', 'e1'),`,
+ ` ('wB_f1','${gameId}','white','bishop','f1'),`,
+ ` ('wN_g1','${gameId}','white','knight','g1'),`,
+ ` ('wR_h1','${gameId}','white','rook', 'h1'),`,
+ ` ('wP_a2','${gameId}','white','pawn', 'a2'),`,
+ ` ('wP_b2','${gameId}','white','pawn', 'b2'),`,
+ ` ('wP_c2','${gameId}','white','pawn', 'c2'),`,
+ ` ('wP_d2','${gameId}','white','pawn', 'd2'),`,
+ ` ('wP_e2','${gameId}','white','pawn', 'e2'),`,
+ ` ('wP_f2','${gameId}','white','pawn', 'f2'),`,
+ ` ('wP_g2','${gameId}','white','pawn', 'g2'),`,
+ ` ('wP_h2','${gameId}','white','pawn', 'h2'),`,
+ ` ('bR_a8','${gameId}','black','rook', 'a8'),`,
+ ` ('bN_b8','${gameId}','black','knight','b8'),`,
+ ` ('bB_c8','${gameId}','black','bishop','c8'),`,
+ ` ('bQ_d8','${gameId}','black','queen', 'd8'),`,
+ ` ('bK_e8','${gameId}','black','king', 'e8'),`,
+ ` ('bB_f8','${gameId}','black','bishop','f8'),`,
+ ` ('bN_g8','${gameId}','black','knight','g8'),`,
+ ` ('bR_h8','${gameId}','black','rook', 'h8'),`,
+ ` ('bP_a7','${gameId}','black','pawn', 'a7'),`,
+ ` ('bP_b7','${gameId}','black','pawn', 'b7'),`,
+ ` ('bP_c7','${gameId}','black','pawn', 'c7'),`,
+ ` ('bP_d7','${gameId}','black','pawn', 'd7'),`,
+ ` ('bP_e7','${gameId}','black','pawn', 'e7'),`,
+ ` ('bP_f7','${gameId}','black','pawn', 'f7'),`,
+ ` ('bP_g7','${gameId}','black','pawn', 'g7'),`,
+ ` ('bP_h7','${gameId}','black','pawn', 'h7');`,
+ ].join('\n');
+ },
+
+ move(mv, moveNum, gameId) {
+ const color = mv.color === 'w' ? 'white' : 'black';
+ const oppColor = color === 'white' ? 'black' : 'white';
+ const piece = pieceName(mv.piece);
+ const isCapture = !!mv.captured;
+ const isEP = mv.flags.includes('e');
+ const isKCastle = mv.flags.includes('k');
+ const isQCastle = mv.flags.includes('q');
+ const isCastle = isKCastle || isQCastle;
+ const isPromo = mv.flags.includes('p');
+
+ let comment = `-- Move #${moveNum}: ${capitalize(color)} ${piece} ${mv.from} → ${mv.to}`;
+ if (isCapture && !isEP) comment = `-- Move #${moveNum}: ${capitalize(color)} ${piece} ${mv.from} ✕ ${mv.to} (captures ${pieceName(mv.captured)})`;
+ if (isEP) comment = `-- Move #${moveNum}: ${capitalize(color)} pawn ${mv.from} ✕ ${mv.to} (en passant)`;
+ if (isCastle) comment = `-- Move #${moveNum}: ${capitalize(color)} castles ${isKCastle ? 'kingside' : 'queenside'}`;
+ if (isPromo) comment += ` → promotes to ${pieceName(mv.promotion)}`;
+
+ const capturedVal = (isCapture && !isEP) ? `'${pieceName(mv.captured)}'` : isEP ? `'pawn'` : 'NULL';
+ const specialVal = isCastle ? `'${isKCastle ? 'kingside-castle' : 'queenside-castle'}'`
+ : isEP ? `'en-passant'`
+ : isPromo ? `'promotion-${pieceName(mv.promotion)}'`
+ : 'NULL';
+
+ const lines = [
+ comment,
+ `INSERT INTO chess_move`,
+ ` (game_id, move_number, color, piece_type, from_square, to_square, captured, special, san)`,
+ `VALUES`,
+ ` ('${gameId}', ${moveNum}, '${color}', '${piece}', '${mv.from}', '${mv.to}', ${capturedVal}, ${specialVal}, '${mv.san}');`,
+ ``,
+ ];
+
+ if (isCapture && !isEP) {
+ lines.push(`-- Remove captured piece`);
+ lines.push(`DELETE FROM chess_piece`);
+ lines.push(`WHERE game_id = '${gameId}'`);
+ lines.push(` AND position = '${mv.to}'`);
+ lines.push(` AND color = '${oppColor}';`);
+ lines.push(``);
+ lines.push(`-- Advance the capturing piece`);
+ lines.push(`UPDATE chess_piece`);
+ lines.push(`SET position = '${mv.to}'`);
+ lines.push(`WHERE game_id = '${gameId}'`);
+ lines.push(` AND position = '${mv.from}'`);
+ lines.push(` AND color = '${color}';`);
+ } else if (isEP) {
+ const capRank = color === 'white' ? parseInt(mv.to[1]) - 1 : parseInt(mv.to[1]) + 1;
+ const capSquare = mv.to[0] + capRank;
+ lines.push(`-- En passant: remove captured pawn`);
+ lines.push(`DELETE FROM chess_piece`);
+ lines.push(`WHERE game_id = '${gameId}'`);
+ lines.push(` AND position = '${capSquare}'`);
+ lines.push(` AND color = '${oppColor}';`);
+ lines.push(``);
+ lines.push(`-- Move capturing pawn`);
+ lines.push(`UPDATE chess_piece`);
+ lines.push(`SET position = '${mv.to}'`);
+ lines.push(`WHERE game_id = '${gameId}'`);
+ lines.push(` AND position = '${mv.from}'`);
+ lines.push(` AND color = '${color}';`);
+ } else if (isCastle) {
+ const rank = color === 'white' ? '1' : '8';
+ const rookFrom = isKCastle ? `h${rank}` : `a${rank}`;
+ const rookTo = isKCastle ? `f${rank}` : `d${rank}`;
+ lines.push(`-- Move king`);
+ lines.push(`UPDATE chess_piece`);
+ lines.push(`SET position = '${mv.to}'`);
+ lines.push(`WHERE game_id = '${gameId}'`);
+ lines.push(` AND position = '${mv.from}'`);
+ lines.push(` AND color = '${color}';`);
+ lines.push(``);
+ lines.push(`-- Move rook (castling)`);
+ lines.push(`UPDATE chess_piece`);
+ lines.push(`SET position = '${rookTo}'`);
+ lines.push(`WHERE game_id = '${gameId}'`);
+ lines.push(` AND position = '${rookFrom}'`);
+ lines.push(` AND color = '${color}';`);
+ } else if (isPromo) {
+ const promName = pieceName(mv.promotion);
+ if (isCapture) {
+ lines.push(`-- Remove captured piece`);
+ lines.push(`DELETE FROM chess_piece`);
+ lines.push(`WHERE game_id = '${gameId}'`);
+ lines.push(` AND position = '${mv.to}'`);
+ lines.push(` AND color = '${oppColor}';`);
+ lines.push(``);
+ }
+ lines.push(`-- Promote pawn to ${promName}`);
+ lines.push(`UPDATE chess_piece`);
+ lines.push(`SET position = '${mv.to}', type = '${promName}'`);
+ lines.push(`WHERE game_id = '${gameId}'`);
+ lines.push(` AND position = '${mv.from}'`);
+ lines.push(` AND color = '${color}';`);
+ } else {
+ lines.push(`UPDATE chess_piece`);
+ lines.push(`SET position = '${mv.to}'`);
+ lines.push(`WHERE game_id = '${gameId}'`);
+ lines.push(` AND position = '${mv.from}'`);
+ lines.push(` AND color = '${color}';`);
+ }
+
+ return lines.join('\n');
+ },
+
+ check(turnAfterMove, gameId) {
+ const inCheck = turnAfterMove === 'w' ? 'white' : 'black';
+ return [
+ `-- ⚠ CHECK! The ${inCheck} king is now in check.`,
+ `UPDATE chess_game`,
+ `SET status = 'check'`,
+ `WHERE id = '${gameId}';`,
+ ].join('\n');
+ },
+
+ gameEnd(chess, gameId) {
+ if (chess.in_checkmate()) {
+ const winner = chess.turn() === 'w' ? 'black' : 'white';
+ return [
+ `-- ♛ CHECKMATE! ${capitalize(winner)} wins!`,
+ `UPDATE chess_game`,
+ `SET status = 'checkmate',`,
+ ` winner = '${winner}'`,
+ `WHERE id = '${gameId}';`,
+ ].join('\n');
+ }
+ if (chess.in_stalemate()) {
+ return [
+ `-- ½-½ STALEMATE — the game is a draw.`,
+ `UPDATE chess_game`,
+ `SET status = 'stalemate'`,
+ `WHERE id = '${gameId}';`,
+ ].join('\n');
+ }
+ return [
+ `-- ½-½ DRAW`,
+ `UPDATE chess_game`,
+ `SET status = 'draw'`,
+ `WHERE id = '${gameId}';`,
+ ].join('\n');
+ },
+};
+
+/* ─── SQL Rendering ───────────────────────────────────────────── */
+function highlightSQL(code) {
+ // Simple keyword highlighting — no external lib needed
+ const keywords = [
+ 'SELECT','FROM','WHERE','INSERT','INTO','VALUES','UPDATE','SET',
+ 'DELETE','CREATE','TABLE','IF','NOT','EXISTS','PRIMARY','KEY',
+ 'FOREIGN','REFERENCES','DEFAULT','NULL','BOOLEAN','AUTO_INCREMENT',
+ 'CHAR','VARCHAR','TIMESTAMP','INT','AND','OR','IN','AS',
+ ];
+ const kwRegex = new RegExp(`\\b(${keywords.join('|')})\\b`, 'g');
+
+ return code
+ .replace(/&/g, '&').replace(//g, '>')
+ // Comments (-- …)
+ .replace(/(--[^\n]*)/g, '$1')
+ // Strings
+ .replace(/'([^']*)'/g, "'$1'")
+ // Numbers (standalone)
+ .replace(/\b(\d+)\b/g, '$1')
+ // Keywords
+ .replace(kwRegex, '$1');
+}
+
+function appendSQL(code, label, moveNum) {
+ const placeholder = document.getElementById('sqlPlaceholder');
+ if (placeholder) placeholder.remove();
+
+ const content = document.getElementById('sqlContent');
+
+ const block = document.createElement('div');
+ block.className = 'sql-block';
+
+ const labelEl = document.createElement('div');
+ labelEl.className = 'sql-block-label';
+ if (moveNum !== null) {
+ const badge = document.createElement('span');
+ badge.className = 'sql-block-badge';
+ badge.textContent = '#' + moveNum;
+ labelEl.appendChild(badge);
+ }
+ labelEl.append(' ' + label);
+
+ const codeEl = document.createElement('div');
+ codeEl.className = 'sql-code';
+ codeEl.innerHTML = highlightSQL(code);
+
+ block.appendChild(labelEl);
+ block.appendChild(codeEl);
+ content.appendChild(block);
+
+ state.sqlBlocks.push({ code, label, moveNum });
+
+ if (document.getElementById('chkAutoScroll').checked) {
+ content.scrollTop = content.scrollHeight;
+ }
+}
+
+/* ─── Game Lifecycle ──────────────────────────────────────────── */
+function startGame(whiteName, blackName, showSQL, existingPGN) {
+ state.chess = new Chess();
+ state.gameId = generateId();
+ state.selectedSquare = null;
+ state.validMoves = [];
+ state.lastMove = null;
+ state.moveCount = 0;
+ state.whitePlayer = whiteName || 'Guest White';
+ state.blackPlayer = blackName || 'Guest Black';
+ state.showSQL = showSQL !== false;
+ state.sqlBlocks = [];
+ state.capturedByWhite = [];
+ state.capturedByBlack = [];
+ state.pendingPromotion = null;
+
+ // Names in UI
+ document.getElementById('whitePlayerName').textContent = state.whitePlayer;
+ document.getElementById('blackPlayerName').textContent = state.blackPlayer;
+
+ // SQL panel visibility
+ const sqlPanel = document.getElementById('sqlPanel');
+ const lblEl = document.getElementById('sqlToggleLabel');
+ const iconEl = document.getElementById('sqlToggleIcon');
+ if (state.showSQL) {
+ sqlPanel.classList.remove('hidden-panel');
+ lblEl.textContent = 'Hide SQL';
+ iconEl.textContent = '◧';
+ } else {
+ sqlPanel.classList.add('hidden-panel');
+ lblEl.textContent = 'Show SQL';
+ iconEl.textContent = '□';
+ }
+
+ // Clear SQL content
+ const sqlContent = document.getElementById('sqlContent');
+ sqlContent.innerHTML = '';
+ if (state.showSQL) {
+ const placeholder = document.createElement('div');
+ placeholder.id = 'sqlPlaceholder';
+ placeholder.className = 'sql-placeholder';
+ placeholder.innerHTML =
+ `⬡` +
+ `SQL queries will appear here as you play.
` +
+ `Each chess move is translated into
real SQL statements in real time.
`;
+ sqlContent.appendChild(placeholder);
+
+ // Emit game-start SQL
+ const initSQL = SQLGen.gameStart(state.gameId, state.whitePlayer, state.blackPlayer);
+ appendSQL(initSQL, 'Game Initialized', null);
+ }
+
+ // Load from PGN if provided (invite link)
+ if (existingPGN && existingPGN.trim()) {
+ try {
+ state.chess.load_pgn(existingPGN);
+ // Replay SQL for every move
+ if (state.showSQL) {
+ const history = state.chess.history({ verbose: true });
+ // Reset and replay
+ const tempChess = new Chess();
+ history.forEach(mv => {
+ tempChess.move(mv);
+ state.moveCount++;
+ const sql = SQLGen.move(mv, state.moveCount, state.gameId);
+ appendSQL(sql, `Move ${Math.ceil(state.moveCount / 2)} — ${capitalize(pieceName(mv.piece))} ${mv.from}→${mv.to}`, state.moveCount);
+ if (mv.captured) {
+ const capturedPiece = { color: mv.color === 'w' ? 'b' : 'w', type: mv.captured };
+ if (mv.color === 'w') state.capturedByWhite.push(capturedPiece);
+ else state.capturedByBlack.push(capturedPiece);
+ }
+ });
+ // Set last move
+ if (history.length > 0) {
+ const last = history[history.length - 1];
+ state.lastMove = { from: last.from, to: last.to };
+ }
+ }
+ } catch (e) {
+ console.warn('Could not load PGN from invite link:', e);
+ }
+ }
+
+ buildBoard();
+ renderAll();
+}
+
+/* ─── Invite URL ──────────────────────────────────────────────── */
+function generateInviteURL() {
+ const pgn = state.chess.pgn() || '';
+ const params = new URLSearchParams({
+ w: state.whitePlayer,
+ b: state.blackPlayer,
+ sql: state.showSQL ? '1' : '0',
+ pgn: btoa(pgn),
+ });
+ const base = window.location.origin + window.location.pathname;
+ return `${base}?${params.toString()}`;
+}
+
+function loadFromURL() {
+ const params = new URLSearchParams(window.location.search);
+ if (!params.has('pgn')) return false;
+ const whiteName = params.get('w') || 'Guest White';
+ const blackName = params.get('b') || 'Guest Black';
+ const showSQL = params.get('sql') !== '0';
+ let pgn = '';
+ try { pgn = atob(params.get('pgn') || ''); } catch (e) { /* ignore */ }
+ startGame(whiteName, blackName, showSQL, pgn);
+ return true;
+}
+
+/* ─── Event Wiring ────────────────────────────────────────────── */
+function init() {
+ // Always start with setup modal hidden; show it only when needed
+ document.getElementById('setupModalOverlay').classList.add('hidden');
+
+ // Try loading from invite URL first
+ const loadedFromURL = loadFromURL();
+
+ // Show setup modal unless loaded from invite
+ if (!loadedFromURL) {
+ document.getElementById('setupModalOverlay').classList.remove('hidden');
+ }
+
+ // New Game button → open setup modal
+ document.getElementById('btnNewGame').addEventListener('click', () => {
+ document.getElementById('setupModalOverlay').classList.remove('hidden');
+ });
+
+ // Start Game
+ document.getElementById('btnStartGame').addEventListener('click', () => {
+ const wName = document.getElementById('inputWhiteName').value.trim() || 'Guest White';
+ const bName = document.getElementById('inputBlackName').value.trim() || 'Guest Black';
+ const showSql = document.getElementById('chkShowSQL').checked;
+ document.getElementById('setupModalOverlay').classList.add('hidden');
+ startGame(wName, bName, showSql, null);
+ });
+
+ // Play as Guest (skip name entry)
+ document.getElementById('btnPlayAsGuest').addEventListener('click', () => {
+ document.getElementById('setupModalOverlay').classList.add('hidden');
+ startGame('Guest White', 'Guest Black', true, null);
+ });
+
+ // Toggle SQL Panel
+ document.getElementById('btnToggleSQL').addEventListener('click', () => {
+ state.showSQL = !state.showSQL;
+ const sqlPanel = document.getElementById('sqlPanel');
+ const lblEl = document.getElementById('sqlToggleLabel');
+ const iconEl = document.getElementById('sqlToggleIcon');
+ if (state.showSQL) {
+ sqlPanel.classList.remove('hidden-panel');
+ lblEl.textContent = 'Hide SQL';
+ iconEl.textContent = '◧';
+ } else {
+ sqlPanel.classList.add('hidden-panel');
+ lblEl.textContent = 'Show SQL';
+ iconEl.textContent = '□';
+ }
+ });
+
+ // Undo
+ document.getElementById('btnUndo').addEventListener('click', () => {
+ if (!state.chess) return;
+ const undone = state.chess.undo();
+ if (!undone) return;
+ state.moveCount = Math.max(0, state.moveCount - 1);
+ state.lastMove = null;
+ // Fix captures
+ if (undone.captured) {
+ if (undone.color === 'w') state.capturedByWhite.pop();
+ else state.capturedByBlack.pop();
+ }
+ // Remove last SQL block from UI
+ const content = document.getElementById('sqlContent');
+ if (content.lastChild && !content.lastChild.id) {
+ content.removeChild(content.lastChild);
+ state.sqlBlocks.pop();
+ }
+ state.selectedSquare = null;
+ state.validMoves = [];
+ renderAll();
+ });
+
+ // Flip board
+ document.getElementById('btnFlip').addEventListener('click', () => {
+ state.flipped = !state.flipped;
+ buildBoard();
+ renderPieces();
+ });
+
+ // Clear SQL
+ document.getElementById('btnClearSQL').addEventListener('click', () => {
+ const content = document.getElementById('sqlContent');
+ content.innerHTML = '';
+ state.sqlBlocks = [];
+ });
+
+ // Copy all SQL
+ document.getElementById('btnCopySQL').addEventListener('click', () => {
+ const allSQL = state.sqlBlocks.map(b => '-- ' + b.label + '\n' + b.code).join('\n\n');
+ copyToClipboard(allSQL, 'All SQL copied!');
+ });
+
+ // Invite button
+ document.getElementById('btnInvite').addEventListener('click', () => {
+ if (!state.chess) {
+ showToast('Start a game first!');
+ return;
+ }
+ const url = generateInviteURL();
+ document.getElementById('inviteUrlInput').value = url;
+ document.getElementById('inviteTurn').textContent =
+ state.chess.turn() === 'w' ? state.whitePlayer : state.blackPlayer;
+ document.getElementById('inviteMoves').textContent = state.moveCount;
+ document.getElementById('inviteModalOverlay').classList.remove('hidden');
+ document.getElementById('copyFeedback').classList.add('hidden');
+ });
+
+ document.getElementById('btnCloseInvite').addEventListener('click', () => {
+ document.getElementById('inviteModalOverlay').classList.add('hidden');
+ });
+ document.getElementById('inviteModalOverlay').addEventListener('click', (e) => {
+ if (e.target === document.getElementById('inviteModalOverlay')) {
+ document.getElementById('inviteModalOverlay').classList.add('hidden');
+ }
+ });
+
+ document.getElementById('btnCopyInvite').addEventListener('click', () => {
+ const url = document.getElementById('inviteUrlInput').value;
+ copyToClipboard(url, null);
+ document.getElementById('copyFeedback').classList.remove('hidden');
+ setTimeout(() => document.getElementById('copyFeedback').classList.add('hidden'), 2500);
+ });
+
+ // Close invite on overlay click already wired above
+}
+
+function copyToClipboard(text, toastMsg) {
+ if (navigator.clipboard && navigator.clipboard.writeText) {
+ navigator.clipboard.writeText(text).then(() => {
+ if (toastMsg) showToast(toastMsg);
+ }).catch(() => fallbackCopy(text, toastMsg));
+ } else {
+ fallbackCopy(text, toastMsg);
+ }
+}
+
+function fallbackCopy(text, toastMsg) {
+ const ta = document.createElement('textarea');
+ ta.value = text;
+ ta.style.cssText = 'position:fixed;left:-9999px;top:-9999px;';
+ document.body.appendChild(ta);
+ ta.select();
+ try {
+ document.execCommand('copy');
+ if (toastMsg) showToast(toastMsg);
+ } catch (e) {
+ showToast('Copy failed — please copy manually');
+ }
+ document.body.removeChild(ta);
+}
+
+/* ─── Bootstrap ───────────────────────────────────────────────── */
+document.addEventListener('DOMContentLoaded', init);
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..2542c90
--- /dev/null
+++ b/package.json
@@ -0,0 +1,5 @@
+{
+ "dependencies": {
+ "chess.js": "^0.10.3"
+ }
+}
diff --git a/vendor/chess.min.js b/vendor/chess.min.js
new file mode 100755
index 0000000..120494d
--- /dev/null
+++ b/vendor/chess.min.js
@@ -0,0 +1,1749 @@
+/*
+ * Copyright (c) 2020, Jeff Hlywa (jhlywa@gmail.com)
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice,
+ * this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ *
+ *----------------------------------------------------------------------------*/
+
+/* minified license below */
+
+/* @license
+ * Copyright (c) 2018, Jeff Hlywa (jhlywa@gmail.com)
+ * Released under the BSD license
+ * https://github.com/jhlywa/chess.js/blob/master/LICENSE
+ */
+
+var Chess = function(fen) {
+ var BLACK = 'b'
+ var WHITE = 'w'
+
+ var EMPTY = -1
+
+ var PAWN = 'p'
+ var KNIGHT = 'n'
+ var BISHOP = 'b'
+ var ROOK = 'r'
+ var QUEEN = 'q'
+ var KING = 'k'
+
+ var SYMBOLS = 'pnbrqkPNBRQK'
+
+ var DEFAULT_POSITION =
+ 'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1'
+
+ var POSSIBLE_RESULTS = ['1-0', '0-1', '1/2-1/2', '*']
+
+ var PAWN_OFFSETS = {
+ b: [16, 32, 17, 15],
+ w: [-16, -32, -17, -15]
+ }
+
+ var PIECE_OFFSETS = {
+ n: [-18, -33, -31, -14, 18, 33, 31, 14],
+ b: [-17, -15, 17, 15],
+ r: [-16, 1, 16, -1],
+ q: [-17, -16, -15, 1, 17, 16, 15, -1],
+ k: [-17, -16, -15, 1, 17, 16, 15, -1]
+ }
+
+ // prettier-ignore
+ var ATTACKS = [
+ 20, 0, 0, 0, 0, 0, 0, 24, 0, 0, 0, 0, 0, 0,20, 0,
+ 0,20, 0, 0, 0, 0, 0, 24, 0, 0, 0, 0, 0,20, 0, 0,
+ 0, 0,20, 0, 0, 0, 0, 24, 0, 0, 0, 0,20, 0, 0, 0,
+ 0, 0, 0,20, 0, 0, 0, 24, 0, 0, 0,20, 0, 0, 0, 0,
+ 0, 0, 0, 0,20, 0, 0, 24, 0, 0,20, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0,20, 2, 24, 2,20, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 2,53, 56, 53, 2, 0, 0, 0, 0, 0, 0,
+ 24,24,24,24,24,24,56, 0, 56,24,24,24,24,24,24, 0,
+ 0, 0, 0, 0, 0, 2,53, 56, 53, 2, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0,20, 2, 24, 2,20, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0,20, 0, 0, 24, 0, 0,20, 0, 0, 0, 0, 0,
+ 0, 0, 0,20, 0, 0, 0, 24, 0, 0, 0,20, 0, 0, 0, 0,
+ 0, 0,20, 0, 0, 0, 0, 24, 0, 0, 0, 0,20, 0, 0, 0,
+ 0,20, 0, 0, 0, 0, 0, 24, 0, 0, 0, 0, 0,20, 0, 0,
+ 20, 0, 0, 0, 0, 0, 0, 24, 0, 0, 0, 0, 0, 0,20
+ ];
+
+ // prettier-ignore
+ var RAYS = [
+ 17, 0, 0, 0, 0, 0, 0, 16, 0, 0, 0, 0, 0, 0, 15, 0,
+ 0, 17, 0, 0, 0, 0, 0, 16, 0, 0, 0, 0, 0, 15, 0, 0,
+ 0, 0, 17, 0, 0, 0, 0, 16, 0, 0, 0, 0, 15, 0, 0, 0,
+ 0, 0, 0, 17, 0, 0, 0, 16, 0, 0, 0, 15, 0, 0, 0, 0,
+ 0, 0, 0, 0, 17, 0, 0, 16, 0, 0, 15, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 17, 0, 16, 0, 15, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 17, 16, 15, 0, 0, 0, 0, 0, 0, 0,
+ 1, 1, 1, 1, 1, 1, 1, 0, -1, -1, -1,-1, -1, -1, -1, 0,
+ 0, 0, 0, 0, 0, 0,-15,-16,-17, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0,-15, 0,-16, 0,-17, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0,-15, 0, 0,-16, 0, 0,-17, 0, 0, 0, 0, 0,
+ 0, 0, 0,-15, 0, 0, 0,-16, 0, 0, 0,-17, 0, 0, 0, 0,
+ 0, 0,-15, 0, 0, 0, 0,-16, 0, 0, 0, 0,-17, 0, 0, 0,
+ 0,-15, 0, 0, 0, 0, 0,-16, 0, 0, 0, 0, 0,-17, 0, 0,
+ -15, 0, 0, 0, 0, 0, 0,-16, 0, 0, 0, 0, 0, 0,-17
+ ];
+
+ var SHIFTS = { p: 0, n: 1, b: 2, r: 3, q: 4, k: 5 }
+
+ var FLAGS = {
+ NORMAL: 'n',
+ CAPTURE: 'c',
+ BIG_PAWN: 'b',
+ EP_CAPTURE: 'e',
+ PROMOTION: 'p',
+ KSIDE_CASTLE: 'k',
+ QSIDE_CASTLE: 'q'
+ }
+
+ var BITS = {
+ NORMAL: 1,
+ CAPTURE: 2,
+ BIG_PAWN: 4,
+ EP_CAPTURE: 8,
+ PROMOTION: 16,
+ KSIDE_CASTLE: 32,
+ QSIDE_CASTLE: 64
+ }
+
+ var RANK_1 = 7
+ var RANK_2 = 6
+ var RANK_3 = 5
+ var RANK_4 = 4
+ var RANK_5 = 3
+ var RANK_6 = 2
+ var RANK_7 = 1
+ var RANK_8 = 0
+
+ // prettier-ignore
+ var SQUARES = {
+ a8: 0, b8: 1, c8: 2, d8: 3, e8: 4, f8: 5, g8: 6, h8: 7,
+ a7: 16, b7: 17, c7: 18, d7: 19, e7: 20, f7: 21, g7: 22, h7: 23,
+ a6: 32, b6: 33, c6: 34, d6: 35, e6: 36, f6: 37, g6: 38, h6: 39,
+ a5: 48, b5: 49, c5: 50, d5: 51, e5: 52, f5: 53, g5: 54, h5: 55,
+ a4: 64, b4: 65, c4: 66, d4: 67, e4: 68, f4: 69, g4: 70, h4: 71,
+ a3: 80, b3: 81, c3: 82, d3: 83, e3: 84, f3: 85, g3: 86, h3: 87,
+ a2: 96, b2: 97, c2: 98, d2: 99, e2: 100, f2: 101, g2: 102, h2: 103,
+ a1: 112, b1: 113, c1: 114, d1: 115, e1: 116, f1: 117, g1: 118, h1: 119
+ };
+
+ var ROOKS = {
+ w: [
+ { square: SQUARES.a1, flag: BITS.QSIDE_CASTLE },
+ { square: SQUARES.h1, flag: BITS.KSIDE_CASTLE }
+ ],
+ b: [
+ { square: SQUARES.a8, flag: BITS.QSIDE_CASTLE },
+ { square: SQUARES.h8, flag: BITS.KSIDE_CASTLE }
+ ]
+ }
+
+ var board = new Array(128)
+ var kings = { w: EMPTY, b: EMPTY }
+ var turn = WHITE
+ var castling = { w: 0, b: 0 }
+ var ep_square = EMPTY
+ var half_moves = 0
+ var move_number = 1
+ var history = []
+ var header = {}
+
+ /* if the user passes in a fen string, load it, else default to
+ * starting position
+ */
+ if (typeof fen === 'undefined') {
+ load(DEFAULT_POSITION)
+ } else {
+ load(fen)
+ }
+
+ function clear(keep_headers) {
+ if (typeof keep_headers === 'undefined') {
+ keep_headers = false
+ }
+
+ board = new Array(128)
+ kings = { w: EMPTY, b: EMPTY }
+ turn = WHITE
+ castling = { w: 0, b: 0 }
+ ep_square = EMPTY
+ half_moves = 0
+ move_number = 1
+ history = []
+ if (!keep_headers) header = {}
+ update_setup(generate_fen())
+ }
+
+ function reset() {
+ load(DEFAULT_POSITION)
+ }
+
+ function load(fen, keep_headers) {
+ if (typeof keep_headers === 'undefined') {
+ keep_headers = false
+ }
+
+ var tokens = fen.split(/\s+/)
+ var position = tokens[0]
+ var square = 0
+
+ if (!validate_fen(fen).valid) {
+ return false
+ }
+
+ clear(keep_headers)
+
+ for (var i = 0; i < position.length; i++) {
+ var piece = position.charAt(i)
+
+ if (piece === '/') {
+ square += 8
+ } else if (is_digit(piece)) {
+ square += parseInt(piece, 10)
+ } else {
+ var color = piece < 'a' ? WHITE : BLACK
+ put({ type: piece.toLowerCase(), color: color }, algebraic(square))
+ square++
+ }
+ }
+
+ turn = tokens[1]
+
+ if (tokens[2].indexOf('K') > -1) {
+ castling.w |= BITS.KSIDE_CASTLE
+ }
+ if (tokens[2].indexOf('Q') > -1) {
+ castling.w |= BITS.QSIDE_CASTLE
+ }
+ if (tokens[2].indexOf('k') > -1) {
+ castling.b |= BITS.KSIDE_CASTLE
+ }
+ if (tokens[2].indexOf('q') > -1) {
+ castling.b |= BITS.QSIDE_CASTLE
+ }
+
+ ep_square = tokens[3] === '-' ? EMPTY : SQUARES[tokens[3]]
+ half_moves = parseInt(tokens[4], 10)
+ move_number = parseInt(tokens[5], 10)
+
+ update_setup(generate_fen())
+
+ return true
+ }
+
+ /* TODO: this function is pretty much crap - it validates structure but
+ * completely ignores content (e.g. doesn't verify that each side has a king)
+ * ... we should rewrite this, and ditch the silly error_number field while
+ * we're at it
+ */
+ function validate_fen(fen) {
+ var errors = {
+ 0: 'No errors.',
+ 1: 'FEN string must contain six space-delimited fields.',
+ 2: '6th field (move number) must be a positive integer.',
+ 3: '5th field (half move counter) must be a non-negative integer.',
+ 4: '4th field (en-passant square) is invalid.',
+ 5: '3rd field (castling availability) is invalid.',
+ 6: '2nd field (side to move) is invalid.',
+ 7: "1st field (piece positions) does not contain 8 '/'-delimited rows.",
+ 8: '1st field (piece positions) is invalid [consecutive numbers].',
+ 9: '1st field (piece positions) is invalid [invalid piece].',
+ 10: '1st field (piece positions) is invalid [row too large].',
+ 11: 'Illegal en-passant square'
+ }
+
+ /* 1st criterion: 6 space-seperated fields? */
+ var tokens = fen.split(/\s+/)
+ if (tokens.length !== 6) {
+ return { valid: false, error_number: 1, error: errors[1] }
+ }
+
+ /* 2nd criterion: move number field is a integer value > 0? */
+ if (isNaN(tokens[5]) || parseInt(tokens[5], 10) <= 0) {
+ return { valid: false, error_number: 2, error: errors[2] }
+ }
+
+ /* 3rd criterion: half move counter is an integer >= 0? */
+ if (isNaN(tokens[4]) || parseInt(tokens[4], 10) < 0) {
+ return { valid: false, error_number: 3, error: errors[3] }
+ }
+
+ /* 4th criterion: 4th field is a valid e.p.-string? */
+ if (!/^(-|[abcdefgh][36])$/.test(tokens[3])) {
+ return { valid: false, error_number: 4, error: errors[4] }
+ }
+
+ /* 5th criterion: 3th field is a valid castle-string? */
+ if (!/^(KQ?k?q?|Qk?q?|kq?|q|-)$/.test(tokens[2])) {
+ return { valid: false, error_number: 5, error: errors[5] }
+ }
+
+ /* 6th criterion: 2nd field is "w" (white) or "b" (black)? */
+ if (!/^(w|b)$/.test(tokens[1])) {
+ return { valid: false, error_number: 6, error: errors[6] }
+ }
+
+ /* 7th criterion: 1st field contains 8 rows? */
+ var rows = tokens[0].split('/')
+ if (rows.length !== 8) {
+ return { valid: false, error_number: 7, error: errors[7] }
+ }
+
+ /* 8th criterion: every row is valid? */
+ for (var i = 0; i < rows.length; i++) {
+ /* check for right sum of fields AND not two numbers in succession */
+ var sum_fields = 0
+ var previous_was_number = false
+
+ for (var k = 0; k < rows[i].length; k++) {
+ if (!isNaN(rows[i][k])) {
+ if (previous_was_number) {
+ return { valid: false, error_number: 8, error: errors[8] }
+ }
+ sum_fields += parseInt(rows[i][k], 10)
+ previous_was_number = true
+ } else {
+ if (!/^[prnbqkPRNBQK]$/.test(rows[i][k])) {
+ return { valid: false, error_number: 9, error: errors[9] }
+ }
+ sum_fields += 1
+ previous_was_number = false
+ }
+ }
+ if (sum_fields !== 8) {
+ return { valid: false, error_number: 10, error: errors[10] }
+ }
+ }
+
+ if (
+ (tokens[3][1] == '3' && tokens[1] == 'w') ||
+ (tokens[3][1] == '6' && tokens[1] == 'b')
+ ) {
+ return { valid: false, error_number: 11, error: errors[11] }
+ }
+
+ /* everything's okay! */
+ return { valid: true, error_number: 0, error: errors[0] }
+ }
+
+ function generate_fen() {
+ var empty = 0
+ var fen = ''
+
+ for (var i = SQUARES.a8; i <= SQUARES.h1; i++) {
+ if (board[i] == null) {
+ empty++
+ } else {
+ if (empty > 0) {
+ fen += empty
+ empty = 0
+ }
+ var color = board[i].color
+ var piece = board[i].type
+
+ fen += color === WHITE ? piece.toUpperCase() : piece.toLowerCase()
+ }
+
+ if ((i + 1) & 0x88) {
+ if (empty > 0) {
+ fen += empty
+ }
+
+ if (i !== SQUARES.h1) {
+ fen += '/'
+ }
+
+ empty = 0
+ i += 8
+ }
+ }
+
+ var cflags = ''
+ if (castling[WHITE] & BITS.KSIDE_CASTLE) {
+ cflags += 'K'
+ }
+ if (castling[WHITE] & BITS.QSIDE_CASTLE) {
+ cflags += 'Q'
+ }
+ if (castling[BLACK] & BITS.KSIDE_CASTLE) {
+ cflags += 'k'
+ }
+ if (castling[BLACK] & BITS.QSIDE_CASTLE) {
+ cflags += 'q'
+ }
+
+ /* do we have an empty castling flag? */
+ cflags = cflags || '-'
+ var epflags = ep_square === EMPTY ? '-' : algebraic(ep_square)
+
+ return [fen, turn, cflags, epflags, half_moves, move_number].join(' ')
+ }
+
+ function set_header(args) {
+ for (var i = 0; i < args.length; i += 2) {
+ if (typeof args[i] === 'string' && typeof args[i + 1] === 'string') {
+ header[args[i]] = args[i + 1]
+ }
+ }
+ return header
+ }
+
+ /* called when the initial board setup is changed with put() or remove().
+ * modifies the SetUp and FEN properties of the header object. if the FEN is
+ * equal to the default position, the SetUp and FEN are deleted
+ * the setup is only updated if history.length is zero, ie moves haven't been
+ * made.
+ */
+ function update_setup(fen) {
+ if (history.length > 0) return
+
+ if (fen !== DEFAULT_POSITION) {
+ header['SetUp'] = '1'
+ header['FEN'] = fen
+ } else {
+ delete header['SetUp']
+ delete header['FEN']
+ }
+ }
+
+ function get(square) {
+ var piece = board[SQUARES[square]]
+ return piece ? { type: piece.type, color: piece.color } : null
+ }
+
+ function put(piece, square) {
+ /* check for valid piece object */
+ if (!('type' in piece && 'color' in piece)) {
+ return false
+ }
+
+ /* check for piece */
+ if (SYMBOLS.indexOf(piece.type.toLowerCase()) === -1) {
+ return false
+ }
+
+ /* check for valid square */
+ if (!(square in SQUARES)) {
+ return false
+ }
+
+ var sq = SQUARES[square]
+
+ /* don't let the user place more than one king */
+ if (
+ piece.type == KING &&
+ !(kings[piece.color] == EMPTY || kings[piece.color] == sq)
+ ) {
+ return false
+ }
+
+ board[sq] = { type: piece.type, color: piece.color }
+ if (piece.type === KING) {
+ kings[piece.color] = sq
+ }
+
+ update_setup(generate_fen())
+
+ return true
+ }
+
+ function remove(square) {
+ var piece = get(square)
+ board[SQUARES[square]] = null
+ if (piece && piece.type === KING) {
+ kings[piece.color] = EMPTY
+ }
+
+ update_setup(generate_fen())
+
+ return piece
+ }
+
+ function build_move(board, from, to, flags, promotion) {
+ var move = {
+ color: turn,
+ from: from,
+ to: to,
+ flags: flags,
+ piece: board[from].type
+ }
+
+ if (promotion) {
+ move.flags |= BITS.PROMOTION
+ move.promotion = promotion
+ }
+
+ if (board[to]) {
+ move.captured = board[to].type
+ } else if (flags & BITS.EP_CAPTURE) {
+ move.captured = PAWN
+ }
+ return move
+ }
+
+ function generate_moves(options) {
+ function add_move(board, moves, from, to, flags) {
+ /* if pawn promotion */
+ if (
+ board[from].type === PAWN &&
+ (rank(to) === RANK_8 || rank(to) === RANK_1)
+ ) {
+ var pieces = [QUEEN, ROOK, BISHOP, KNIGHT]
+ for (var i = 0, len = pieces.length; i < len; i++) {
+ moves.push(build_move(board, from, to, flags, pieces[i]))
+ }
+ } else {
+ moves.push(build_move(board, from, to, flags))
+ }
+ }
+
+ var moves = []
+ var us = turn
+ var them = swap_color(us)
+ var second_rank = { b: RANK_7, w: RANK_2 }
+
+ var first_sq = SQUARES.a8
+ var last_sq = SQUARES.h1
+ var single_square = false
+
+ /* do we want legal moves? */
+ var legal =
+ typeof options !== 'undefined' && 'legal' in options
+ ? options.legal
+ : true
+
+ /* are we generating moves for a single square? */
+ if (typeof options !== 'undefined' && 'square' in options) {
+ if (options.square in SQUARES) {
+ first_sq = last_sq = SQUARES[options.square]
+ single_square = true
+ } else {
+ /* invalid square */
+ return []
+ }
+ }
+
+ for (var i = first_sq; i <= last_sq; i++) {
+ /* did we run off the end of the board */
+ if (i & 0x88) {
+ i += 7
+ continue
+ }
+
+ var piece = board[i]
+ if (piece == null || piece.color !== us) {
+ continue
+ }
+
+ if (piece.type === PAWN) {
+ /* single square, non-capturing */
+ var square = i + PAWN_OFFSETS[us][0]
+ if (board[square] == null) {
+ add_move(board, moves, i, square, BITS.NORMAL)
+
+ /* double square */
+ var square = i + PAWN_OFFSETS[us][1]
+ if (second_rank[us] === rank(i) && board[square] == null) {
+ add_move(board, moves, i, square, BITS.BIG_PAWN)
+ }
+ }
+
+ /* pawn captures */
+ for (j = 2; j < 4; j++) {
+ var square = i + PAWN_OFFSETS[us][j]
+ if (square & 0x88) continue
+
+ if (board[square] != null && board[square].color === them) {
+ add_move(board, moves, i, square, BITS.CAPTURE)
+ } else if (square === ep_square) {
+ add_move(board, moves, i, ep_square, BITS.EP_CAPTURE)
+ }
+ }
+ } else {
+ for (var j = 0, len = PIECE_OFFSETS[piece.type].length; j < len; j++) {
+ var offset = PIECE_OFFSETS[piece.type][j]
+ var square = i
+
+ while (true) {
+ square += offset
+ if (square & 0x88) break
+
+ if (board[square] == null) {
+ add_move(board, moves, i, square, BITS.NORMAL)
+ } else {
+ if (board[square].color === us) break
+ add_move(board, moves, i, square, BITS.CAPTURE)
+ break
+ }
+
+ /* break, if knight or king */
+ if (piece.type === 'n' || piece.type === 'k') break
+ }
+ }
+ }
+ }
+
+ /* check for castling if: a) we're generating all moves, or b) we're doing
+ * single square move generation on the king's square
+ */
+ if (!single_square || last_sq === kings[us]) {
+ /* king-side castling */
+ if (castling[us] & BITS.KSIDE_CASTLE) {
+ var castling_from = kings[us]
+ var castling_to = castling_from + 2
+
+ if (
+ board[castling_from + 1] == null &&
+ board[castling_to] == null &&
+ !attacked(them, kings[us]) &&
+ !attacked(them, castling_from + 1) &&
+ !attacked(them, castling_to)
+ ) {
+ add_move(board, moves, kings[us], castling_to, BITS.KSIDE_CASTLE)
+ }
+ }
+
+ /* queen-side castling */
+ if (castling[us] & BITS.QSIDE_CASTLE) {
+ var castling_from = kings[us]
+ var castling_to = castling_from - 2
+
+ if (
+ board[castling_from - 1] == null &&
+ board[castling_from - 2] == null &&
+ board[castling_from - 3] == null &&
+ !attacked(them, kings[us]) &&
+ !attacked(them, castling_from - 1) &&
+ !attacked(them, castling_to)
+ ) {
+ add_move(board, moves, kings[us], castling_to, BITS.QSIDE_CASTLE)
+ }
+ }
+ }
+
+ /* return all pseudo-legal moves (this includes moves that allow the king
+ * to be captured)
+ */
+ if (!legal) {
+ return moves
+ }
+
+ /* filter out illegal moves */
+ var legal_moves = []
+ for (var i = 0, len = moves.length; i < len; i++) {
+ make_move(moves[i])
+ if (!king_attacked(us)) {
+ legal_moves.push(moves[i])
+ }
+ undo_move()
+ }
+
+ return legal_moves
+ }
+
+ /* convert a move from 0x88 coordinates to Standard Algebraic Notation
+ * (SAN)
+ *
+ * @param {boolean} sloppy Use the sloppy SAN generator to work around over
+ * disambiguation bugs in Fritz and Chessbase. See below:
+ *
+ * r1bqkbnr/ppp2ppp/2n5/1B1pP3/4P3/8/PPPP2PP/RNBQK1NR b KQkq - 2 4
+ * 4. ... Nge7 is overly disambiguated because the knight on c6 is pinned
+ * 4. ... Ne7 is technically the valid SAN
+ */
+ function move_to_san(move, sloppy) {
+ var output = ''
+
+ if (move.flags & BITS.KSIDE_CASTLE) {
+ output = 'O-O'
+ } else if (move.flags & BITS.QSIDE_CASTLE) {
+ output = 'O-O-O'
+ } else {
+ var disambiguator = get_disambiguator(move, sloppy)
+
+ if (move.piece !== PAWN) {
+ output += move.piece.toUpperCase() + disambiguator
+ }
+
+ if (move.flags & (BITS.CAPTURE | BITS.EP_CAPTURE)) {
+ if (move.piece === PAWN) {
+ output += algebraic(move.from)[0]
+ }
+ output += 'x'
+ }
+
+ output += algebraic(move.to)
+
+ if (move.flags & BITS.PROMOTION) {
+ output += '=' + move.promotion.toUpperCase()
+ }
+ }
+
+ make_move(move)
+ if (in_check()) {
+ if (in_checkmate()) {
+ output += '#'
+ } else {
+ output += '+'
+ }
+ }
+ undo_move()
+
+ return output
+ }
+
+ // parses all of the decorators out of a SAN string
+ function stripped_san(move) {
+ return move.replace(/=/, '').replace(/[+#]?[?!]*$/, '')
+ }
+
+ function attacked(color, square) {
+ for (var i = SQUARES.a8; i <= SQUARES.h1; i++) {
+ /* did we run off the end of the board */
+ if (i & 0x88) {
+ i += 7
+ continue
+ }
+
+ /* if empty square or wrong color */
+ if (board[i] == null || board[i].color !== color) continue
+
+ var piece = board[i]
+ var difference = i - square
+ var index = difference + 119
+
+ if (ATTACKS[index] & (1 << SHIFTS[piece.type])) {
+ if (piece.type === PAWN) {
+ if (difference > 0) {
+ if (piece.color === WHITE) return true
+ } else {
+ if (piece.color === BLACK) return true
+ }
+ continue
+ }
+
+ /* if the piece is a knight or a king */
+ if (piece.type === 'n' || piece.type === 'k') return true
+
+ var offset = RAYS[index]
+ var j = i + offset
+
+ var blocked = false
+ while (j !== square) {
+ if (board[j] != null) {
+ blocked = true
+ break
+ }
+ j += offset
+ }
+
+ if (!blocked) return true
+ }
+ }
+
+ return false
+ }
+
+ function king_attacked(color) {
+ return attacked(swap_color(color), kings[color])
+ }
+
+ function in_check() {
+ return king_attacked(turn)
+ }
+
+ function in_checkmate() {
+ return in_check() && generate_moves().length === 0
+ }
+
+ function in_stalemate() {
+ return !in_check() && generate_moves().length === 0
+ }
+
+ function insufficient_material() {
+ var pieces = {}
+ var bishops = []
+ var num_pieces = 0
+ var sq_color = 0
+
+ for (var i = SQUARES.a8; i <= SQUARES.h1; i++) {
+ sq_color = (sq_color + 1) % 2
+ if (i & 0x88) {
+ i += 7
+ continue
+ }
+
+ var piece = board[i]
+ if (piece) {
+ pieces[piece.type] = piece.type in pieces ? pieces[piece.type] + 1 : 1
+ if (piece.type === BISHOP) {
+ bishops.push(sq_color)
+ }
+ num_pieces++
+ }
+ }
+
+ /* k vs. k */
+ if (num_pieces === 2) {
+ return true
+ } else if (
+ /* k vs. kn .... or .... k vs. kb */
+ num_pieces === 3 &&
+ (pieces[BISHOP] === 1 || pieces[KNIGHT] === 1)
+ ) {
+ return true
+ } else if (num_pieces === pieces[BISHOP] + 2) {
+ /* kb vs. kb where any number of bishops are all on the same color */
+ var sum = 0
+ var len = bishops.length
+ for (var i = 0; i < len; i++) {
+ sum += bishops[i]
+ }
+ if (sum === 0 || sum === len) {
+ return true
+ }
+ }
+
+ return false
+ }
+
+ function in_threefold_repetition() {
+ /* TODO: while this function is fine for casual use, a better
+ * implementation would use a Zobrist key (instead of FEN). the
+ * Zobrist key would be maintained in the make_move/undo_move functions,
+ * avoiding the costly that we do below.
+ */
+ var moves = []
+ var positions = {}
+ var repetition = false
+
+ while (true) {
+ var move = undo_move()
+ if (!move) break
+ moves.push(move)
+ }
+
+ while (true) {
+ /* remove the last two fields in the FEN string, they're not needed
+ * when checking for draw by rep */
+ var fen = generate_fen()
+ .split(' ')
+ .slice(0, 4)
+ .join(' ')
+
+ /* has the position occurred three or move times */
+ positions[fen] = fen in positions ? positions[fen] + 1 : 1
+ if (positions[fen] >= 3) {
+ repetition = true
+ }
+
+ if (!moves.length) {
+ break
+ }
+ make_move(moves.pop())
+ }
+
+ return repetition
+ }
+
+ function push(move) {
+ history.push({
+ move: move,
+ kings: { b: kings.b, w: kings.w },
+ turn: turn,
+ castling: { b: castling.b, w: castling.w },
+ ep_square: ep_square,
+ half_moves: half_moves,
+ move_number: move_number
+ })
+ }
+
+ function make_move(move) {
+ var us = turn
+ var them = swap_color(us)
+ push(move)
+
+ board[move.to] = board[move.from]
+ board[move.from] = null
+
+ /* if ep capture, remove the captured pawn */
+ if (move.flags & BITS.EP_CAPTURE) {
+ if (turn === BLACK) {
+ board[move.to - 16] = null
+ } else {
+ board[move.to + 16] = null
+ }
+ }
+
+ /* if pawn promotion, replace with new piece */
+ if (move.flags & BITS.PROMOTION) {
+ board[move.to] = { type: move.promotion, color: us }
+ }
+
+ /* if we moved the king */
+ if (board[move.to].type === KING) {
+ kings[board[move.to].color] = move.to
+
+ /* if we castled, move the rook next to the king */
+ if (move.flags & BITS.KSIDE_CASTLE) {
+ var castling_to = move.to - 1
+ var castling_from = move.to + 1
+ board[castling_to] = board[castling_from]
+ board[castling_from] = null
+ } else if (move.flags & BITS.QSIDE_CASTLE) {
+ var castling_to = move.to + 1
+ var castling_from = move.to - 2
+ board[castling_to] = board[castling_from]
+ board[castling_from] = null
+ }
+
+ /* turn off castling */
+ castling[us] = ''
+ }
+
+ /* turn off castling if we move a rook */
+ if (castling[us]) {
+ for (var i = 0, len = ROOKS[us].length; i < len; i++) {
+ if (
+ move.from === ROOKS[us][i].square &&
+ castling[us] & ROOKS[us][i].flag
+ ) {
+ castling[us] ^= ROOKS[us][i].flag
+ break
+ }
+ }
+ }
+
+ /* turn off castling if we capture a rook */
+ if (castling[them]) {
+ for (var i = 0, len = ROOKS[them].length; i < len; i++) {
+ if (
+ move.to === ROOKS[them][i].square &&
+ castling[them] & ROOKS[them][i].flag
+ ) {
+ castling[them] ^= ROOKS[them][i].flag
+ break
+ }
+ }
+ }
+
+ /* if big pawn move, update the en passant square */
+ if (move.flags & BITS.BIG_PAWN) {
+ if (turn === 'b') {
+ ep_square = move.to - 16
+ } else {
+ ep_square = move.to + 16
+ }
+ } else {
+ ep_square = EMPTY
+ }
+
+ /* reset the 50 move counter if a pawn is moved or a piece is captured */
+ if (move.piece === PAWN) {
+ half_moves = 0
+ } else if (move.flags & (BITS.CAPTURE | BITS.EP_CAPTURE)) {
+ half_moves = 0
+ } else {
+ half_moves++
+ }
+
+ if (turn === BLACK) {
+ move_number++
+ }
+ turn = swap_color(turn)
+ }
+
+ function undo_move() {
+ var old = history.pop()
+ if (old == null) {
+ return null
+ }
+
+ var move = old.move
+ kings = old.kings
+ turn = old.turn
+ castling = old.castling
+ ep_square = old.ep_square
+ half_moves = old.half_moves
+ move_number = old.move_number
+
+ var us = turn
+ var them = swap_color(turn)
+
+ board[move.from] = board[move.to]
+ board[move.from].type = move.piece // to undo any promotions
+ board[move.to] = null
+
+ if (move.flags & BITS.CAPTURE) {
+ board[move.to] = { type: move.captured, color: them }
+ } else if (move.flags & BITS.EP_CAPTURE) {
+ var index
+ if (us === BLACK) {
+ index = move.to - 16
+ } else {
+ index = move.to + 16
+ }
+ board[index] = { type: PAWN, color: them }
+ }
+
+ if (move.flags & (BITS.KSIDE_CASTLE | BITS.QSIDE_CASTLE)) {
+ var castling_to, castling_from
+ if (move.flags & BITS.KSIDE_CASTLE) {
+ castling_to = move.to + 1
+ castling_from = move.to - 1
+ } else if (move.flags & BITS.QSIDE_CASTLE) {
+ castling_to = move.to - 2
+ castling_from = move.to + 1
+ }
+
+ board[castling_to] = board[castling_from]
+ board[castling_from] = null
+ }
+
+ return move
+ }
+
+ /* this function is used to uniquely identify ambiguous moves */
+ function get_disambiguator(move, sloppy) {
+ var moves = generate_moves({ legal: !sloppy })
+
+ var from = move.from
+ var to = move.to
+ var piece = move.piece
+
+ var ambiguities = 0
+ var same_rank = 0
+ var same_file = 0
+
+ for (var i = 0, len = moves.length; i < len; i++) {
+ var ambig_from = moves[i].from
+ var ambig_to = moves[i].to
+ var ambig_piece = moves[i].piece
+
+ /* if a move of the same piece type ends on the same to square, we'll
+ * need to add a disambiguator to the algebraic notation
+ */
+ if (piece === ambig_piece && from !== ambig_from && to === ambig_to) {
+ ambiguities++
+
+ if (rank(from) === rank(ambig_from)) {
+ same_rank++
+ }
+
+ if (file(from) === file(ambig_from)) {
+ same_file++
+ }
+ }
+ }
+
+ if (ambiguities > 0) {
+ /* if there exists a similar moving piece on the same rank and file as
+ * the move in question, use the square as the disambiguator
+ */
+ if (same_rank > 0 && same_file > 0) {
+ return algebraic(from)
+ } else if (same_file > 0) {
+ /* if the moving piece rests on the same file, use the rank symbol as the
+ * disambiguator
+ */
+ return algebraic(from).charAt(1)
+ } else {
+ /* else use the file symbol */
+ return algebraic(from).charAt(0)
+ }
+ }
+
+ return ''
+ }
+
+ function ascii() {
+ var s = ' +------------------------+\n'
+ for (var i = SQUARES.a8; i <= SQUARES.h1; i++) {
+ /* display the rank */
+ if (file(i) === 0) {
+ s += ' ' + '87654321'[rank(i)] + ' |'
+ }
+
+ /* empty piece */
+ if (board[i] == null) {
+ s += ' . '
+ } else {
+ var piece = board[i].type
+ var color = board[i].color
+ var symbol = color === WHITE ? piece.toUpperCase() : piece.toLowerCase()
+ s += ' ' + symbol + ' '
+ }
+
+ if ((i + 1) & 0x88) {
+ s += '|\n'
+ i += 8
+ }
+ }
+ s += ' +------------------------+\n'
+ s += ' a b c d e f g h\n'
+
+ return s
+ }
+
+ // convert a move from Standard Algebraic Notation (SAN) to 0x88 coordinates
+ function move_from_san(move, sloppy) {
+ // strip off any move decorations: e.g Nf3+?!
+ var clean_move = stripped_san(move)
+
+ // if we're using the sloppy parser run a regex to grab piece, to, and from
+ // this should parse invalid SAN like: Pe2-e4, Rc1c4, Qf3xf7
+ if (sloppy) {
+ var matches = clean_move.match(
+ /([pnbrqkPNBRQK])?([a-h][1-8])x?-?([a-h][1-8])([qrbnQRBN])?/
+ )
+ if (matches) {
+ var piece = matches[1]
+ var from = matches[2]
+ var to = matches[3]
+ var promotion = matches[4]
+ }
+ }
+
+ var moves = generate_moves()
+ for (var i = 0, len = moves.length; i < len; i++) {
+ // try the strict parser first, then the sloppy parser if requested
+ // by the user
+ if (
+ clean_move === stripped_san(move_to_san(moves[i])) ||
+ (sloppy && clean_move === stripped_san(move_to_san(moves[i], true)))
+ ) {
+ return moves[i]
+ } else {
+ if (
+ matches &&
+ (!piece || piece.toLowerCase() == moves[i].piece) &&
+ SQUARES[from] == moves[i].from &&
+ SQUARES[to] == moves[i].to &&
+ (!promotion || promotion.toLowerCase() == moves[i].promotion)
+ ) {
+ return moves[i]
+ }
+ }
+ }
+
+ return null
+ }
+
+ /*****************************************************************************
+ * UTILITY FUNCTIONS
+ ****************************************************************************/
+ function rank(i) {
+ return i >> 4
+ }
+
+ function file(i) {
+ return i & 15
+ }
+
+ function algebraic(i) {
+ var f = file(i),
+ r = rank(i)
+ return 'abcdefgh'.substring(f, f + 1) + '87654321'.substring(r, r + 1)
+ }
+
+ function swap_color(c) {
+ return c === WHITE ? BLACK : WHITE
+ }
+
+ function is_digit(c) {
+ return '0123456789'.indexOf(c) !== -1
+ }
+
+ /* pretty = external move object */
+ function make_pretty(ugly_move) {
+ var move = clone(ugly_move)
+ move.san = move_to_san(move, false)
+ move.to = algebraic(move.to)
+ move.from = algebraic(move.from)
+
+ var flags = ''
+
+ for (var flag in BITS) {
+ if (BITS[flag] & move.flags) {
+ flags += FLAGS[flag]
+ }
+ }
+ move.flags = flags
+
+ return move
+ }
+
+ function clone(obj) {
+ var dupe = obj instanceof Array ? [] : {}
+
+ for (var property in obj) {
+ if (typeof property === 'object') {
+ dupe[property] = clone(obj[property])
+ } else {
+ dupe[property] = obj[property]
+ }
+ }
+
+ return dupe
+ }
+
+ function trim(str) {
+ return str.replace(/^\s+|\s+$/g, '')
+ }
+
+ /*****************************************************************************
+ * DEBUGGING UTILITIES
+ ****************************************************************************/
+ function perft(depth) {
+ var moves = generate_moves({ legal: false })
+ var nodes = 0
+ var color = turn
+
+ for (var i = 0, len = moves.length; i < len; i++) {
+ make_move(moves[i])
+ if (!king_attacked(color)) {
+ if (depth - 1 > 0) {
+ var child_nodes = perft(depth - 1)
+ nodes += child_nodes
+ } else {
+ nodes++
+ }
+ }
+ undo_move()
+ }
+
+ return nodes
+ }
+
+ return {
+ /***************************************************************************
+ * PUBLIC CONSTANTS (is there a better way to do this?)
+ **************************************************************************/
+ WHITE: WHITE,
+ BLACK: BLACK,
+ PAWN: PAWN,
+ KNIGHT: KNIGHT,
+ BISHOP: BISHOP,
+ ROOK: ROOK,
+ QUEEN: QUEEN,
+ KING: KING,
+ SQUARES: (function() {
+ /* from the ECMA-262 spec (section 12.6.4):
+ * "The mechanics of enumerating the properties ... is
+ * implementation dependent"
+ * so: for (var sq in SQUARES) { keys.push(sq); } might not be
+ * ordered correctly
+ */
+ var keys = []
+ for (var i = SQUARES.a8; i <= SQUARES.h1; i++) {
+ if (i & 0x88) {
+ i += 7
+ continue
+ }
+ keys.push(algebraic(i))
+ }
+ return keys
+ })(),
+ FLAGS: FLAGS,
+
+ /***************************************************************************
+ * PUBLIC API
+ **************************************************************************/
+ load: function(fen) {
+ return load(fen)
+ },
+
+ reset: function() {
+ return reset()
+ },
+
+ moves: function(options) {
+ /* The internal representation of a chess move is in 0x88 format, and
+ * not meant to be human-readable. The code below converts the 0x88
+ * square coordinates to algebraic coordinates. It also prunes an
+ * unnecessary move keys resulting from a verbose call.
+ */
+
+ var ugly_moves = generate_moves(options)
+ var moves = []
+
+ for (var i = 0, len = ugly_moves.length; i < len; i++) {
+ /* does the user want a full move object (most likely not), or just
+ * SAN
+ */
+ if (
+ typeof options !== 'undefined' &&
+ 'verbose' in options &&
+ options.verbose
+ ) {
+ moves.push(make_pretty(ugly_moves[i]))
+ } else {
+ moves.push(move_to_san(ugly_moves[i], false))
+ }
+ }
+
+ return moves
+ },
+
+ in_check: function() {
+ return in_check()
+ },
+
+ in_checkmate: function() {
+ return in_checkmate()
+ },
+
+ in_stalemate: function() {
+ return in_stalemate()
+ },
+
+ in_draw: function() {
+ return (
+ half_moves >= 100 ||
+ in_stalemate() ||
+ insufficient_material() ||
+ in_threefold_repetition()
+ )
+ },
+
+ insufficient_material: function() {
+ return insufficient_material()
+ },
+
+ in_threefold_repetition: function() {
+ return in_threefold_repetition()
+ },
+
+ game_over: function() {
+ return (
+ half_moves >= 100 ||
+ in_checkmate() ||
+ in_stalemate() ||
+ insufficient_material() ||
+ in_threefold_repetition()
+ )
+ },
+
+ validate_fen: function(fen) {
+ return validate_fen(fen)
+ },
+
+ fen: function() {
+ return generate_fen()
+ },
+
+ board: function() {
+ var output = [],
+ row = []
+
+ for (var i = SQUARES.a8; i <= SQUARES.h1; i++) {
+ if (board[i] == null) {
+ row.push(null)
+ } else {
+ row.push({ type: board[i].type, color: board[i].color })
+ }
+ if ((i + 1) & 0x88) {
+ output.push(row)
+ row = []
+ i += 8
+ }
+ }
+
+ return output
+ },
+
+ pgn: function(options) {
+ /* using the specification from http://www.chessclub.com/help/PGN-spec
+ * example for html usage: .pgn({ max_width: 72, newline_char: "
" })
+ */
+ var newline =
+ typeof options === 'object' && typeof options.newline_char === 'string'
+ ? options.newline_char
+ : '\n'
+ var max_width =
+ typeof options === 'object' && typeof options.max_width === 'number'
+ ? options.max_width
+ : 0
+ var result = []
+ var header_exists = false
+
+ /* add the PGN header headerrmation */
+ for (var i in header) {
+ /* TODO: order of enumerated properties in header object is not
+ * guaranteed, see ECMA-262 spec (section 12.6.4)
+ */
+ result.push('[' + i + ' "' + header[i] + '"]' + newline)
+ header_exists = true
+ }
+
+ if (header_exists && history.length) {
+ result.push(newline)
+ }
+
+ /* pop all of history onto reversed_history */
+ var reversed_history = []
+ while (history.length > 0) {
+ reversed_history.push(undo_move())
+ }
+
+ var moves = []
+ var move_string = ''
+
+ /* build the list of moves. a move_string looks like: "3. e3 e6" */
+ while (reversed_history.length > 0) {
+ var move = reversed_history.pop()
+
+ /* if the position started with black to move, start PGN with 1. ... */
+ if (!history.length && move.color === 'b') {
+ move_string = move_number + '. ...'
+ } else if (move.color === 'w') {
+ /* store the previous generated move_string if we have one */
+ if (move_string.length) {
+ moves.push(move_string)
+ }
+ move_string = move_number + '.'
+ }
+
+ move_string = move_string + ' ' + move_to_san(move, false)
+ make_move(move)
+ }
+
+ /* are there any other leftover moves? */
+ if (move_string.length) {
+ moves.push(move_string)
+ }
+
+ /* is there a result? */
+ if (typeof header.Result !== 'undefined') {
+ moves.push(header.Result)
+ }
+
+ /* history should be back to what is was before we started generating PGN,
+ * so join together moves
+ */
+ if (max_width === 0) {
+ return result.join('') + moves.join(' ')
+ }
+
+ /* wrap the PGN output at max_width */
+ var current_width = 0
+ for (var i = 0; i < moves.length; i++) {
+ /* if the current move will push past max_width */
+ if (current_width + moves[i].length > max_width && i !== 0) {
+ /* don't end the line with whitespace */
+ if (result[result.length - 1] === ' ') {
+ result.pop()
+ }
+
+ result.push(newline)
+ current_width = 0
+ } else if (i !== 0) {
+ result.push(' ')
+ current_width++
+ }
+ result.push(moves[i])
+ current_width += moves[i].length
+ }
+
+ return result.join('')
+ },
+
+ load_pgn: function(pgn, options) {
+ // allow the user to specify the sloppy move parser to work around over
+ // disambiguation bugs in Fritz and Chessbase
+ var sloppy =
+ typeof options !== 'undefined' && 'sloppy' in options
+ ? options.sloppy
+ : false
+
+ function mask(str) {
+ return str.replace(/\\/g, '\\')
+ }
+
+ function has_keys(object) {
+ for (var key in object) {
+ return true
+ }
+ return false
+ }
+
+ function parse_pgn_header(header, options) {
+ var newline_char =
+ typeof options === 'object' &&
+ typeof options.newline_char === 'string'
+ ? options.newline_char
+ : '\r?\n'
+ var header_obj = {}
+ var headers = header.split(new RegExp(mask(newline_char)))
+ var key = ''
+ var value = ''
+
+ for (var i = 0; i < headers.length; i++) {
+ key = headers[i].replace(/^\[([A-Z][A-Za-z]*)\s.*\]$/, '$1')
+ value = headers[i].replace(/^\[[A-Za-z]+\s"(.*)"\]$/, '$1')
+ if (trim(key).length > 0) {
+ header_obj[key] = value
+ }
+ }
+
+ return header_obj
+ }
+
+ var newline_char =
+ typeof options === 'object' && typeof options.newline_char === 'string'
+ ? options.newline_char
+ : '\r?\n'
+
+ // RegExp to split header. Takes advantage of the fact that header and movetext
+ // will always have a blank line between them (ie, two newline_char's).
+ // With default newline_char, will equal: /^(\[((?:\r?\n)|.)*\])(?:\r?\n){2}/
+ var header_regex = new RegExp(
+ '^(\\[((?:' +
+ mask(newline_char) +
+ ')|.)*\\])' +
+ '(?:' +
+ mask(newline_char) +
+ '){2}'
+ )
+
+ // If no header given, begin with moves.
+ var header_string = header_regex.test(pgn)
+ ? header_regex.exec(pgn)[1]
+ : ''
+
+ // Put the board in the starting position
+ reset()
+
+ /* parse PGN header */
+ var headers = parse_pgn_header(header_string, options)
+ for (var key in headers) {
+ set_header([key, headers[key]])
+ }
+
+ /* load the starting position indicated by [Setup '1'] and
+ * [FEN position] */
+ if (headers['SetUp'] === '1') {
+ if (!('FEN' in headers && load(headers['FEN'], true))) {
+ // second argument to load: don't clear the headers
+ return false
+ }
+ }
+
+ /* delete header to get the moves */
+ var ms = pgn
+ .replace(header_string, '')
+ .replace(new RegExp(mask(newline_char), 'g'), ' ')
+
+ /* delete comments */
+ ms = ms.replace(/(\{[^}]+\})+?/g, '')
+
+ /* delete recursive annotation variations */
+ var rav_regex = /(\([^\(\)]+\))+?/g
+ while (rav_regex.test(ms)) {
+ ms = ms.replace(rav_regex, '')
+ }
+
+ /* delete move numbers */
+ ms = ms.replace(/\d+\.(\.\.)?/g, '')
+
+ /* delete ... indicating black to move */
+ ms = ms.replace(/\.\.\./g, '')
+
+ /* delete numeric annotation glyphs */
+ ms = ms.replace(/\$\d+/g, '')
+
+ /* trim and get array of moves */
+ var moves = trim(ms).split(new RegExp(/\s+/))
+
+ /* delete empty entries */
+ moves = moves
+ .join(',')
+ .replace(/,,+/g, ',')
+ .split(',')
+ var move = ''
+
+ for (var half_move = 0; half_move < moves.length - 1; half_move++) {
+ move = move_from_san(moves[half_move], sloppy)
+
+ /* move not possible! (don't clear the board to examine to show the
+ * latest valid position)
+ */
+ if (move == null) {
+ return false
+ } else {
+ make_move(move)
+ }
+ }
+
+ /* examine last move */
+ move = moves[moves.length - 1]
+ if (POSSIBLE_RESULTS.indexOf(move) > -1) {
+ if (has_keys(header) && typeof header.Result === 'undefined') {
+ set_header(['Result', move])
+ }
+ } else {
+ move = move_from_san(move, sloppy)
+ if (move == null) {
+ return false
+ } else {
+ make_move(move)
+ }
+ }
+ return true
+ },
+
+ header: function() {
+ return set_header(arguments)
+ },
+
+ ascii: function() {
+ return ascii()
+ },
+
+ turn: function() {
+ return turn
+ },
+
+ move: function(move, options) {
+ /* The move function can be called with in the following parameters:
+ *
+ * .move('Nxb7') <- where 'move' is a case-sensitive SAN string
+ *
+ * .move({ from: 'h7', <- where the 'move' is a move object (additional
+ * to :'h8', fields are ignored)
+ * promotion: 'q',
+ * })
+ */
+
+ // allow the user to specify the sloppy move parser to work around over
+ // disambiguation bugs in Fritz and Chessbase
+ var sloppy =
+ typeof options !== 'undefined' && 'sloppy' in options
+ ? options.sloppy
+ : false
+
+ var move_obj = null
+
+ if (typeof move === 'string') {
+ move_obj = move_from_san(move, sloppy)
+ } else if (typeof move === 'object') {
+ var moves = generate_moves()
+
+ /* convert the pretty move object to an ugly move object */
+ for (var i = 0, len = moves.length; i < len; i++) {
+ if (
+ move.from === algebraic(moves[i].from) &&
+ move.to === algebraic(moves[i].to) &&
+ (!('promotion' in moves[i]) ||
+ move.promotion === moves[i].promotion)
+ ) {
+ move_obj = moves[i]
+ break
+ }
+ }
+ }
+
+ /* failed to find move */
+ if (!move_obj) {
+ return null
+ }
+
+ /* need to make a copy of move because we can't generate SAN after the
+ * move is made
+ */
+ var pretty_move = make_pretty(move_obj)
+
+ make_move(move_obj)
+
+ return pretty_move
+ },
+
+ undo: function() {
+ var move = undo_move()
+ return move ? make_pretty(move) : null
+ },
+
+ clear: function() {
+ return clear()
+ },
+
+ put: function(piece, square) {
+ return put(piece, square)
+ },
+
+ get: function(square) {
+ return get(square)
+ },
+
+ remove: function(square) {
+ return remove(square)
+ },
+
+ perft: function(depth) {
+ return perft(depth)
+ },
+
+ square_color: function(square) {
+ if (square in SQUARES) {
+ var sq_0x88 = SQUARES[square]
+ return (rank(sq_0x88) + file(sq_0x88)) % 2 === 0 ? 'light' : 'dark'
+ }
+
+ return null
+ },
+
+ history: function(options) {
+ var reversed_history = []
+ var move_history = []
+ var verbose =
+ typeof options !== 'undefined' &&
+ 'verbose' in options &&
+ options.verbose
+
+ while (history.length > 0) {
+ reversed_history.push(undo_move())
+ }
+
+ while (reversed_history.length > 0) {
+ var move = reversed_history.pop()
+ if (verbose) {
+ move_history.push(make_pretty(move))
+ } else {
+ move_history.push(move_to_san(move))
+ }
+ make_move(move)
+ }
+
+ return move_history
+ }
+ }
+}
+
+/* export Chess object if using node or any other CommonJS compatible
+ * environment */
+if (typeof exports !== 'undefined') exports.Chess = Chess
+/* export Chess object for any RequireJS compatible environment */
+if (typeof define !== 'undefined')
+ define(function() {
+ return Chess
+ })