Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
277 changes: 277 additions & 0 deletions project/classic-games/20260704-2245-leopards-and-cows.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,277 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Mini Leopards and Cows</title>
<script src="https://cdn.tailwindcss.com"></script>
</head>
<body class="min-h-screen bg-stone-950 text-stone-100">
<main class="mx-auto flex min-h-screen max-w-5xl flex-col gap-5 px-4 py-6">
<header class="flex flex-col gap-3 border-b border-stone-800 pb-4 sm:flex-row sm:items-end sm:justify-between">
<div>
<p class="text-sm uppercase tracking-[0.22em] text-lime-300">Classic Game</p>
<h1 class="text-3xl font-bold">Mini Leopards and Cows</h1>
</div>
<button id="resetBtn" class="rounded bg-lime-300 px-4 py-2 text-sm font-semibold text-stone-950 hover:bg-lime-200">
重新开始
</button>
</header>

<section class="grid gap-5 lg:grid-cols-[1fr_19rem]">
<div class="rounded border border-stone-800 bg-stone-900 p-4">
<div id="board" class="mx-auto grid aspect-square w-full max-w-[590px] grid-cols-5 gap-2"></div>
</div>

<aside class="space-y-4">
<div class="rounded border border-stone-800 bg-stone-900 p-4">
<p class="text-sm text-stone-400">当前回合</p>
<p id="turnText" class="mt-1 text-2xl font-bold text-lime-200">牛群</p>
<p id="statusText" class="mt-2 text-sm leading-6 text-stone-300"></p>
</div>
<div class="grid grid-cols-2 gap-3">
<div class="rounded border border-stone-800 bg-stone-900 p-4">
<p class="text-sm text-stone-400">牛群</p>
<p id="cowStats" class="mt-1 text-lg font-semibold"></p>
</div>
<div class="rounded border border-stone-800 bg-stone-900 p-4">
<p class="text-sm text-stone-400">豹子</p>
<p id="leopardStats" class="mt-1 text-lg font-semibold"></p>
</div>
</div>
<div class="rounded border border-stone-800 bg-stone-900 p-4 text-sm leading-6 text-stone-300">
<p class="font-semibold text-stone-100">规则</p>
<p class="mt-2">牛群先放完 12 子,然后移动一格围堵。豹子从角落出发,可移动一格,也可跳过相邻牛吃掉它。牛困住两只豹子获胜,豹子吃掉 5 头牛获胜。</p>
</div>
</aside>
</section>
</main>

<script>
const boardEl = document.getElementById("board");
const resetBtn = document.getElementById("resetBtn");
const turnText = document.getElementById("turnText");
const statusText = document.getElementById("statusText");
const cowStats = document.getElementById("cowStats");
const leopardStats = document.getElementById("leopardStats");
const size = 5;
const cowTotal = 12;
const eatGoal = 5;
let board;
let turn;
let cowsToPlace;
let cowsCaptured;
let selected;
let winner;
let message;

function resetGame() {
board = Array(size * size).fill(null);
board[0] = "L";
board[4] = "L";
turn = "C";
cowsToPlace = cowTotal;
cowsCaptured = 0;
selected = null;
winner = null;
message = "牛群先在空格中布子。";
render();
}

function sideName(side) {
return side === "C" ? "牛群" : "豹子";
}

function isPlacementPhase() {
return cowsToPlace > 0;
}

function rowCol(index) {
return [Math.floor(index / size), index % size];
}

function indexAt(row, col) {
return row * size + col;
}

function inside(row, col) {
return row >= 0 && row < size && col >= 0 && col < size;
}

function directions() {
return [
[-1, 0],
[1, 0],
[0, -1],
[0, 1],
[-1, -1],
[-1, 1],
[1, -1],
[1, 1],
];
}

function adjacentMoves(index) {
const [row, col] = rowCol(index);
return directions()
.map(([dr, dc]) => [row + dr, col + dc])
.filter(([nextRow, nextCol]) => inside(nextRow, nextCol))
.map(([nextRow, nextCol]) => indexAt(nextRow, nextCol))
.filter((next) => board[next] === null);
}

function leopardMoves(index) {
const [row, col] = rowCol(index);
const moves = adjacentMoves(index).map((to) => ({ to, capture: null }));
directions().forEach(([dr, dc]) => {
const midRow = row + dr;
const midCol = col + dc;
const landRow = row + dr * 2;
const landCol = col + dc * 2;
if (!inside(midRow, midCol) || !inside(landRow, landCol)) return;
const mid = indexAt(midRow, midCol);
const land = indexAt(landRow, landCol);
if (board[mid] === "C" && board[land] === null) moves.push({ to: land, capture: mid });
});
return moves;
}

function cowMoves(index) {
return adjacentMoves(index).map((to) => ({ to, capture: null }));
}

function legalMovesFrom(index) {
if (board[index] === "L") return leopardMoves(index);
if (board[index] === "C") return cowMoves(index);
return [];
}

function allMoves(side) {
const moves = [];
board.forEach((piece, index) => {
if (piece === side) legalMovesFrom(index).forEach((move) => moves.push({ from: index, ...move }));
});
return moves;
}

function checkWin() {
if (cowsCaptured >= eatGoal) {
winner = "L";
message = "豹子吃掉 5 头牛,豹子获胜。";
} else if (!isPlacementPhase() && allMoves("L").length === 0) {
winner = "C";
message = "豹子无路可走,牛群获胜。";
} else if (!isPlacementPhase() && allMoves("C").length === 0) {
winner = "L";
message = "牛群无法移动,豹子获胜。";
}
}

function finishTurn() {
checkWin();
if (!winner) {
turn = turn === "C" ? "L" : "C";
message = isPlacementPhase() && turn === "C" ? "继续布置牛群。" : `选择一枚${sideName(turn)}棋子。`;
}
selected = null;
render();
}

function placeCow(index) {
if (board[index] !== null) {
message = "请选择空格放牛。";
render();
return;
}
board[index] = "C";
cowsToPlace -= 1;
finishTurn();
}

function movePiece(index) {
if (selected === null) {
if (board[index] === turn) {
selected = index;
message = "选择高亮空格移动。";
} else {
message = `请选择${sideName(turn)}棋子。`;
}
render();
return;
}
if (index === selected) {
selected = null;
message = "已取消选择。";
render();
return;
}
if (board[index] === turn) {
selected = index;
message = "已切换棋子。";
render();
return;
}
const move = legalMovesFrom(selected).find((item) => item.to === index);
if (!move) {
message = "只能走到高亮空格。";
render();
return;
}
board[index] = turn;
board[selected] = null;
if (move.capture !== null) {
board[move.capture] = null;
cowsCaptured += 1;
}
finishTurn();
}

function handleCell(index) {
if (winner) return;
if (isPlacementPhase() && turn === "C") {
placeCow(index);
} else if (isPlacementPhase() && turn === "L") {
movePiece(index);
} else {
movePiece(index);
}
}

function renderCell(index) {
const value = board[index];
const legal = selected !== null && legalMovesFrom(selected).some((move) => move.to === index);
const cell = document.createElement("button");
cell.type = "button";
cell.setAttribute("aria-label", `格子 ${index + 1}`);
cell.className = [
"aspect-square rounded border text-center transition",
selected === index ? "border-lime-300 bg-lime-300/20" : "border-stone-700 bg-stone-950",
legal ? "ring-2 ring-lime-300" : "",
!value ? "hover:bg-stone-800" : "",
].join(" ");
if (value) {
const piece = document.createElement("span");
piece.textContent = value === "L" ? "豹" : "牛";
piece.className = value === "L"
? "mx-auto flex h-12 w-12 items-center justify-center rounded-full bg-amber-300 font-bold text-stone-950"
: "mx-auto flex h-10 w-10 items-center justify-center rounded-full bg-stone-100 font-bold text-stone-950";
cell.appendChild(piece);
}
cell.addEventListener("click", () => handleCell(index));
return cell;
}

function render() {
boardEl.innerHTML = "";
board.forEach((_, index) => boardEl.appendChild(renderCell(index)));
turnText.textContent = winner ? `${sideName(winner)}胜利` : sideName(turn);
statusText.textContent = message;
cowStats.textContent = `${board.filter((piece) => piece === "C").length} 子 / 待放 ${cowsToPlace}`;
leopardStats.textContent = `${board.filter((piece) => piece === "L").length} 子 / 已吃 ${cowsCaptured}`;
}

resetBtn.addEventListener("click", resetGame);
resetGame();
</script>
</body>
</html>