QuanAI只读快照

共享会话

🎲 遊戲階段規則變更

分享于 2026年8月29日 00:33
提问者

更改規則: 之前的規則是「如果一方無法合規放完五個法諾,該方立即判負。」現在把這個規則去掉,改為:

「每方在遊戲中有三個階段,法諾、米諾、維諾,當處於前面的階段,玩家可以自由選擇跳到後面的階段,但不能再跳回前面的階段。

比如當一個玩家手上還有法諾棋子沒有落完,如果他願意,或者確實無法再落下一個法諾,則他可以跳到米諾階段,甚至維諾階段。」

程式邏輯實現:

當輪到一方行動時如果處於法諾階段,他可以選擇米諾或維諾棋子(當然可以再選回法諾),當米諾或維諾棋子落下,則等於他選擇跳過了法諾階段,此後他將不能再落下一個法諾。

當一方處於米諾落子階段,他如果選擇一個維諾並落下,或者他移動了一個場上的米諾,則等同於他跳過了米諾階段,直接到維諾階段。此後他將不能再落下一個米諾。

vino2242.htmltext/plain · 116.6 KB预览下载
QuanAI

可以按以下方式修改 vino2242.html。核心做法是為每名玩家分別保存:

  • playerPhase:已正式進入的階段。
  • selectedPieceType:本回合目前選中的棋子。
  • 選擇棋子不會推進階段;真正落下後才推進。
  • 米諾階段移動場上米諾時,立即進入維諾階段,但仍沿用原本流程,可在同一回合繼續放置維諾。

1. 新增階段初始值

放在 createOrientationMemory() 後面:

js
function createPlayerPhases() {
  return {
    1: "fino",
    2: "fino"
  };
}

function createSelectedPieceTypes() {
  return {
    1: "fino",
    2: "fino"
  };
}

state 裡,緊接 inventory 後面加入:

js
playerPhase: createPlayerPhases(),
selectedPieceType: createSelectedPieceTypes(),

即:

js
inventory: createInventory(),

playerPhase: createPlayerPhases(),
selectedPieceType: createSelectedPieceTypes(),

scoreStats: {

2. 替換階段與目前棋子判定

刪除原本的:

js
function getRequiredPiece(player) {
  ...
}

並把原本的 getActivePiece() 一起替換成:

js
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() 替換成:

js
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() 替換成:

js
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() 裡找到:

js
state.inventory[active.player][active.type] -= 1;
rebuildBoard();

替換成:

js
state.inventory[active.player][active.type] -= 1;

// 較後階段的棋子真正落下時,才正式跳過前面的階段。
// 如果目前階段的棋子剛好用完,也會自動進入下一階段。
advancePlayerPhaseTo(active.player, active.type);

rebuildBoard();

再把:

js
if (
  active.type === "vino" &&
  state.inventory[active.player].vino === 0
) {
  state.playerEnded[active.player] = true;
}

替換成:

js
if (
  state.playerPhase[active.player] === "vino" &&
  state.inventory[active.player].vino === 0
) {
  state.playerEnded[active.player] = true;
}

6. 移除「無合法法諾/米諾立即判負」

刪除整個函式:

js
function completeRoundByNoLegalMove(loser, requiredType) {
  ...
}

hasAnyLegalPlacement() 此後不再使用,也可以整個刪除。

resolveNextTurn() 整個替換成:

js
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;
}

原本以下判負部分必須完全移除:

js
if (
  (required === "fino" || required === "mino") &&
  !hasAnyLegalPlacement(state.currentPlayer, required)
) {
  completeRoundByNoLegalMove(
    state.currentPlayer,
    required
  );
}

7. 允許米諾階段移動場上米諾

修改 renderMoveArrows()

在函式開頭的判定中加入階段限制:

js
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()

在函式開始處加入:

js
if (state.playerPhase[state.currentPlayer] === "fino") {
  return;
}

完整前段應為:

js
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()

js
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() 中找到:

js
if (
  active.type === "vino" &&
  occupied &&
  occupied.owner === active.player &&
  occupied.type === "mino"
) {

替換成:

js
if (
  state.playerPhase[active.player] !== "fino" &&
  occupied &&
  occupied.owner === active.player &&
  occupied.type === "mino"
) {

8. 「結束本方遊戲」改為依正式階段判定

renderEndButtons() 替換成:

js
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() 中刪除:

js
const required = getRequiredPiece(player);

if (required !== "vino") {

改成:

js
if (state.playerPhase[player] !== "vino") {

也就是:

js
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 後加入:

js
playerPhase: {
  1: state.playerPhase[1],
  2: state.playerPhase[2]
},

selectedPieceType: {
  1: state.selectedPieceType[1],
  2: state.selectedPieceType[2]
},

restoreSnapshot() 的 inventory 還原後加入:

js
state.playerPhase = {
  1: snapshot.playerPhase[1],
  2: snapshot.playerPhase[2]
};

state.selectedPieceType = {
  1: snapshot.selectedPieceType[1],
  2: snapshot.selectedPieceType[2]
};

startRound() 的:

js
state.inventory = createInventory();

後加入:

js
state.playerPhase = createPlayerPhases();
state.selectedPieceType = createSelectedPieceTypes();

10. 移除舊判負結果文案

英文 I18N 中刪除:

js
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 中刪除:

js
noLegalFinoSummary:
  "{loser}無法合規放完五個法諾,因此{winner}立即贏得本局。",
noLegalRequiredSummary:
  "{loser}無法合規放置目前必須使用的{piece},因此{winner}立即贏得本局。",

showRoundModal() 中,把原本處理 noLegalFinonoLegalRequired 的整段分支替換成:

js
if (result.endInfo.winner === 0) {
  els.resultSummary.textContent =
    translate("finishedTieSummary");
} else {
  els.resultSummary.textContent =
    translate("finishedWinSummary", {
      winner: playerToken(result.endInfo.winner)
    });
}

11. 更新介面文字

把英文:

js
turnAuto: "{player} · {piece} selected",

把中文:

js
turnAuto: "輪到{player}・已選取{piece}",

取代原本帶有「自動選取」的文字。

規則視窗中的前三條建議改為:

中文

html
<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>

英文

html
<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>

完成後的階段狀態為:

流程图
正在绘制流程图…

這套修改會保留原本的計分、兩局比賽、回退/前進和米諾移動方式,同時完全取消「法諾或米諾無合法位置就立即判負」的流程。