-
Notifications
You must be signed in to change notification settings - Fork 2
feat(classic-games): 新增 Mini Treblecross 小游戏 #1221
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
wyf027
wants to merge
1
commit into
main
Choose a base branch
from
codex/classic-game-20260717-2224-treblecross-lite
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
180 changes: 180 additions & 0 deletions
180
project/classic-games/20260717-2224-treblecross-lite.html
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,180 @@ | ||
| <!DOCTYPE html> | ||
| <html lang="zh-CN"> | ||
| <head> | ||
| <meta charset="UTF-8"> | ||
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | ||
| <title>Mini Treblecross</title> | ||
| <script src="https://cdn.tailwindcss.com"></script> | ||
| </head> | ||
| <body class="min-h-screen bg-indigo-950 text-indigo-50"> | ||
| <main class="mx-auto flex min-h-screen max-w-5xl flex-col justify-center px-4 py-8"> | ||
| <section class="rounded-lg border border-indigo-800 bg-indigo-900 p-5 shadow-2xl shadow-black/30"> | ||
| <div class="mb-5 flex flex-col gap-3 sm:flex-row sm:items-end sm:justify-between"> | ||
| <div> | ||
| <p class="text-sm uppercase tracking-[0.18em] text-rose-300">Classic Pencil Game</p> | ||
| <h1 class="text-3xl font-semibold text-white">Mini Treblecross</h1> | ||
| </div> | ||
| <button id="resetBtn" class="rounded-md bg-rose-400 px-4 py-2 text-sm font-semibold text-indigo-950 hover:bg-rose-300">重开</button> | ||
| </div> | ||
|
|
||
| <div class="grid gap-4 lg:grid-cols-[1fr_250px]"> | ||
| <div class="rounded-lg border border-indigo-700 bg-indigo-950 p-4"> | ||
| <div id="board" class="grid grid-cols-12 gap-2"></div> | ||
| </div> | ||
| <aside class="rounded-lg border border-indigo-700 bg-indigo-950 p-4"> | ||
| <div class="grid grid-cols-3 gap-3 text-sm"> | ||
| <div> | ||
| <p class="text-indigo-300">你</p> | ||
| <p id="humanCount" class="text-2xl font-semibold text-white">0</p> | ||
| </div> | ||
| <div> | ||
| <p class="text-indigo-300">AI</p> | ||
| <p id="aiCount" class="text-2xl font-semibold text-white">0</p> | ||
| </div> | ||
| <div> | ||
| <p class="text-indigo-300">空位</p> | ||
| <p id="emptyCount" class="text-2xl font-semibold text-white">12</p> | ||
| </div> | ||
| </div> | ||
| <p id="message" class="mt-4 min-h-16 rounded-md bg-indigo-900 p-3 text-sm leading-6 text-indigo-100"></p> | ||
| <div class="mt-4 space-y-2 text-sm text-indigo-200"> | ||
| <p>你和 AI 都在同一条线上放 X。</p> | ||
| <p>谁的落子让任意连续 3 格都变成 X,谁立刻获胜。</p> | ||
| <p>绿色提示代表该位置可立即成三。</p> | ||
| </div> | ||
| </aside> | ||
| </div> | ||
| </section> | ||
| </main> | ||
|
|
||
| <script> | ||
| const length = 12; | ||
| let boardState = Array(length).fill(""); | ||
| let locked = false; | ||
| let gameOver = false; | ||
|
|
||
| const boardEl = document.getElementById("board"); | ||
| const messageEl = document.getElementById("message"); | ||
| const humanCountEl = document.getElementById("humanCount"); | ||
| const aiCountEl = document.getElementById("aiCount"); | ||
| const emptyCountEl = document.getElementById("emptyCount"); | ||
| const resetBtn = document.getElementById("resetBtn"); | ||
|
|
||
| function emptyCells(state = boardState) { | ||
| return state.flatMap((piece, index) => piece === "" ? [index] : []); | ||
| } | ||
|
|
||
| function hasTriple(state = boardState) { | ||
| for (let index = 0; index <= state.length - 3; index += 1) { | ||
| if (state[index] && state[index + 1] && state[index + 2]) return true; | ||
| } | ||
| return false; | ||
| } | ||
|
|
||
| function wouldWin(index, state = boardState) { | ||
| const next = [...state]; | ||
| next[index] = "X"; | ||
| return hasTriple(next); | ||
| } | ||
|
|
||
| function countOwner(owner) { | ||
| return boardState.filter((piece) => piece === owner).length; | ||
| } | ||
|
|
||
| function dangerScore(index) { | ||
| let score = 0; | ||
| for (let start = index - 2; start <= index; start += 1) { | ||
| if (start < 0 || start + 2 >= length) continue; | ||
| const cells = [start, start + 1, start + 2]; | ||
| const filled = cells.filter((cell) => boardState[cell] !== "").length; | ||
| if (cells.includes(index)) score += filled; | ||
| } | ||
| return score; | ||
| } | ||
|
|
||
| function chooseAiMove() { | ||
| const options = emptyCells(); | ||
| const winning = options.find((index) => wouldWin(index)); | ||
| if (winning !== undefined) return winning; | ||
| return options | ||
| .map((index) => ({ index, score: dangerScore(index) + (5.5 - Math.abs(5.5 - index)) * 0.1 })) | ||
| .sort((left, right) => right.score - left.score)[0].index; | ||
| } | ||
|
|
||
| function placeMark(index, owner) { | ||
| if (boardState[index] !== "" || gameOver) return; | ||
| boardState[index] = owner; | ||
| render(); | ||
| if (hasTriple()) { | ||
| gameOver = true; | ||
| locked = true; | ||
| setMessage(owner === "H" ? "你完成连续三个 X,获胜!" : "AI 完成连续三个 X,点击重开再试。"); | ||
| return; | ||
| } | ||
| if (!emptyCells().length) { | ||
| gameOver = true; | ||
| locked = true; | ||
| setMessage("棋盘已满,没有人完成三连,平局。"); | ||
| return; | ||
| } | ||
| if (owner === "H") { | ||
| locked = true; | ||
| setMessage("AI 思考中..."); | ||
| setTimeout(playComputerTurn, 420); | ||
| } else { | ||
| locked = false; | ||
| setMessage("轮到你选择一个空格。"); | ||
| } | ||
| } | ||
|
|
||
| function chooseCell(index) { | ||
| if (locked || gameOver) return; | ||
| placeMark(index, "H"); | ||
| } | ||
|
|
||
| function playComputerTurn() { | ||
| if (gameOver) return; | ||
| placeMark(chooseAiMove(), "A"); | ||
| } | ||
|
|
||
| function setMessage(text) { | ||
| messageEl.textContent = text; | ||
| } | ||
|
|
||
| function startGame() { | ||
| boardState = Array(length).fill(""); | ||
| locked = false; | ||
| gameOver = false; | ||
| render(); | ||
| setMessage("选择一个空格放下 X。"); | ||
| } | ||
|
|
||
| function render() { | ||
| boardEl.innerHTML = ""; | ||
| boardState.forEach((piece, index) => { | ||
| const cell = document.createElement("button"); | ||
| const previewWin = piece === "" && wouldWin(index); | ||
| cell.type = "button"; | ||
| cell.className = [ | ||
| "aspect-square min-h-12 rounded-md border text-2xl font-bold transition", | ||
| piece === "H" ? "border-cyan-300 bg-cyan-400 text-indigo-950" : "", | ||
| piece === "A" ? "border-rose-300 bg-rose-400 text-indigo-950" : "", | ||
| piece === "" ? "border-indigo-700 bg-indigo-900 hover:border-white" : "", | ||
| previewWin ? "ring-2 ring-emerald-300" : "", | ||
| locked || gameOver ? "cursor-default" : "cursor-pointer" | ||
| ].join(" "); | ||
| cell.textContent = piece ? "X" : previewWin ? "!" : ""; | ||
| cell.setAttribute("aria-label", `cell ${index + 1}`); | ||
| cell.addEventListener("click", () => chooseCell(index)); | ||
| boardEl.appendChild(cell); | ||
| }); | ||
| humanCountEl.textContent = countOwner("H"); | ||
| aiCountEl.textContent = countOwner("A"); | ||
| emptyCountEl.textContent = emptyCells().length; | ||
| } | ||
|
|
||
| resetBtn.addEventListener("click", startGame); | ||
| startGame(); | ||
| </script> | ||
| </body> | ||
| </html> | ||
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When the player clicks 重开 during the 420 ms AI-thinking window, this timeout is still queued;
startGame()resetsgameOvertofalse, so the oldplayComputerTurnruns against the fresh board and lets the AI place an unexpected first move. Repeating quick reset/move cycles can also leave multiple stale AI turns queued for the new game, so the reset button does not reliably start from an empty player-to-move state.Useful? React with 👍 / 👎.