共享会话
增加規則
分享于 2026年8月30日 23:04增加規則 當一方處於法諾放置階段,如果場上存在至少一個法諾,他就可以選擇跳到米諾放置階段。 當一方處於米諾放置階段,如果場上存在至少一個米諾,他就可以選擇跳到維諾放置階段。 當一方處於維諾放置階段,如果場上存在至少一個維諾,他就可以選擇飛走一個米諾以結束本局遊戲。 當一方在某階段,如果連一個棋子都無法放置,則直接判負(兩局的總勝負)。
程式實現:
當一方在法諾放置階段,如果場上已經至少有一個他的法諾,則他可以點擊米諾棋子使之選中,在落子前他可以再選回法諾,但當他落下一個米諾時,他將無法再選中法諾。
當一方在米諾放置階段,如果場上已經至少有一個他的米諾,則他可以點擊維諾棋子使之選中,在落子前他可以再選回米諾,但當他落下一個維諾時,他將無法再選中米諾。當他移動一個場上的米諾時,棋子選中將自動切換到維諾。
明確給出所要修改的地方和所要替換的代碼。
QuanAI以下修改以這個判定為準:
- 「無法放置」指:該階段仍有該類棋子庫存,但棋盤上沒有任何合法落點。
- 如果維諾已全部成功放完,仍可依「場上已有自己的維諾」規則飛走米諾,不會因庫存為 0 自動判負。
- 因無合法落點而判負時,直接決定兩局總勝負,不再進入下一局,也不比較分數。
一、增加新的翻譯文字
在 I18N.en 中,找到:
phaseVinoToFlight:
'{player} cannot place another Vino, even after a normal Mino move. Mino flight phase begins.',在它後面加入:
turnVinoOnlyFlight: '{player} · select a Mino to fly and finish',
flightNeedsVino:
'Flying a Mino is allowed only during the Vino phase and after you have placed at least one Vino.',
noPlacementDefeat:
'{loser} has no legal {piece} placement and immediately loses the match. {winner} wins.',
noPlacementResultSummary:
'{loser} had at least one {piece} remaining but no legal placement. Under the no-placement rule, {winner} wins the entire two-round match immediately.',
decidedByRule: 'Match decided by the no-placement rule',在 I18N.zh 中,找到:
phaseVinoToFlight: '{player}即使正常移動一個米諾也無法再放置維諾,自動進入米諾飛行階段。',在它後面加入:
turnVinoOnlyFlight: '輪到{player}・請選取一個米諾飛走並結束本方遊戲',
flightNeedsVino: '只有進入維諾放置階段,而且場上已有至少一個本方維諾時,才能飛走米諾。',
noPlacementDefeat: '{loser}已無法合法放置任何一個{piece},直接判負;{winner}贏得兩局總勝負。',
noPlacementResultSummary:
'{loser}仍有{piece}尚未放置,但棋盤上已經沒有任何合法落點。依照無法放置直接判負的規則,{winner}立即贏得整場兩局比賽。',
decidedByRule: '本場由「無法放置直接判負」規則決定',舊的:
phaseFinoToMino
phaseMinoToVino
phaseVinoToFlight可以保留,不會再被新階段判定使用。
二、增加狀態欄位
在 state 裡找到:
selectedMinoId: null,
movedThisTurn: false,
pendingMinoMove: null,
pendingFlight: null,替換成:
selectedMinoId: null,
// 玩家目前在控制區選中的「待放置棋子」。
// null 表示自動使用該玩家目前正式所處的放置階段。
selectedPlacementType: null,
movedThisTurn: false,
pendingMinoMove: null,
pendingFlight: null,再找到:
roundEnded: false,
endInfo: null,
roundResults: [],替換成:
roundEnded: false,
endInfo: null,
roundResults: [],
// 因某階段沒有合法落點而直接輸掉兩局總勝負。
matchForfeit: null,三、替換階段與棋子選擇函式
找到從:
function isMinoFlightPhase(player) {開始,一直到 getActivePiece() 結束的整段程式:
function getActivePiece() {
...
}全部替換成:
function isMinoFlightPhase(player) {
return Boolean(player && state.minoFlightMode[player] && !state.playerEnded[player]);
}
function hasPlacedPiece(player, type) {
return state.pieces.some(piece => piece.owner === player && piece.type === type);
}
/*
* 回傳玩家目前正式所處的放置階段。
*
* phaseSkipped.fino === true:
* 玩家已經實際放下一個米諾,因此不能再選法諾。
*
* phaseSkipped.mino === true:
* 玩家已經實際放下一個維諾,因此不能再選米諾。
*
* 維諾即使庫存為 0,仍維持在維諾階段,
* 以便玩家飛走米諾結束本方遊戲。
*/
function getRequiredPiece(player) {
if (!player || !state.inventory[player] || state.playerEnded[player]) {
return null;
}
if (!state.phaseSkipped[player].fino && state.inventory[player].fino > 0) {
return 'fino';
}
if (!state.phaseSkipped[player].mino && state.inventory[player].mino > 0) {
return 'mino';
}
return 'vino';
}
/*
* 判斷控制區中的某種棋子目前是否可以被選中。
*/
function canSelectPlacementType(player, type) {
if (
!player ||
!state.inventory[player] ||
state.playerEnded[player] ||
state.roundEnded ||
state.gameOver ||
state.inventory[player][type] <= 0
) {
return false;
}
const phase = getRequiredPiece(player);
if (!phase) {
return false;
}
// 正式階段本身永遠可以選回。
if (type === phase) {
return true;
}
/*
* 法諾階段:
* 場上至少已有一個自己的法諾時,可暫時選擇米諾。
*/
if (phase === 'fino' && type === 'mino') {
return hasPlacedPiece(player, 'fino');
}
/*
* 米諾階段:
* 場上至少已有一個自己的米諾時,可暫時選擇維諾。
*/
if (phase === 'mino' && type === 'vino') {
return hasPlacedPiece(player, 'mino');
}
return false;
}
function getSelectedPlacementType(player) {
const phase = getRequiredPiece(player);
if (!phase) {
return null;
}
if (
state.selectedPlacementType &&
canSelectPlacementType(player, state.selectedPlacementType)
) {
return state.selectedPlacementType;
}
return phase;
}
/*
* 主動飛走米諾必須同時符合:
* 1. 正式處於維諾階段;
* 2. 場上至少有一個自己的維諾。
*/
function canVoluntarilyFlyMino(player) {
return (
getRequiredPiece(player) === 'vino' &&
hasPlacedPiece(player, 'vino')
);
}
function getActivePiece() {
if (
!state.gameStarted ||
state.roundEnded ||
state.gameOver ||
!state.currentPlayer ||
isMinoFlightPhase(state.currentPlayer)
) {
return null;
}
const type = getSelectedPlacementType(state.currentPlayer);
if (!type || state.inventory[state.currentPlayer][type] <= 0) {
return null;
}
return {
player: state.currentPlayer,
type,
orientation:
state.orientationMemory[state.currentPlayer][type] ||
(type === 'mino' ? 'single' : 'tl')
};
}四、刪除自動跳階段,改成無法放置直接判負
找到從:
function canContinueVinoPhase(player) {開始,一直到:
function preparePlayerPhase(player) {
...
}結束的整段,包括:
function enterMinoFlightPhase(player) {全部替換成:
function finishMatchByNoPlacement(loser, requiredType) {
const winner = otherPlayer(loser);
state.matchForfeit = {
winner,
loser,
requiredType,
round: state.roundNumber
};
state.gameOver = true;
state.roundEnded = true;
state.currentPlayer = 0;
state.selectedPlacementType = null;
state.selectedMinoId = null;
state.pendingMinoMove = null;
state.pendingFlight = null;
state.movedThisTurn = false;
state.hoverCell = null;
state.endInfo = {
reason: 'no-placement',
winner,
loser,
requiredType
};
hideFlightConfirmDialog();
showToast(
'noPlacementDefeat',
{
loser: playerToken(loser),
winner: playerToken(winner),
piece: pieceToken(requiredType)
},
1800
);
window.setTimeout(() => {
if (state.gameOver && state.matchForfeit) {
showFinalModal();
}
}, 260);
}
function preparePlayerPhase(player) {
if (
!player ||
state.playerEnded[player] ||
state.roundEnded ||
state.gameOver
) {
return;
}
const required = getRequiredPiece(player);
if (!required) {
return;
}
/*
* 維諾已全部成功放完時,不再檢查維諾落點;
* 玩家留在維諾階段,並可飛走一個米諾結束本方遊戲。
*
* 正常情況下,只要維諾全部放完,場上必然已有自己的維諾。
* 如果資料異常,沒有維諾或沒有米諾,則無法完成飛子,直接判負。
*/
if (required === 'vino' && state.inventory[player].vino <= 0) {
if (hasPlacedPiece(player, 'vino') && hasPlacedPiece(player, 'mino')) {
return;
}
finishMatchByNoPlacement(player, 'vino');
return;
}
/*
* 該階段仍有棋子,但整個棋盤沒有任何合法落點:
* 不再自動跳到下一階段,而是直接輸掉兩局總勝負。
*/
if (!hasAnyLegalPlacement(player, required)) {
finishMatchByNoPlacement(player, required);
}
}這一段會完全取消原本的:
- 無法放法諾時自動進入米諾;
- 無法放米諾時自動進入維諾;
- 無法放維諾時自動進入米諾飛行階段。
五、替換棋子控制區的顯示與按鈕啟用邏輯
找到完整的:
function 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 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 inFlightPhase = isMinoFlightPhase(player);
const selectedType = activePlayer
? getSelectedPlacementType(player)
: null;
const selectable =
activePlayer &&
!inFlightPhase &&
canSelectPlacementType(player, type);
/*
* 選取場上米諾準備移動時,暫時不把維諾控制框畫成選中,
* 直到米諾移動完成或取消選取。
*/
const blockedByBoardMinoSelection =
activePlayer &&
type === 'vino' &&
state.selectedMinoId !== null;
const selected =
selectable &&
selectedType === type &&
!blockedByBoardMinoSelection;
const locked = count > 0 && !selectable;
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 || blockedByBoardMinoSelection;
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);
});
}六、替換棋子控制區的點擊函式
找到完整的:
function handlePieceFrameClick(event, frame) {
...
}替換成:
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 (!canSelectPlacementType(player, type)) {
return;
}
/*
* 在法諾與米諾之間,以及米諾與維諾之間,
* 只改變暫時選中的棋子,不立即提交階段跳轉。
*/
state.selectedPlacementType = type;
state.hoverCell = null;
/*
* 法諾和維諾仍然可以透過點擊控制框的四個象限改變方向。
* 米諾沒有方向,因此只切換選中狀態。
*/
if (type !== 'mino') {
const orientation = getOrientationFromFrameClick(event, frame);
state.orientationMemory[player][type] = orientation;
}
renderPieces();
renderPreview();
renderMoveArrows();
renderStatus();
}七、落下米諾或維諾時,正式提交階段跳轉
找到完整的:
function placePiece(validation, active) {
...
}替換成:
function placePiece(validation, active) {
/*
* active.type 是目前暫時選中的棋子;
* committedPhase 是落子前正式所處的階段。
*/
const committedPhase = getRequiredPiece(active.player);
const piece = {
id: state.nextPieceId,
owner: active.player,
type: active.type,
orientation: active.orientation,
cells: validation.cells.map(cell => ({ ...cell }))
};
state.nextPieceId += 1;
state.pieces.push(piece);
state.inventory[active.player][active.type] -= 1;
/*
* 法諾階段實際落下一個米諾:
* 從此正式進入米諾階段,不能再選回法諾。
*/
if (committedPhase === 'fino' && active.type === 'mino') {
state.phaseSkipped[active.player].fino = true;
}
/*
* 米諾階段實際落下一個維諾:
* 從此正式進入維諾階段,不能再選回米諾。
*/
if (committedPhase === 'mino' && active.type === 'vino') {
state.phaseSkipped[active.player].mino = true;
}
rebuildBoard();
recalculateScores();
state.hoverCell = null;
state.selectedMinoId = null;
state.selectedPlacementType = null;
state.pendingMinoMove = null;
state.movedThisTurn = false;
resolveNextTurn(active.player);
pushHistorySnapshot();
renderAll();
if (state.gameOver) {
return;
}
if (state.roundEnded) {
window.setTimeout(() => {
if (state.roundEnded && !state.gameOver) {
showRoundModal();
}
}, 120);
}
}八、換手時清除暫時選中的放置棋子
在 resolveNextTurn() 中找到:
state.movedThisTurn = false;
state.selectedMinoId = null;
state.pendingMinoMove = null;
state.hoverCell = null;
preparePlayerPhase(state.currentPlayer);替換成:
state.movedThisTurn = false;
state.selectedMinoId = null;
state.selectedPlacementType = null;
state.pendingMinoMove = null;
state.hoverCell = null;
preparePlayerPhase(state.currentPlayer);九、移動場上的米諾後,自動選中維諾
在 performNormalMinoMove() 中找到:
state.movedThisTurn = true;
state.selectedMinoId = null;
state.hoverCell = null;替換成:
state.movedThisTurn = true;
state.selectedMinoId = null;
/*
* 不論原本是從米諾階段暫時選中維諾,
* 還是已正式進入維諾階段,
* 移動場上的米諾後都自動切換為維諾選中。
*/
state.selectedPlacementType = 'vino';
state.hoverCell = null;在 revertPendingMinoMove() 中找到:
state.pendingMinoMove = null;
state.movedThisTurn = false;
state.selectedMinoId = null;
state.hoverCell = null;替換成:
state.pendingMinoMove = null;
state.movedThisTurn = false;
state.selectedMinoId = null;
state.selectedPlacementType = 'vino';
state.hoverCell = null;十、飛子必須處於維諾階段,而且場上已有自己的維諾
在 attemptMinoFlight() 開頭找到:
function attemptMinoFlight(pieceId, destination) {
const player = state.currentPlayer;
const piece = findPiece(pieceId);
if (替換成:
function attemptMinoFlight(pieceId, destination) {
const player = state.currentPlayer;
const piece = findPiece(pieceId);
if (!canVoluntarilyFlyMino(player)) {
showToast('flightNeedsVino', {}, 1400);
return;
}
if (接著,在同一個 attemptMinoFlight() 中找到:
const wasAutomaticFlightPhase = isMinoFlightPhase(player);替換成:
const wasAutomaticFlightPhase = false;雖然舊的自動飛行階段已經不會再啟動,但保留後面的確認窗口邏輯。
十一、取消飛子後保持維諾選中
在 cancelPendingFlight() 中找到:
state.pendingFlight = null;
state.movedThisTurn = false;
state.pendingMinoMove = null;
state.hoverCell = null;替換成:
state.pendingFlight = null;
state.movedThisTurn = false;
state.pendingMinoMove = null;
state.selectedPlacementType = 'vino';
state.hoverCell = null;在 finishPlayerByFlight() 中找到:
state.pendingFlight = null;
state.playerEnded[player] = true;
state.selectedMinoId = null;
state.pendingMinoMove = null;替換成:
state.pendingFlight = null;
state.playerEnded[player] = true;
state.selectedMinoId = null;
state.selectedPlacementType = null;
state.pendingMinoMove = null;十二、沒有維諾庫存時不要顯示普通移動箭頭
在 renderMoveArrows() 中找到:
// 飛行階段只顯示選中框,不顯示任何移動箭頭。
if (isMinoFlightPhase(state.currentPlayer) || state.movedThisTurn) {
return;
}替換成:
/*
* 沒有待放置維諾時,只能把米諾飛走結束,
* 不能再進行「先正常移動米諾、再放維諾」的操作。
*/
const active = getActivePiece();
if (
isMinoFlightPhase(state.currentPlayer) ||
state.movedThisTurn ||
!active ||
active.type !== 'vino'
) {
return;
}十三、完整替換棋盤點擊函式
原本的 handleBoardClick() 依賴 active.type === 'vino'。當維諾庫存為 0 時,active 會是 null,因此必須改用正式階段判斷。
找到完整的:
function handleBoardClick(event) {
...
}替換成:
function handleBoardClick(event) {
if (!state.gameStarted) {
return;
}
if (state.roundEnded || state.gameOver) {
openCurrentResult();
return;
}
const arrow = event.target.closest('[data-move-dir]');
if (arrow) {
executeMinoMove(arrow.dataset.moveDir);
return;
}
const cell = getBoardCellFromPointer(event);
if (!cell) {
return;
}
const player = state.currentPlayer;
const occupied = state.board[cell.row][cell.col];
const placementPhase = getRequiredPiece(player);
const active = getActivePiece();
if (isMinoFlightPhase(player)) {
handleMinoFlightBoardClick(cell, occupied);
return;
}
/*
* 正常移動後再次點擊被移動的米諾,
* 立即使它退回原來的位置。
*/
if (
placementPhase === 'vino' &&
occupied &&
occupied.owner === player &&
occupied.type === 'mino' &&
state.pendingMinoMove &&
occupied.pieceId === state.pendingMinoMove.pieceId
) {
revertPendingMinoMove();
return;
}
/*
* 只要目前選中的是維諾,或者正式處於維諾階段,
* 就可以點擊本方場上米諾進入米諾移動選取。
*
* 因此也支援:
* 米諾階段 → 點擊控制區維諾 → 點擊場上米諾移動。
*/
const canUseBoardMino =
placementPhase === 'vino' ||
(active && active.type === 'vino');
if (
canUseBoardMino &&
occupied &&
occupied.owner === player &&
occupied.type === 'mino'
) {
selectMino(occupied.pieceId);
return;
}
/*
* 已選中場上米諾時,空格點擊被解釋為米諾目的地。
*/
if (canUseBoardMino && state.selectedMinoId) {
if (occupied) {
showToast('overlap');
return;
}
const legalDestinations =
getLegalMinoDestinations(state.selectedMinoId);
const normalDestination =
legalDestinations.get(cellKey(cell.row, cell.col));
if (normalDestination) {
/*
* 只有仍有維諾可放時,才允許普通移動米諾。
* 普通移動完成後會自動選中維諾。
*/
if (active && active.type === 'vino') {
performNormalMinoMove(
state.selectedMinoId,
normalDestination
);
} else {
/*
* 維諾已用完時,能正常到達的位置不能作為飛子落點。
*/
showToast('flightReachableCell', {}, 1000);
}
} else if (canVoluntarilyFlyMino(player)) {
attemptMinoFlight(state.selectedMinoId, cell);
} else {
showToast('flightNeedsVino', {}, 1400);
}
return;
}
/*
* 沒有可放置的 active 棋子時,表示維諾庫存已用完;
* 此時只能選取米諾進行飛子,不能普通落子。
*/
if (!active) {
return;
}
const validation = validatePlacement(
active.player,
active.type,
active.orientation,
cell.row,
cell.col
);
if (!validation.ok) {
const toastKey =
{
bounds: 'outOfBounds',
overlap: 'overlap',
contact: 'ownContact'
}[validation.reason] || 'overlap';
showToast(toastKey);
renderMoveArrows();
return;
}
placePiece(validation, active);
}十四、維諾用完時顯示飛子提示
修改 renderBoard()
在 renderBoard() 中找到:
const active = getActivePiece();
status = active
? translate('turnAuto', {
player: playerToken(active.player),
piece: pieceToken(active.type)
})
: '';替換成:
const active = getActivePiece();
const phase = getRequiredPiece(state.currentPlayer);
if (active) {
status = translate('turnAuto', {
player: playerToken(active.player),
piece: pieceToken(active.type)
});
} else if (
phase === 'vino' &&
canVoluntarilyFlyMino(state.currentPlayer)
) {
status = translate('turnVinoOnlyFlight', {
player: playerToken(state.currentPlayer)
});
} else {
status = '';
}修改 renderStatus()
在 renderStatus() 中找到最後這一段:
} else if (active) {
els.turnText.textContent = translate('turnAuto', {
player: playerToken(active.player),
piece: pieceToken(active.type)
});
} else {
els.turnText.textContent = '';
}替換成:
} else if (active) {
els.turnText.textContent = translate('turnAuto', {
player: playerToken(active.player),
piece: pieceToken(active.type)
});
} else if (
getRequiredPiece(state.currentPlayer) === 'vino' &&
canVoluntarilyFlyMino(state.currentPlayer)
) {
els.turnText.textContent = translate('turnVinoOnlyFlight', {
player: playerToken(state.currentPlayer)
});
} else {
els.turnText.textContent = '';
}十五、初始化與重新開局時清除新增狀態
在 startRound() 中找到:
state.hoverCell = null;
state.selectedMinoId = null;
state.movedThisTurn = false;
state.pendingMinoMove = null;
state.pendingFlight = null;替換成:
state.hoverCell = null;
state.selectedMinoId = null;
state.selectedPlacementType = null;
state.movedThisTurn = false;
state.pendingMinoMove = null;
state.pendingFlight = null;
state.matchForfeit = null;在 startGame() 中找到:
state.gameStarted = true;
state.gameOver = false;
state.roundResults = [];替換成:
state.gameStarted = true;
state.gameOver = false;
state.roundResults = [];
state.matchForfeit = null;在 restoreSnapshot() 中找到:
state.selectedMinoId = null;
state.hoverCell = null;替換成:
state.selectedMinoId = null;
state.selectedPlacementType = null;
state.hoverCell = null;在 returnToTitle() 中找到:
state.pendingFlight = null;
state.pendingMinoMove = null;
state.gameStarted = false;
state.gameOver = false;替換成:
state.pendingFlight = null;
state.pendingMinoMove = null;
state.selectedPlacementType = null;
state.matchForfeit = null;
state.gameStarted = false;
state.gameOver = false;在正常的 finishMatch() 開頭加入:
state.matchForfeit = null;修改後為:
function finishMatch() {
state.matchForfeit = null;
commitPendingRoundResult();
state.gameOver = true;
state.roundEnded = true;
renderAll();
showFinalModal();
}十六、增加「無法放置直接輸掉總比賽」的結果窗口
在 showFinalModal() 前面加入:
function showNoPlacementFinalModal(focusPrimary = true) {
const forfeit = state.matchForfeit;
if (!forfeit) {
return;
}
state.resultMode = 'final';
els.resultEyebrow.textContent = translate('finalEyebrow');
els.resultTitle.textContent = translate('winsMatch', {
player: playerToken(forfeit.winner)
});
els.resultSummary.textContent = translate(
'noPlacementResultSummary',
{
loser: playerToken(forfeit.loser),
winner: playerToken(forfeit.winner),
piece: pieceToken(forfeit.requiredType)
}
);
/*
* 這次勝負由直接判負規則決定,
* 分數不參與總勝負,所以隱藏分數區。
*/
els.resultSectionLabel.textContent = translate('decidedByRule');
els.resultSectionLabel.hidden = false;
els.resultScores.hidden = true;
els.resultScores.replaceChildren();
els.resultBreakdown.hidden = true;
els.resultBreakdown.replaceChildren();
els.resultPrimary.textContent = translate('playAgain');
els.resultSecondary.textContent = translate('backToTitle');
els.resultSecondary.hidden = false;
els.resultPrimary.onclick = () => {
state.roundResults = [];
state.matchForfeit = null;
state.gameOver = false;
state.gameStarted = true;
startRound(1);
};
els.resultSecondary.onclick = returnToTitle;
els.resultBackdrop.hidden = false;
if (focusPrimary) {
requestAnimationFrame(() => {
els.resultPrimary.focus();
});
}
}然後找到 showFinalModal() 開頭:
function showFinalModal(focusPrimary = true) {
const results = [...state.roundResults].sort((a, b) => a.round - b.round);
if (!results.length) {
return;
}替換成:
function showFinalModal(focusPrimary = true) {
if (state.matchForfeit) {
showNoPlacementFinalModal(focusPrimary);
return;
}
const results = [...state.roundResults].sort(
(a, b) => a.round - b.round
);
if (!results.length) {
return;
}
els.resultSectionLabel.hidden = false;
els.resultScores.hidden = false;注意:只替換函式開頭,showFinalModal() 後面的原有內容繼續保留。
在 showRoundModal() 中,找到:
els.resultSectionLabel.textContent = translate('roundScores');
els.resultScores.innerHTML = scoreCardsHTML(result.stats[1], result.stats[2]);替換成:
els.resultSectionLabel.hidden = false;
els.resultSectionLabel.textContent = translate('roundScores');
els.resultScores.hidden = false;
els.resultScores.innerHTML = scoreCardsHTML(
result.stats[1],
result.stats[2]
);十七、更新規則窗口
中文 rulesHtml
將 I18N.zh.rulesHtml 完整替換成:
rulesHtml: `
<div class="rules-grid">
<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">04</span>
<h3>維諾與米諾移動</h3>
<p>
選中維諾時,可以先點擊一個本方場上米諾並按照正常移動規則移動。
米諾移動完成後,控制區會自動選中維諾。
點擊移動後的米諾,可以在放置棋子前把它退回原位。
</p>
</article>
<article class="rule-block">
<span class="rule-index">05</span>
<h3>飛走米諾結束本方遊戲</h3>
<p>
正式處於維諾階段,而且場上已有至少一個本方維諾時,
可以選擇把一個本方米諾飛到它目前按照正常移動規則走不到的空位。
確認飛子後,該玩家立即結束本方遊戲,不能再進行行動。
另一方繼續遊戲,直到他也結束本方遊戲。
</p>
</article>
<article class="rule-block">
<span class="rule-index">06</span>
<h3>無法放置直接判負</h3>
<p>
如果輪到一方時,該方在目前正式所處的放置階段仍有棋子,
但整個棋盤上連一個合法落點都不存在,該方立即判負。
這不是單局判負,而是直接輸掉兩局的總勝負;
比賽立即結束,不再比較分數,也不再進行下一局。
</p>
</article>
<article class="rule-block wide">
<span class="rule-index">07</span>
<h3>計分</h3>
<p>
每個由同一玩家完整佔用的正方形,都會按照面積計分:
1 × 1 計 1 分、2 × 2 計 4 分、3 × 3 計 9 分,依此類推。
不同位置及互相重疊的正方形會分別計算。
如果比賽因「無法放置直接判負」而結束,分數不影響總勝負。
</p>
</article>
</div>
`英文 rulesHtml
將 I18N.en.rulesHtml 完整替換成:
rulesHtml: `
<div class="rules-grid">
<article class="rule-block">
<span class="rule-index">01</span>
<h3>Pieces and placement phases</h3>
<p>
Each player has Fino × 5, Mino × 3, and Vino × 7.
Each player advances independently through
<strong>Fino → Mino → Vino</strong>.
</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, including diagonally. After placing at least one Fino,
the player may temporarily select Mino. The player may select Fino
again before placing, but once a Mino is placed, Fino can no longer
be selected.
</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. After placing at least one Mino,
the player may temporarily select Vino. The player may select Mino
again before placing, but once a Vino is placed, Mino can no longer
be selected.
</p>
</article>
<article class="rule-block">
<span class="rule-index">04</span>
<h3>Vino and normal Mino movement</h3>
<p>
While Vino is selected, the player may first select and normally move
one of their Mino pieces. After that Mino moves, Vino is selected
automatically. Clicking the moved Mino before placement returns it
to its original cell.
</p>
</article>
<article class="rule-block">
<span class="rule-index">05</span>
<h3>Flying a Mino to finish</h3>
<p>
During the Vino phase, after at least one of the player's Vino pieces
is on the board, the player may fly one of their Mino pieces to an
empty cell that it cannot reach by a normal move. Confirming the
flight immediately ends that player's game.
</p>
</article>
<article class="rule-block">
<span class="rule-index">06</span>
<h3>No legal placement means defeat</h3>
<p>
If a player still has pieces for their current placement phase but
there is no legal placement anywhere on the board, that player
immediately loses the entire two-round match. The match ends without
comparing scores and no further round is played.
</p>
</article>
<article class="rule-block wide">
<span class="rule-index">07</span>
<h3>Scoring</h3>
<p>
Every fully occupied square owned by a player scores its area:
a 1 × 1 scores 1 point, a 2 × 2 scores 4 points, a 3 × 3
scores 9 points, and so on. Overlapping positions count separately.
Scores do not affect the result when the match ends under the
no-legal-placement defeat rule.
</p>
</article>
</div>
`修改後的核心行為
-
法諾階段
- 場上已有自己的法諾後,法諾和米諾按鈕都可以點。
- 放米諾前可以選回法諾。
- 實際放下米諾後永久鎖定法諾。
-
米諾階段
- 場上已有自己的米諾後,米諾和維諾按鈕都可以點。
- 放維諾前可以選回米諾。
- 實際放下維諾後永久鎖定米諾。
- 移動場上的米諾後自動選中維諾。
-
維諾階段
- 場上已有自己的維諾後,可以飛走自己的米諾。
- 飛子後結束該玩家本局行動。
- 維諾全部放完後仍可飛子。
-
無合法落點
- 不再自動跳到下一階段。
- 直接輸掉整場兩局比賽。
- 結果窗口顯示規則判負,不比較總分。