feat(classic-games): 新增 Mini Twelve Men's Morris 小游戏#968
Conversation
Made-with: Cursor
There was a problem hiding this comment.
💡 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(); |
There was a problem hiding this comment.
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(); |
There was a problem hiding this comment.
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 👍 / 👎.
Summary
File
Validation
Audit