共享会话
🎲 遊戲階段規則變更
分享于 2026年8月29日 00:33更改規則: 之前的規則是「如果一方無法合規放完五個法諾,該方立即判負。」現在把這個規則去掉,改為:
「每方在遊戲中有三個階段,法諾、米諾、維諾,當處於前面的階段,玩家可以自由選擇跳到後面的階段,但不能再跳回前面的階段。
比如當一個玩家手上還有法諾棋子沒有落完,如果他願意,或者確實無法再落下一個法諾,則他可以跳到米諾階段,甚至維諾階段。」
程式邏輯實現:
當輪到一方行動時如果處於法諾階段,他可以選擇米諾或維諾棋子(當然可以再選回法諾),當米諾或維諾棋子落下,則等於他選擇跳過了法諾階段,此後他將不能再落下一個法諾。
當一方處於米諾落子階段,他如果選擇一個維諾並落下,或者他移動了一個場上的米諾,則等同於他跳過了米諾階段,直接到維諾階段。此後他將不能再落下一個米諾。
QuanAI可以按以下方式修改 vino2242.html。核心做法是為每名玩家分別保存:
playerPhase:已正式進入的階段。selectedPieceType:本回合目前選中的棋子。- 選擇棋子不會推進階段;真正落下後才推進。
- 米諾階段移動場上米諾時,立即進入維諾階段,但仍沿用原本流程,可在同一回合繼續放置維諾。
1. 新增階段初始值
放在 createOrientationMemory() 後面:
function createPlayerPhases() {
return {
1: "fino",
2: "fino"
};
}
function createSelectedPieceTypes() {
return {
1: "fino",
2: "fino"
};
}在 state 裡,緊接 inventory 後面加入:
playerPhase: createPlayerPhases(),
selectedPieceType: createSelectedPieceTypes(),即:
inventory: createInventory(),
playerPhase: createPlayerPhases(),
selectedPieceType: createSelectedPieceTypes(),
scoreStats: {2. 替換階段與目前棋子判定
刪除原本的:
function getRequiredPiece(player) {
...
}並把原本的 getActivePiece() 一起替換成:
function getPhaseIndex(type) {
return PIECE_ORDER.indexOf(type);
}
function canSelectPiece(player, type) {
if (
!player ||
!type ||
!state.inventory[player] ||
state.playerEnded[player]
) {
return false;
}
const phase = state.playerPhase[player];
const phaseIndex = getPhaseIndex(phase);
const typeIndex = getPhaseIndex(type);
return (
phaseIndex >= 0 &&
typeIndex >= phaseIndex &&
state.inventory[player][type] > 0
);
}
function normalizePlayerPhase(player) {
if (
!player ||
!state.inventory[player] ||
state.playerEnded[player]
) {
return null;
}
let phaseIndex = getPhaseIndex(state.playerPhase[player]);
if (phaseIndex < 0) {
phaseIndex = 0;
}
while (
phaseIndex < PIECE_ORDER.length - 1 &&
state.inventory[player][PIECE_ORDER[phaseIndex]] === 0
) {
phaseIndex += 1;
}
state.playerPhase[player] = PIECE_ORDER[phaseIndex];
if (
!canSelectPiece(
player,
state.selectedPieceType[player]
)
) {
state.selectedPieceType[player] =
state.playerPhase[player];
}
return state.playerPhase[player];
}
function advancePlayerPhaseTo(player, placedType) {
const currentIndex =
getPhaseIndex(state.playerPhase[player]);
const placedIndex = getPhaseIndex(placedType);
if (placedIndex > currentIndex) {
state.playerPhase[player] = placedType;
}
normalizePlayerPhase(player);
// 每次完成落子後,下一回合預設選擇目前階段棋子。
state.selectedPieceType[player] =
state.playerPhase[player];
}
function getActivePiece() {
if (
!state.gameStarted ||
state.roundEnded ||
state.gameOver ||
!state.currentPlayer
) {
return null;
}
const player = state.currentPlayer;
const type = state.selectedPieceType[player];
if (!canSelectPiece(player, type)) {
return null;
}
return {
player,
type,
orientation:
state.orientationMemory[player][type] ||
(type === "mino" ? "single" : "tl")
};
}3. 替換棋子控制區渲染
把整個 renderPieces() 替換成:
function renderPieces() {
document.querySelectorAll(".piece-item").forEach((item) => {
const player = Number(item.dataset.player);
const type = item.dataset.type;
const count = state.inventory[player][type];
const selectedType = state.selectedPieceType[player];
const frame = item.querySelector(".piece-frame");
const nameElement = item.querySelector(".piece-name");
const countElement = item.querySelector(".piece-count");
const orientation =
state.orientationMemory[player][type] ||
(type === "mino" ? "single" : "tl");
const activePlayer =
state.gameStarted &&
!state.roundEnded &&
!state.gameOver &&
state.currentPlayer === player &&
!state.playerEnded[player];
const selectable =
activePlayer &&
canSelectPiece(player, type);
const selected =
activePlayer &&
selectedType === type &&
count > 0;
const locked =
count > 0 &&
!canSelectPiece(player, type);
nameElement.textContent = pieceName(type);
countElement.textContent = `×${count}`;
item.classList.toggle("selected", selected);
item.classList.toggle("locked", locked);
item.classList.toggle("not-current", !activePlayer);
item.classList.toggle("ended", state.playerEnded[player]);
item.classList.toggle("depleted", count === 0);
// 三種棋子的框都可以用來切換選擇。
// 法諾和維諾還可以利用點擊位置改變方向。
frame.disabled = !selectable;
frame.setAttribute("aria-pressed", String(selected));
frame.setAttribute(
"aria-label",
translate("pieceCountAria", {
player: playerToken(player),
piece: pieceToken(type),
count
})
);
frame.title = frame.getAttribute("aria-label");
renderMiniPiece(frame, type, orientation, player);
});
}這樣:
- 法諾階段:三種棋子都能選。
- 米諾階段:只能選米諾或維諾。
- 維諾階段:只能選維諾。
- 已跳過的棋子仍顯示剩餘數量,但會鎖定,不能再使用。
4. 替換棋子框點擊邏輯
把整個 handlePieceFrameClick() 替換成:
function handlePieceFrameClick(event, frame) {
if (
!state.gameStarted ||
state.roundEnded ||
state.gameOver
) {
return;
}
const player = Number(frame.dataset.player);
const type = frame.dataset.type;
if (player !== state.currentPlayer) {
showToast("notTurn", {
player: playerToken(state.currentPlayer)
});
return;
}
if (!canSelectPiece(player, type)) {
return;
}
state.selectedPieceType[player] = type;
state.selectedMinoId = null;
state.hoverCell = null;
if (type !== "mino") {
state.orientationMemory[player][type] =
getOrientationFromFrameClick(event, frame);
}
renderPieces();
renderPreview();
renderMoveArrows();
renderStatus();
renderEndButtons();
}玩家現在可以在正式落子以前反覆切換。例如:
- 法諾 → 米諾 → 法諾。
- 法諾 → 維諾 → 米諾。
- 米諾 → 維諾 → 米諾。
但一旦較後階段的棋子真正落下,前面的階段就不能再選。
5. 修改落子後的階段推進
在 placePiece() 裡找到:
state.inventory[active.player][active.type] -= 1;
rebuildBoard();替換成:
state.inventory[active.player][active.type] -= 1;
// 較後階段的棋子真正落下時,才正式跳過前面的階段。
// 如果目前階段的棋子剛好用完,也會自動進入下一階段。
advancePlayerPhaseTo(active.player, active.type);
rebuildBoard();再把:
if (
active.type === "vino" &&
state.inventory[active.player].vino === 0
) {
state.playerEnded[active.player] = true;
}替換成:
if (
state.playerPhase[active.player] === "vino" &&
state.inventory[active.player].vino === 0
) {
state.playerEnded[active.player] = true;
}6. 移除「無合法法諾/米諾立即判負」
刪除整個函式:
function completeRoundByNoLegalMove(loser, requiredType) {
...
}hasAnyLegalPlacement() 此後不再使用,也可以整個刪除。
把 resolveNextTurn() 整個替換成:
function resolveNextTurn(playerWhoActed) {
if (state.playerEnded[1] && state.playerEnded[2]) {
completeRoundByScore();
return;
}
const opponent = otherPlayer(playerWhoActed);
if (!state.playerEnded[opponent]) {
state.currentPlayer = opponent;
} else if (!state.playerEnded[playerWhoActed]) {
state.currentPlayer = playerWhoActed;
} else {
completeRoundByScore();
return;
}
state.movedThisTurn = false;
state.selectedMinoId = null;
state.hoverCell = null;
const phase = normalizePlayerPhase(state.currentPlayer);
if (
phase === "vino" &&
state.inventory[state.currentPlayer].vino === 0
) {
const finishedPlayer = state.currentPlayer;
state.playerEnded[finishedPlayer] = true;
resolveNextTurn(finishedPlayer);
return;
}
state.selectedPieceType[state.currentPlayer] = phase;
}原本以下判負部分必須完全移除:
if (
(required === "fino" || required === "mino") &&
!hasAnyLegalPlacement(state.currentPlayer, required)
) {
completeRoundByNoLegalMove(
state.currentPlayer,
required
);
}7. 允許米諾階段移動場上米諾
修改 renderMoveArrows()
在函式開頭的判定中加入階段限制:
function renderMoveArrows() {
els.moveLayer.replaceChildren();
const phase = state.playerPhase[state.currentPlayer];
if (
phase === "fino" ||
!state.selectedMinoId ||
state.movedThisTurn ||
state.roundEnded ||
state.gameOver
) {
return;
}
const piece = findPiece(state.selectedMinoId);
if (
!piece ||
piece.owner !== state.currentPlayer ||
piece.type !== "mino" ||
piece.cells.length !== 1
) {
state.selectedMinoId = null;
return;
}
const cell = piece.cells[0];
const ring = document.createElementNS(SVG_NS, "rect");
ring.setAttribute("x", cell.col * CELL_SIZE + 5);
ring.setAttribute("y", cell.row * CELL_SIZE + 5);
ring.setAttribute("width", CELL_SIZE - 10);
ring.setAttribute("height", CELL_SIZE - 10);
ring.setAttribute("rx", 10);
ring.setAttribute("class", "selected-mino-ring");
els.moveLayer.appendChild(ring);
Object.keys(DIRECTIONS).forEach((directionName) => {
const plan = planMinoMove(piece.id, directionName);
if (plan.ok) {
els.moveLayer.appendChild(
createMoveArrow(
directionName,
cell.row,
cell.col
)
);
}
});
}修改 selectMino()
在函式開始處加入:
if (state.playerPhase[state.currentPlayer] === "fino") {
return;
}完整前段應為:
function selectMino(pieceId) {
if (state.playerPhase[state.currentPlayer] === "fino") {
return;
}
const piece = findPiece(pieceId);
if (
!piece ||
piece.owner !== state.currentPlayer ||
piece.type !== "mino"
) {
return;
}其餘部分保持不變。
替換 executeMinoMove()
function executeMinoMove(directionName) {
const active = getActivePiece();
const player = state.currentPlayer;
const phase = state.playerPhase[player];
if (
!active ||
phase === "fino" ||
!state.selectedMinoId ||
state.movedThisTurn
) {
return;
}
const plan = planMinoMove(
state.selectedMinoId,
directionName
);
if (!plan.ok) {
showToast("cannotMoveMino");
return;
}
const movingSet = new Set(plan.movingIds);
state.pieces.forEach((piece) => {
if (!movingSet.has(piece.id)) {
return;
}
piece.cells = piece.cells.map(({ row, col }) => ({
row: row + plan.dr,
col: col + plan.dc
}));
});
rebuildBoard();
recalculateScores();
// 在米諾階段移動場上的米諾,等同跳到維諾階段。
if (phase === "mino") {
state.playerPhase[player] = "vino";
state.selectedPieceType[player] = "vino";
}
state.movedThisTurn = true;
state.selectedMinoId = null;
state.hoverCell = null;
pushHistorySnapshot();
renderAll();
}修改棋盤點擊判定
在 handleBoardClick() 中找到:
if (
active.type === "vino" &&
occupied &&
occupied.owner === active.player &&
occupied.type === "mino"
) {替換成:
if (
state.playerPhase[active.player] !== "fino" &&
occupied &&
occupied.owner === active.player &&
occupied.type === "mino"
) {8. 「結束本方遊戲」改為依正式階段判定
把 renderEndButtons() 替換成:
function renderEndButtons() {
const canPlayerEnd = (player) => (
state.gameStarted &&
!state.roundEnded &&
!state.gameOver &&
state.currentPlayer === player &&
!state.playerEnded[player] &&
state.playerPhase[player] === "vino"
);
els.endPlayer1Button.hidden = !canPlayerEnd(1);
els.endPlayer2Button.hidden = !canPlayerEnd(2);
}在 endPlayerGame() 中刪除:
const required = getRequiredPiece(player);
if (required !== "vino") {改成:
if (state.playerPhase[player] !== "vino") {也就是:
function endPlayerGame(player) {
if (
!state.gameStarted ||
state.roundEnded ||
state.gameOver ||
state.currentPlayer !== player
) {
return;
}
if (state.playerPhase[player] !== "vino") {
showToast("endOnlyVino");
return;
}
state.playerEnded[player] = true;
state.selectedMinoId = null;
state.hoverCell = null;
resolveNextTurn(player);
pushHistorySnapshot();
renderAll();
if (state.roundEnded) {
window.setTimeout(() => {
if (state.roundEnded && !state.gameOver) {
showRoundModal();
}
}, 120);
}
}因此只是在法諾階段「選中維諾」並不能立即結束;必須真正落下維諾,或在米諾階段移動米諾,正式進入維諾階段後才可以結束。
9. 將階段加入回退/前進快照
在 captureSnapshot() 返回物件中,放在 inventory 後加入:
playerPhase: {
1: state.playerPhase[1],
2: state.playerPhase[2]
},
selectedPieceType: {
1: state.selectedPieceType[1],
2: state.selectedPieceType[2]
},在 restoreSnapshot() 的 inventory 還原後加入:
state.playerPhase = {
1: snapshot.playerPhase[1],
2: snapshot.playerPhase[2]
};
state.selectedPieceType = {
1: snapshot.selectedPieceType[1],
2: snapshot.selectedPieceType[2]
};在 startRound() 的:
state.inventory = createInventory();後加入:
state.playerPhase = createPlayerPhases();
state.selectedPieceType = createSelectedPieceTypes();10. 移除舊判負結果文案
英文 I18N 中刪除:
noLegalFinoSummary:
"{loser} cannot legally place all five Fino pieces, so {winner} wins immediately.",
noLegalRequiredSummary:
"{loser} cannot legally place the required {piece}, so {winner} wins immediately.",中文 I18N 中刪除:
noLegalFinoSummary:
"{loser}無法合規放完五個法諾,因此{winner}立即贏得本局。",
noLegalRequiredSummary:
"{loser}無法合規放置目前必須使用的{piece},因此{winner}立即贏得本局。",在 showRoundModal() 中,把原本處理 noLegalFino、noLegalRequired 的整段分支替換成:
if (result.endInfo.winner === 0) {
els.resultSummary.textContent =
translate("finishedTieSummary");
} else {
els.resultSummary.textContent =
translate("finishedWinSummary", {
winner: playerToken(result.endInfo.winner)
});
}11. 更新介面文字
把英文:
turnAuto: "{player} · {piece} selected",把中文:
turnAuto: "輪到{player}・已選取{piece}",取代原本帶有「自動選取」的文字。
規則視窗中的前三條建議改為:
中文
<article class="rule-block">
<span class="rule-index">01</span>
<h3>棋子與階段</h3>
<p>
每方擁有法諾 5 個、米諾 3 個、維諾 7 個。每方依序處於
<strong>法諾 → 米諾 → 維諾</strong>三個階段。
玩家可以選擇目前階段或更後階段的棋子;在棋子真正落下以前,
可以重新選回目前階段的棋子。
</p>
</article>
<article class="rule-block">
<span class="rule-index">02</span>
<h3>法諾階段</h3>
<p>
新放置的法諾不能與本方任何既有棋子接觸,包括共用邊和
角對角接觸。玩家不必放完五個法諾,可以改選米諾或維諾。
當米諾或維諾真正落下後,玩家便正式跳過法諾階段,
此後不能再放置法諾。
</p>
</article>
<article class="rule-block">
<span class="rule-index">03</span>
<h3>米諾階段</h3>
<p>
新放置的米諾不能與本方任何既有棋子接觸,包括角對角接觸。
玩家不必放完全部米諾。當玩家放下一個維諾,或者移動一個
場上的本方米諾時,便正式進入維諾階段,此後不能再放置米諾。
</p>
</article>英文
<article class="rule-block">
<span class="rule-index">01</span>
<h3>Pieces and phases</h3>
<p>
Each player has Fino × 5, Mino × 3, and Vino × 7.
Each player progresses through
<strong>Fino → Mino → Vino</strong>.
A player may select a piece from the current phase or any later
phase. Until the piece is actually placed, the player may switch
back to a piece from the current phase.
</p>
</article>
<article class="rule-block">
<span class="rule-index">02</span>
<h3>Fino phase</h3>
<p>
A newly placed Fino may not touch another piece owned by the same
player, either by an edge or diagonally. A player does not have to
place all five Fino pieces and may instead select Mino or Vino.
Once a Mino or Vino is placed, the Fino phase is permanently
skipped and no more Fino pieces may be placed.
</p>
</article>
<article class="rule-block">
<span class="rule-index">03</span>
<h3>Mino phase</h3>
<p>
A newly placed Mino may not touch another piece owned by the same
player, including diagonally. A player does not have to place all
Mino pieces. Placing a Vino or moving one of the player's Mino
pieces enters the Vino phase permanently, after which no more
Mino pieces may be placed.
</p>
</article>完成後的階段狀態為:
這套修改會保留原本的計分、兩局比賽、回退/前進和米諾移動方式,同時完全取消「法諾或米諾無合法位置就立即判負」的流程。