Skip to content

feat(classic-games): 新增 Mini Twelve Men's Morris 小游戏#968

Open
wyf027 wants to merge 2 commits into
mainfrom
codex/classic-game-20260709-1506-twelve-mens-morris
Open

feat(classic-games): 新增 Mini Twelve Men's Morris 小游戏#968
wyf027 wants to merge 2 commits into
mainfrom
codex/classic-game-20260709-1506-twelve-mens-morris

Conversation

@wyf027

@wyf027 wyf027 commented Jul 9, 2026

Copy link
Copy Markdown
Owner

Summary

  • 新增 Mini Twelve Men's Morris 单文件 HTML 小游戏
  • 使用 TailwindCSS CDN,无新增依赖或构建流程
  • 玩法:双方各放置 12 枚棋子,成三连后移除对方棋子;放完后沿相邻点移动
  • 轮换说明:nine-men-morris 已用,本次选择同一 Morris 家族的 12 子经典变体,文件名和 PR 元数据未重复

File

  • project/classic-games/20260709-1506-twelve-mens-morris.html

Validation

  • 远端历史扫描:twelve-mens-morris 未出现在最近 PR 标题/body/分支或 main 的 classic-games 文件名中
  • 轻量校验:HTML 结构、Tailwind CDN、关键游戏逻辑标记、内联 JS 语法通过
  • git diff --check -- project/classic-games/20260709-1506-twelve-mens-morris.html 通过
  • 本地临时提交:22d833416dfb0595e8d79eeaf05a8d65feb383cd
  • 远端提交:aaeba41bec89c3d5031b5bbaa687349612f81eb5

Audit

  • AUDIT_DIR=/tmp/git-commit-audit-20260709-150735-35650
  • OVERALL_EXIT=0
  • WARN: 无

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: aaeba41bec

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

<!doctype html>
<html lang="zh-CN"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Mini Twelve Men's Morris</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-4xl flex-col gap-4 px-4 py-5"><header class="flex flex-wrap items-center justify-between gap-3"><div><h1 class="text-2xl font-bold">Mini Twelve Men's Morris</h1><p class="text-sm text-indigo-200">Place twelve pieces each, make mills, remove rivals, then slide along the board.</p></div><button id="newGameBtn" class="rounded bg-fuchsia-300 px-4 py-2 font-semibold text-indigo-950 hover:bg-fuchsia-200">New Game</button></header><section class="grid gap-3 rounded border border-indigo-700 bg-indigo-900 p-3 sm:grid-cols-3"><div>Turn: <b id="turnLabel">White</b></div><div>Phase: <b id="phaseLabel">Place</b></div><div id="message">Choose an empty point.</div></section><section class="grid gap-4 lg:grid-cols-[1fr_220px]"><div id="board" class="mx-auto grid aspect-square w-full max-w-xl grid-cols-7 grid-rows-7 rounded border border-indigo-700 bg-indigo-800 p-3"></div><aside class="rounded border border-indigo-700 bg-indigo-900 p-3 text-sm"><div>White placed: <b id="wPlaced">0</b>/12</div><div>Black placed: <b id="bPlaced">0</b>/12</div><div>White alive: <b id="wAlive">0</b></div><div>Black alive: <b id="bAlive">0</b></div></aside></section></main><script>
const pts=[[0,0],[3,0],[6,0],[1,1],[3,1],[5,1],[2,2],[3,2],[4,2],[0,3],[1,3],[2,3],[4,3],[5,3],[6,3],[2,4],[3,4],[4,4],[1,5],[3,5],[5,5],[0,6],[3,6],[6,6]],mills=[[0,1,2],[3,4,5],[6,7,8],[9,10,11],[12,13,14],[15,16,17],[18,19,20],[21,22,23],[0,9,21],[3,10,18],[6,11,15],[1,4,7],[16,19,22],[8,12,17],[5,13,20],[2,14,23]],adj=[[1,9],[0,2,4],[1,14],[4,10],[1,3,5,7],[4,13],[7,11],[4,6,8],[7,12],[0,10,21],[3,9,11,18],[6,10,15],[8,13,17],[5,12,14,20],[2,13,23],[11,16],[15,17,19],[12,16],[10,19],[16,18,20,22],[13,19],[9,22],[19,21,23],[14,22]];let board,turn,placed,phase,removing,selected,gameOver;const el=id=>document.getElementById(id),boardEl=el('board'),message=el('message'),turnLabel=el('turnLabel'),phaseLabel=el('phaseLabel'),newGameBtn=el('newGameBtn');function enemy(p){return p==='W'?'B':'W'}function name(p){return p==='W'?'White':'Black'}function canFormMill(i,p){return mills.some(m=>m.includes(i)&&m.every(x=>board[x]===p))}function alive(p){return board.filter(x=>x===p).length}function setTurn(p){turn=p;turnLabel.textContent=name(turn)}function next(){selected=null;setTurn(enemy(turn));message.textContent=phase==='place'?'Choose an empty point.':'Select one of your pieces.'}function removable(i){const e=enemy(turn);return board[i]===e&&(!canFormMill(i,e)||board.every((v,j)=>v!==e||canFormMill(j,e)))}function handlePoint(i){if(gameOver)return;if(removing){if(!removable(i)){message.textContent='Pick an opponent piece outside a mill if possible.';return}board[i]='';removing=false;if(phase==='move'&&alive(enemy(turn))<3){message.textContent=`${name(turn)} wins.`;gameOver=true;render();return}next();render();return}if(phase==='place'){if(board[i])return;board[i]=turn;placed[turn]++;if(placed.W===12&&placed.B===12)phase='move';if(canFormMill(i,turn)){removing=true;message.textContent='Mill formed. Remove one opponent piece.'}else next();render();return}if(selected===null){if(board[i]===turn){selected=i;message.textContent='Choose an adjacent empty point.';render()}return}if(i===selected){selected=null;message.textContent='Selection cleared.';render();return}if(!board[i]&&adj[selected].includes(i)){board[i]=turn;board[selected]='';if(canFormMill(i,turn)){removing=true;message.textContent='Mill formed. Remove one opponent piece.'}else next();render()}}function render(){boardEl.innerHTML='';pts.forEach(([c,r],i)=>{const b=document.createElement('button');b.className='m-auto flex h-11 w-11 items-center justify-center rounded-full border-2 '+(selected===i?'border-fuchsia-300 ':'border-indigo-500 ')+(board[i]==='W'?'bg-white text-indigo-950':board[i]==='B'?'bg-indigo-950 text-white':'bg-indigo-700 hover:bg-indigo-600');b.style.gridColumn=c+1;b.style.gridRow=r+1;b.textContent=board[i]||'';b.onclick=()=>handlePoint(i);boardEl.appendChild(b)});phaseLabel.textContent=removing?'Remove':phase==='place'?'Place':'Move';el('wPlaced').textContent=placed.W;el('bPlaced').textContent=placed.B;el('wAlive').textContent=alive('W');el('bAlive').textContent=alive('B')}function newGame(){board=Array(24).fill('');placed={W:0,B:0};phase='place';removing=false;selected=null;gameOver=false;setTurn('W');message.textContent='Choose an empty point.';render()}newGameBtn.addEventListener('click',newGame);newGame();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge End filled boards as draws

When both placed.W and placed.B reach 12 this switches straight to phase='move' unless the last placement formed a mill. If neither player has formed a mill before all 24 points are occupied, there is no empty point to slide into, so the game is left in Move phase with no legal action instead of ending the Twelve Men's Morris filled-board draw.

Useful? React with 👍 / 👎.

<!doctype html>
<html lang="zh-CN"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Mini Twelve Men's Morris</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-4xl flex-col gap-4 px-4 py-5"><header class="flex flex-wrap items-center justify-between gap-3"><div><h1 class="text-2xl font-bold">Mini Twelve Men's Morris</h1><p class="text-sm text-indigo-200">Place twelve pieces each, make mills, remove rivals, then slide along the board.</p></div><button id="newGameBtn" class="rounded bg-fuchsia-300 px-4 py-2 font-semibold text-indigo-950 hover:bg-fuchsia-200">New Game</button></header><section class="grid gap-3 rounded border border-indigo-700 bg-indigo-900 p-3 sm:grid-cols-3"><div>Turn: <b id="turnLabel">White</b></div><div>Phase: <b id="phaseLabel">Place</b></div><div id="message">Choose an empty point.</div></section><section class="grid gap-4 lg:grid-cols-[1fr_220px]"><div id="board" class="mx-auto grid aspect-square w-full max-w-xl grid-cols-7 grid-rows-7 rounded border border-indigo-700 bg-indigo-800 p-3"></div><aside class="rounded border border-indigo-700 bg-indigo-900 p-3 text-sm"><div>White placed: <b id="wPlaced">0</b>/12</div><div>Black placed: <b id="bPlaced">0</b>/12</div><div>White alive: <b id="wAlive">0</b></div><div>Black alive: <b id="bAlive">0</b></div></aside></section></main><script>
const pts=[[0,0],[3,0],[6,0],[1,1],[3,1],[5,1],[2,2],[3,2],[4,2],[0,3],[1,3],[2,3],[4,3],[5,3],[6,3],[2,4],[3,4],[4,4],[1,5],[3,5],[5,5],[0,6],[3,6],[6,6]],mills=[[0,1,2],[3,4,5],[6,7,8],[9,10,11],[12,13,14],[15,16,17],[18,19,20],[21,22,23],[0,9,21],[3,10,18],[6,11,15],[1,4,7],[16,19,22],[8,12,17],[5,13,20],[2,14,23]],adj=[[1,9],[0,2,4],[1,14],[4,10],[1,3,5,7],[4,13],[7,11],[4,6,8],[7,12],[0,10,21],[3,9,11,18],[6,10,15],[8,13,17],[5,12,14,20],[2,13,23],[11,16],[15,17,19],[12,16],[10,19],[16,18,20,22],[13,19],[9,22],[19,21,23],[14,22]];let board,turn,placed,phase,removing,selected,gameOver;const el=id=>document.getElementById(id),boardEl=el('board'),message=el('message'),turnLabel=el('turnLabel'),phaseLabel=el('phaseLabel'),newGameBtn=el('newGameBtn');function enemy(p){return p==='W'?'B':'W'}function name(p){return p==='W'?'White':'Black'}function canFormMill(i,p){return mills.some(m=>m.includes(i)&&m.every(x=>board[x]===p))}function alive(p){return board.filter(x=>x===p).length}function setTurn(p){turn=p;turnLabel.textContent=name(turn)}function next(){selected=null;setTurn(enemy(turn));message.textContent=phase==='place'?'Choose an empty point.':'Select one of your pieces.'}function removable(i){const e=enemy(turn);return board[i]===e&&(!canFormMill(i,e)||board.every((v,j)=>v!==e||canFormMill(j,e)))}function handlePoint(i){if(gameOver)return;if(removing){if(!removable(i)){message.textContent='Pick an opponent piece outside a mill if possible.';return}board[i]='';removing=false;if(phase==='move'&&alive(enemy(turn))<3){message.textContent=`${name(turn)} wins.`;gameOver=true;render();return}next();render();return}if(phase==='place'){if(board[i])return;board[i]=turn;placed[turn]++;if(placed.W===12&&placed.B===12)phase='move';if(canFormMill(i,turn)){removing=true;message.textContent='Mill formed. Remove one opponent piece.'}else next();render();return}if(selected===null){if(board[i]===turn){selected=i;message.textContent='Choose an adjacent empty point.';render()}return}if(i===selected){selected=null;message.textContent='Selection cleared.';render();return}if(!board[i]&&adj[selected].includes(i)){board[i]=turn;board[selected]='';if(canFormMill(i,turn)){removing=true;message.textContent='Mill formed. Remove one opponent piece.'}else next();render()}}function render(){boardEl.innerHTML='';pts.forEach(([c,r],i)=>{const b=document.createElement('button');b.className='m-auto flex h-11 w-11 items-center justify-center rounded-full border-2 '+(selected===i?'border-fuchsia-300 ':'border-indigo-500 ')+(board[i]==='W'?'bg-white text-indigo-950':board[i]==='B'?'bg-indigo-950 text-white':'bg-indigo-700 hover:bg-indigo-600');b.style.gridColumn=c+1;b.style.gridRow=r+1;b.textContent=board[i]||'';b.onclick=()=>handlePoint(i);boardEl.appendChild(b)});phaseLabel.textContent=removing?'Remove':phase==='place'?'Place':'Move';el('wPlaced').textContent=placed.W;el('bPlaced').textContent=placed.B;el('wAlive').textContent=alive('W');el('bAlive').textContent=alive('B')}function newGame(){board=Array(24).fill('');placed={W:0,B:0};phase='place';removing=false;selected=null;gameOver=false;setTurn('W');message.textContent='Choose an empty point.';render()}newGameBtn.addEventListener('click',newGame);newGame();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Add the Twelve Men's Morris diagonals

The board data here is the orthogonal Nine Men's Morris graph: for example point 0 is only adjacent to 1 and 9, so a slide along the Twelve Men's Morris diagonal from 0 to 3 is rejected. Since this game is advertised as Twelve Men's Morris, the four diagonal lines need to be represented in the adjacency data (and any corresponding mill logic for the chosen ruleset) or legal moves on the variant board cannot be played.

Useful? React with 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant