「益智遊戲」和「抽象弈棋」
回复
头像
ejsoon
一枝独秀
帖子: 6458
注册时间: 2022年 11月 18日 17:36
联系:

Re: 一個新的遊戲創意

帖子 ejsoon »

gemini 3.1 pro + thinking

代码: 全选

這幾個改進都非常切中要害!特別是把 `Date.now()` 的依賴去掉並將 ID 改為落子次序,不僅讓系統邏輯更加乾淨,還為「棋譜導出導入」功能打下了完美的基礎。使用 Undo/Redo Stack 的做法也遠比反覆 `JSON.parse` 效能更好且不會產生深拷貝帶來的副作用。

以下是為你精確編寫的代碼替換與修改方案。請依照步驟將原本的代碼替換掉:

### 一、 修改多國語言字典 (i18n)

在 `<script>` 頂部的 `const i18n = { ... }` 裡,加入以下四組新翻譯(用於「遊戲資訊」關閉按鈕與導出/導入面板):

```javascript
        'ui-info-close': { zh: '關閉', en: 'Close' },
        'ui-io-title': { zh: '棋譜導出 / 導入', en: 'Export / Import Notation' },
        'ui-io-copy': { zh: '複製', en: 'Copy' },
        'ui-io-confirm': { zh: '確認', en: 'Confirm' }

```

### 二、 更新 CSS 樣式 (關閉按鈕、恢復按鈕放大)

在 `<style>` 標籤中,找到 `.close-icon` 的部分,並增加寬高設定(因為從文字改為 SVG),同時把 `#btn-restore` 稍微放大:

```css
      .close-icon {
        position: absolute;
        top: 10px;
        right: 15px;
        cursor: pointer;
        color: #aaa;
        width: 24px;  /* 新增 */
        height: 24px; /* 新增 */
      }
      .close-icon:hover {
        color: white;
      }
      /* 放大恢復視窗按鈕 */
      #btn-restore {
        transform: scale(1.15);
      }

```

### 三、 替換 HTML 結構 (SVG 圖標、視窗按鈕、導入導出面板)

**1. 替換所有 `.close-icon` 與導航箭頭 (◄ / ►)**
找到 `#rules-modal`、`#arb-result-dialog`、`#info-dialog` 裡面的 `<span class="close-icon"...>✖</span>`,並把 `#arb-nav-prev` 和 `#arb-nav-next` 的文字清空,統一換成 SVG:

```html
    <svg class="close-icon" onclick="document.getElementById('rules-modal').style.display = 'none'" viewBox="0 0 24 24" style="z-index: 2100"><path fill="currentColor" d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"/></svg>

    <svg class="close-icon" onclick="document.getElementById('arb-result-dialog').style.display = 'none'" viewBox="0 0 24 24"><path fill="currentColor" d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"/></svg>

    <svg class="close-icon" onclick="document.getElementById('info-dialog').style.display = 'none'" viewBox="0 0 24 24"><path fill="currentColor" d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"/></svg>
    
    <button class="menu-btn" style="background: #555" onclick="document.getElementById('info-dialog').style.display = 'none'" id="ui-info-close">關閉</button>

    <div id="arb-nav-prev" class="arb-nav-btn" onclick="arbNav(-1)">
      <svg viewBox="0 0 24 24" width="24" height="24"><path fill="currentColor" d="M15.41 16.59L10.83 12l4.58-4.59L14 6l-6 6 6 6 1.41-1.41z"/></svg>
    </div>
    <div id="arb-nav-next" class="arb-nav-btn" onclick="arbNav(1)">
      <svg viewBox="0 0 24 24" width="24" height="24"><path fill="currentColor" d="M8.59 16.59L13.17 12 8.59 7.41 10 6l6 6-6 6-1.41-1.41z"/></svg>
    </div>

```

**2. 替換工具列 `#toolbar` 中的 仲裁、恢復按鈕,並增加 導出/導入 按鈕**
在 `<div id="toolbar">` 中,找到仲裁和恢復的 `<svg>`,替換為新的,並在「下載」按鈕前插入導出按鈕:

```html
        <svg class="tool-btn" id="btn-arbitrate" viewBox="0 0 24 24" onclick="requestArbitration()">
          <path d="M5.2,12.7L2.4,15.5c-0.8,0.8-0.8,2,0,2.8l2.8,2.8c0.8,0.8,2,0.8,2.8,0l2.8-2.8L5.2,12.7z M20.9,4.3l-2.1-2.1c-0.6-0.6-1.5-0.6-2.1,0L13,5.8l4.2,4.2l3.7-3.7C21.5,5.8,21.5,4.9,20.9,4.3z M15.8,11.4l-4.2-4.2L6.6,12.2l4.2,4.2L15.8,11.4z"/>
        </svg>
        <svg class="tool-btn" id="btn-restore" viewBox="0 0 24 24" onclick="restoreWindow()">
          <path d="M3 3h18v18H3V3zm2 2v14h14V5H5z"/>
          <path id="restore-num" d="M11 15h2V9l-2 1v1l1-.5V15z"/>
        </svg>
        <svg class="tool-btn" id="btn-export-import" viewBox="0 0 24 24" onclick="openIO()">
          <path d="M16 3l-4 4h3v7h2V7h3l-4-4zm-5 14H8v-7H6v7H3l4 4 4-4z"/>
        </svg>

```

**3. 在 `<body>` 結尾前 (任意 dialog 旁邊) 加入導出導入面板**

```html
    <div id="io-dialog" class="custom-dialog">
      <div class="dialog-box" style="width: 420px; max-width: 90vw;">
        <svg class="close-icon" onclick="document.getElementById('io-dialog').style.display = 'none'" viewBox="0 0 24 24"><path fill="currentColor" d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"/></svg>
        <h2 id="ui-io-title">棋譜導出 / 導入</h2>
        <div style="display: flex; gap: 10px; margin-bottom: 15px; height: 75px;">
          <textarea id="io-export-text" style="flex: 1; resize: none; border-radius: 5px; padding: 5px;" readonly></textarea>
          <button class="menu-btn" style="margin: 0; height: 100%;" onclick="copyExport()" id="ui-io-copy">複製</button>
        </div>
        <div style="display: flex; gap: 10px; height: 75px;">
          <textarea id="io-import-text" style="flex: 1; resize: none; border-radius: 5px; padding: 5px;"></textarea>
          <button class="menu-btn" style="margin: 0; height: 100%; background: #43b581;" onclick="confirmImport()" id="ui-io-confirm">確認</button>
        </div>
      </div>
    </div>

```

### 四、 JS 邏輯修改 (核心區塊)

**1. 新增全域變數** (加在 `let SCORING_MODE = 0;` 附近)

```javascript
      let completedGamesNotation = ""; // 儲存第一局的完整棋譜
      let arbEnded = false;            // 記錄是否被仲裁終結

```

**2. 替換 `updateUI()` 中關於 `#btn-restore` 數字更新的邏輯**
在 `updateUI()` 中增加一行控制:

```javascript
        let numPath = document.getElementById('restore-num');
        if (numPath) {
          numPath.setAttribute('d', gameNumber === 1 ? 'M11 15h2V9l-2 1v1l1-.5V15z' : 'M10 15h4v-1.5h-2.5v-.5l2.5-1.5V9h-4v1.5h2.5v.5L10 13v2z');
        }

```

**3. 替換 `selectTile` 與 `commitGhost` 裡的 ID 生成邏輯**
在 `selectTile` 裡的第一手棋建立:

```javascript
          tempPieces = [
            {
              id: pieces.length + tempPieces.length + 1, // 改為次序號
              owner: currentPlayer,
              type: SHAPE_MAP[tileId],
              svgId: tileId,
              isFlipped: isFlipped[tileId],
              vertices: vertices,
              parentId: null,
              level: 0,
              boardAngle: boardTransform.angle === 0 ? 360 : (boardTransform.angle || 360) // 記錄初始旋轉供導出
            }
          ];

```

在 `commitGhost` 裡:

```javascript
      function commitGhost(index) {
        let gp = ghosts[index];
        gp.id = pieces.length + tempPieces.length + 1; // 改為次序號
        gp.targetId = targetOpponentPieceId;
        // ... (保持原有代碼)

```

**4. 徹底重構 `saveState`、`undo`、`redo` 以及修正順序**
找到原來的這三個函數並完全替換,同時移除了所有的 `JSON.parse` 依賴,並加入自動彈出對應視窗的邏輯:

```javascript
      function saveState() {
        gameHistory.length = historyIndex + 1;
        gameHistory.push({
          newPieces: [...lastTurnPieces], // 淺拷貝陣列,只存參照
          scores: { ...scores },
          totalScores: { ...totalScores },
          piecesCount: { ...piecesCount },
          turnNumber: turnNumber,
          currentPlayer: currentPlayer,
          gameNumber: gameNumber,
          startingPlayer: startingPlayer,
          scoredVictims: new Set(scoredVictims),
          boardAngle: boardTransform.angle,
          arbEnded: arbEnded,
          dialogMode: currentDialogMode // 記錄該狀態時的視窗情境
        });
        historyIndex++;
        updateToolbar();
      }

      function undo() {
        if (historyIndex > 0) {
          let currentState = gameHistory[historyIndex];
          historyIndex--;
          let prevState = gameHistory[historyIndex];

          // 從 pieces 中 pop 出對應數量的棋子
          let removeCount = currentState.newPieces ? currentState.newPieces.length : 0;
          for (let i = 0; i < removeCount; i++) pieces.pop();

          scores = { ...prevState.scores };
          totalScores = { ...prevState.totalScores };
          piecesCount = { ...prevState.piecesCount };
          turnNumber = prevState.turnNumber;
          currentPlayer = prevState.currentPlayer;
          gameNumber = prevState.gameNumber;
          startingPlayer = prevState.startingPlayer;
          boardTransform.angle = prevState.boardAngle || 0;
          applyRotation();
          scoredVictims = new Set(prevState.scoredVictims);
          lastTurnPieces = prevState.newPieces ? [...prevState.newPieces] : [];
          tempPieces = [];
          ghosts = [];
          selectedTile = null;
          targetOpponentPieceId = null;
          arbEnded = prevState.arbEnded;
          currentDialogMode = prevState.dialogMode;

          // 隱藏所有彈窗,再根據狀態決定開啟哪一個
          document.querySelectorAll('.custom-dialog').forEach(d => d.style.display = 'none');
          if (currentDialogMode === 'end-game') {
            checkEndGame(); 
          } else if (currentDialogMode === 'arbitration') {
            let ways = arbValidMoves.length;
            let msgEl = document.getElementById('ui-res-msg');
            let waysEl = document.getElementById('ui-res-ways');
            if (ways > 0) {
              msgEl.innerText = i18n['arb-fail'][currentLang];
              waysEl.innerText = i18n['arb-ways'][currentLang].replace('{n}', ways);
              document.getElementById('arb-nav-prev').style.display = ways > 1 ? 'block' : 'none';
              document.getElementById('arb-nav-next').style.display = ways > 1 ? 'block' : 'none';
              applyArbMove(arbCurrentIndex);
            } else {
              msgEl.innerText = i18n['arb-success'][currentLang];
              waysEl.innerText = i18n['arb-ways'][currentLang].replace('{n}', 0);
              document.getElementById('arb-nav-prev').style.display = 'none';
              document.getElementById('arb-nav-next').style.display = 'none';
            }
            document.getElementById('arb-result-dialog').style.display = 'flex';
          }

          updateTransform();
          updateUI();
          renderBoard();
          updateToolbar();
        }
      }

      function redo() {
        if (historyIndex < gameHistory.length - 1) {
          historyIndex++;
          let nextState = gameHistory[historyIndex];

          // 將重做的棋子 push 回 pieces
          let piecesToAdd = nextState.newPieces ? [...nextState.newPieces] : [];
          for (let p of piecesToAdd) pieces.push(p);

          scores = { ...nextState.scores };
          totalScores = { ...nextState.totalScores };
          piecesCount = { ...nextState.piecesCount };
          turnNumber = nextState.turnNumber;
          currentPlayer = nextState.currentPlayer;
          gameNumber = nextState.gameNumber;
          startingPlayer = nextState.startingPlayer;
          boardTransform.angle = nextState.boardAngle || 0;
          applyRotation();
          scoredVictims = new Set(nextState.scoredVictims);
          lastTurnPieces = [...piecesToAdd];
          tempPieces = [];
          ghosts = [];
          selectedTile = null;
          targetOpponentPieceId = null;
          arbEnded = nextState.arbEnded;
          currentDialogMode = nextState.dialogMode;

          document.querySelectorAll('.custom-dialog').forEach(d => d.style.display = 'none');
          if (currentDialogMode === 'end-game') {
            checkEndGame();
          } else if (currentDialogMode === 'arbitration') {
            let ways = arbValidMoves.length;
            if (ways > 0) applyArbMove(arbCurrentIndex);
            document.getElementById('arb-result-dialog').style.display = 'flex';
          }

          updateTransform();
          updateUI();
          renderBoard();
          updateToolbar();
        }
      }

```

**5. 調整 `actionCheck` 和 `executeArbitration` 確保狀態正確存取**
在 `actionCheck()` 最末端,將原來的:

```javascript
        lastTurnPieces = JSON.parse(JSON.stringify(tempPieces));
        pieces = allNewP;
        // ... (省略)
        saveState();
        checkEndGame();

```

更改為:

```javascript
        pieces.push(...tempPieces);
        lastTurnPieces = [...tempPieces];
        tempPieces = [];
        selectedTile = null;
        targetOpponentPieceId = null;
        ghosts = [];

        if (allRings.length > 0) {
          setTimeout(() => { allRings = []; renderBoard(); }, 1200);
        }

        turnNumber++;
        currentPlayer = currentPlayer === 1 ? 2 : 1;
        applyRotation();

        updateUI();
        renderBoard();

        // 必須先判斷結局,再保存狀態(這樣才能將正確的 dialogMode 存入)
        checkEndGame();
        saveState();

```

在 `executeArbitration()` 裡,為了保證 Undo 可以正常返回沒彈窗的狀態,修改如下:

```javascript
      function executeArbitration() {
        document.getElementById('arb-confirm-dialog').style.display = 'none';
        actionCross(false);

        arbValidMoves = getAllValidMoves(currentPlayer);
        let ways = arbValidMoves.length;
        
        arbEnded = true;
        currentDialogMode = 'arbitration';
        saveState(); // 先保存為終局狀態,之後再顯示預覽棋子

        let resDialog = document.getElementById('arb-result-dialog');
        let msgEl = document.getElementById('ui-res-msg');
        let waysEl = document.getElementById('ui-res-ways');

        if (ways > 0) {
          arbCurrentIndex = 0;
          applyArbMove(arbCurrentIndex);
        // ...(保持原有代碼)

```

**6. 更新 `restartFromArb`、`startGame` 與 `nextGame**`
由於 Arbitration 重來或者兩局轉換的資料清空,將它們加入清理的屬性中。

```javascript
      function restartFromArb() {
        // ... 原代碼 ...
        arbEnded = false;
        completedGamesNotation = "";
        // ...
      }

      function startGame() {
        // ... 原代碼 ...
        arbEnded = false;
        completedGamesNotation = "";
        // ...
      }
      
      function nextGame() {
        document.getElementById('end-game-dialog').style.display = 'none';
        if (gameNumber === 1) {
          completedGamesNotation = getGameNotation(pieces); // 進入第二局前存儲第一局棋譜
          totalScores[1] = scores[1];
          // ... 原代碼

```

**7. 加入棋譜導出/導入核心邏輯 (放在 JS 檔案的末尾即可)**

```javascript
      function getSvgIdForPlayer(type, isFlipped, owner) {
        if (owner === 1) {
          if (type === 'B') return 'tile0';
          if (type === 'A') return 'tile1';
          if (type === 'C') return 'tile2';
        } else {
          if (type === 'B') return 'tile3';
          if (type === 'A') return 'tile4';
          if (type === 'C') return 'tile5';
        }
      }

      function getGameNotation(pts) {
        if (!pts || pts.length === 0) return "";
        let res = [];
        for (let i = 0; i < pts.length; i++) {
          let p = pts[i];
          let t;
          if (p.type === 'B' && !p.isFlipped) t = 1;
          else if (p.type === 'B' && p.isFlipped) t = 2;
          else if (p.type === 'A' && !p.isFlipped) t = 3;
          else if (p.type === 'A' && p.isFlipped) t = 4;
          else if (p.type === 'C' && !p.isFlipped) t = 5;
          else if (p.type === 'C' && p.isFlipped) t = 6;

          if (i === 0) {
            res.push(t + "" + (p.boardAngle || 360));
          } else {
            res.push(t + "" + (p.myEdge + 1) + "" + p.parentId + "" + (p.targetEdge + 1));
          }
        }
        return res.join(" ");
      }

      function openIO() {
        let cur = getGameNotation(pieces.concat(tempPieces));
        let full = completedGamesNotation ? completedGamesNotation + (cur ? " " + cur : "") : cur;
        if (arbEnded) full += " 0";
        document.getElementById('io-export-text').value = full;
        document.getElementById('io-dialog').style.display = 'flex';
      }

      function copyExport() {
        let el = document.getElementById('io-export-text');
        el.select();
        document.execCommand('copy');
        showMessage(currentLang === 'zh' ? '已複製' : 'Copied', 1500, true);
      }

      function confirmImport() {
        let txt = document.getElementById('io-import-text').value.trim();
        if (!txt) return;
        
        document.getElementById('io-dialog').style.display = 'none';
        document.getElementById('start-screen').style.display = 'none';
        
        // 將遊戲狀態完整初始化
        completedGamesNotation = "";
        arbEnded = false;
        gameNumber = 1;
        totalScores = { 1: 0, 2: 0 };
        startingPlayer = 1;
        resetGame(false);
        saveState();

        let tokens = txt.split(/\s+/);
        
        for (let i = 0; i < tokens.length; i++) {
          let token = tokens[i];
          if (token === "0") {
            arbEnded = true;
            executeArbitration(); 
            break;
          }
          
          let isGameStart = (pieces.length === 0 && tempPieces.length === 0);
          let t = parseInt(token[0]);
          let shapeType = (t === 1 || t === 2) ? 'B' : (t === 3 || t === 4) ? 'A' : 'C';
          let isFlipped = (t % 2 === 0);
          let svgId = getSvgIdForPlayer(shapeType, isFlipped, currentPlayer);
          
          if (isGameStart) {
            let ang = parseInt(token.substring(1));
            boardTransform.angle = (ang === 360 ? 0 : ang);
            
            let shape = SHAPES[SHAPE_MAP[svgId]];
            let { vertices } = buildShapeVertices(ZERO, 0, shape, isFlipped);
            let p = {
              id: pieces.length + tempPieces.length + 1,
              owner: currentPlayer,
              type: shapeType,
              svgId: svgId,
              isFlipped: isFlipped,
              vertices: vertices,
              parentId: null,
              level: 0,
              boardAngle: ang
            };
            tempPieces.push(p);
            piecesCount[svgId]--;
            actionCheck(); // 自動落子驗算
          } else {
            let me = parseInt(token[1]) - 1;
            let te = parseInt(token[token.length - 1]) - 1;
            let pid = parseInt(token.substring(2, token.length - 1));
            
            let tPiece = pieces.find(x => x.id === pid);
            let res = attachByEdge(shapeType, isFlipped, me, tPiece.vertices[te], tPiece.vertices[(te + 1) % 4], !!tPiece.isFlipped);
            
            let p = {
              id: pieces.length + tempPieces.length + 1,
              vertices: res.vertices,
              type: shapeType,
              owner: currentPlayer,
              svgId: svgId,
              isFlipped: isFlipped,
              targetId: tPiece.id,
              myEdge: me,
              targetEdge: te,
              parentId: tPiece.id,
              level: tPiece.level + 1
            };
            tempPieces.push(p);
            piecesCount[svgId]--;
            
            let req = (turnNumber === 1 || pieces.length === N_PIECES * 6 - 1) ? 1 : 2;
            if (tempPieces.length === req) {
              actionCheck(); 
            }
          }
          
          // 若導入過程中自動觸發了局數替換
          if (pieces.length === N_PIECES * 6 && gameNumber === 1 && i < tokens.length - 1) {
            nextGame();
          }
        }
      }

```
https://ejsoon.vip/
弈趣極光:享受思維樂趣
回复
  • 相似主题
    回复总数
    阅读次数
    最新帖子

在线用户

正浏览此版面之用户: 没有注册用户 和 10 访客