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

Re: 一個新的遊戲創意

帖子 ejsoon »

遊戲規則

中文

代码: 全选

<h1>頂鑫棋規則</h1>

<h2>遊戲目標</h2>
<p>我們把「一個棋子的端點正好落在另一個棋子的邊上」稱作「頂鑫結構」。本棋的目標就是形成「頂鑫結構」得到分數,兩局總分高者獲勝。</p>

<h2>落子規則</h2>
<p>先手方開始行動,之後雙方輪流行動。</p>
<p>每方每種棋子有9個,則每方一共有27個棋子。除了先手方的第一回合及後手方的最後回合只放一個棋子之外,每方每回合要放兩個棋子。</p>
<p>先手方首回合將把他的一個棋子放至棋盤中心,此後每一方的回合中,他所放的每一個棋子都要滿足以下條件:</p>
<p>1,每個棋子落下時,它的其中一條邊要完全貼合一個在場上已有的「對方棋子」的一條邊。</p>
<p>2,這個棋子不能跟場上已有的棋子的面重疊。</p>
<p>3,這個棋子不能有另一條邊跟場上的其它棋子貼合,包括部分重合但不完全貼合的情況。</p>
<p>4,這個棋子每一個懸空的端點都不能跟場上已有的棋子的端點重合。</p>
<p>5,這一回合所放的兩個棋子都一定要貼合到同一個「對方棋子」的兩個不同的邊上,對方在上一回合必須保證至少有一個棋子能使本方在這一回合中合規放置兩個棋子。</p>
<p>6,當兩手棋落下之後,其中一個棋子跟場上已有棋子或者這兩個棋子形成了頂鑫結構,則允許這兩手棋不落在同一個「對方棋子」上。</p>

<h2>計分規則</h2>
<p>1,當一個棋子的端點正好落在另一個棋子的邊上時,即形成了一個「頂鑫結構」,前者為「頂棋」,後者為「鑫棋」。</p>
<p>2,從鑫棋沿邊貼邊走到頂棋,每經過一個棋子計一分,頂棋和鑫棋本身不算,則經過多少個棋子就計多少分。</p>
<p>3,如果一個棋子被多個棋子在同一點頂到,則把從鑫棋沿邊貼邊走到這些「頂到它的棋子」中最近的一個看作是頂棋。</p>
<p>4,在遊戲開始前需要選擇是計分給頂棋所屬一方,還是計分給鑫棋所屬一方。</p>

<h2>勝負判定</h2>
<p>1,本遊戲需要互換先手玩兩局,之後把兩局的分數加起來,分高者獲勝。</p>
<p>2,當一方認為對方在上一回合違反規則,他可以申請仲裁,仲裁結果將直接決定最終勝負。</p>
英文

代码: 全选

原來你的命名邏輯是把「鑫」對應到 **TriGolden**(三金為鑫,非常精妙的意譯!),所以「頂鑫」必須包含「頂(Bracing)」的意思。這個考量非常合理!

在這種情況下,**Bracing TriGolden Structure** 是完全通順且合理的,甚至比 Brace TriGolden Structure 更符合英文文法。

### 為什麼 Bracing TriGolden Structure 很合適?

1. **文法結構正確**:
* **Bracing** 在這裡作為分詞修飾語(Adjectival participle),意思是「產生支撐/頂住作用的」。
* 整句結構變成:`[Bracing] [TriGolden Structure]`(具有頂持特性的鑫棋結構),邏輯非常清晰。


2. **對應精準**:
* **鑫棋** = TriGolden Piece
* **頂棋** = Bracing Piece
* **頂鑫結構** = Bracing TriGolden Structure



---

### 最終整合修訂(去除括號、更新術語)

將條款 4 的 `(free)` 去除,並把所有「頂鑫結構」統一替換為 **Bracing TriGolden Structure**:

```html
<h1>Brace the TriGolden Rules</h1>

<h2>Game Objective</h2>
<p>A "Bracing TriGolden Structure" is formed whenever a vertex of one piece rests exactly on an edge of another piece. The objective of the game is to score points by constructing "Bracing TriGolden Structures." The player with the higher cumulative score across two games wins.</p>

<h2>Placement Rules</h2>
<p>The first player takes the opening turn, after which players alternate turns.</p>
<p>Each player possesses 9 pieces of each piece type, totaling 27 pieces per player. Except for the first player's initial turn and the second player's final turn—during which only a single piece is placed—each player must place exactly two pieces on their turn.</p>

<p>The first player places their first piece at the center of the board. On all subsequent turns, every piece placed by a player must satisfy the following conditions:</p>
<p>1. Upon placement, one edge of the piece must be fully contiguous and aligned flush with an edge of an opponent's piece currently on the board.</p>
<p>2. The piece must not cause any interior overlap with the face of any piece already on the board.</p>
<p>3. No other edge of the piece may touch or align with any other piece on the board, including partial edge overlaps.</p>
<p>4. None of the piece's unattached vertices may coincide with any vertex of any piece already on the board.</p>
<p>5. Both pieces placed in the same turn must align with two distinct edges of the exact same opponent piece. The opponent is obligated on their preceding turn to ensure that at least one of their pieces permits a legal placement of two pieces on the current turn.</p>
<p>6. Exception: If placing both pieces forms a Bracing TriGolden Structure with existing pieces on the board or between the two newly placed pieces themselves, the two pieces are permitted to align with different opponent pieces.</p>

<h2>Scoring Rules</h2>
<p>1. When a vertex of one piece touches an edge of another piece, a "Bracing TriGolden Structure" is created. The piece contributing the vertex is designated the "Bracing Piece," and the piece contributing the edge is designated the "TriGolden Piece."</p>
<p>2. Trace an edge-to-edge path from the TriGolden Piece to the Bracing Piece along contiguous pieces. One point is awarded for each intermediate piece along the path (excluding the TriGolden Piece and the Bracing Piece themselves).</p>
<p>3. If a piece's edge is touched at the exact same point by the vertices of multiple pieces, the path distance is calculated to the closest touching piece when moving edge-to-edge from the TriGolden Piece, which is then regarded as the Bracing Piece.</p>
<p>4. Prior to the start of the game, players must agree whether points accrue to the owner of the Bracing Piece or the owner of the TriGolden Piece.</p>

<h2>Determining Victory</h2>
<p>1. A full match consists of two games with players swapping the opening initiative. Scores from both games are summed, and the player with the higher total score wins.</p>
<p>2. If a player claims that their opponent committed a rule violation during the preceding turn, they may request an official arbitration. The ruling of the arbitration immediately determines the outcome of the match.</p>

```
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
一枝独秀
帖子: 6446
注册时间: 2022年 11月 18日 17:36
联系:

Re: 一個新的遊戲創意

帖子 ejsoon »

修復和改進
一,應把工具欄放入棋盤區

現在在手機端,工具欄遮到了先手方的控制區,應把工具欄放入棋盤區(div.board-container)並絕對定位在最下方中間。

二,當輪到一方時應為turn-tracker增加active類

當輪到一方時應為當前的turn-tracker增加active類,同時去掉對方的active。

三,改進undo和redo

當後退和前進時,應同時改變雙方的turn-tracker的active狀態。

同時也要更新上回合的兩個或一個白點所標示的棋子。

四,改進數據格式以便未來做MCTS搜尋

在當前的數據格式中,存在以下問題:

1. **貼合關係沒有被持久化**。每次模擬(rollout)都要對所有棋子重新做 `segmentsOverlap` / `pointOnOpenSegment` 的 O(n²) 幾何檢查來找出合法落子與計分路徑(`getGraphPath` 是 BFS,也要重新掃 O(n²) 建圖)。MCTS 動輒成千上萬次 rollout,這種每步都要重新反推鄰接關係的做法會非常慢。
→ 建議在下棋時就直接維護一個**鄰接圖**(`pieceId -> [{neighborId, myEdge, neighborEdge}]`),落子時 O(1) 更新,計分/合法性檢查時直接查表,不用重掃全盤。

2. **狀態複製成本高**。目前 `pieces` 是物件陣列,每個棋子帶 4 個頂點(每個頂點又是 4 元組),`JSON.parse(JSON.stringify(...))` 做深拷貝(`saveState`/`loadState` 已經這樣做了)。MCTS 需要頻繁複製/回退狀態,這種深拷貝比較重。
→ 如果要跑真正的 MCTS,建議把狀態改成更緊湊的表示,例如「一串放置序列 + 增量式 undo/redo」而不是整盤深拷貝,或改用不可變資料結構 + 結構共享。

3. **合法落子生成是暴力窮舉**(對每個對方棋子的每條邊 × 己方每種棋型 × 翻轉 × 4×4 邊配對),沒有利用鄰接圖剪枝,`boardCanAcceptTwo` 更是 O(n × types × 4×4) 的雙重動作組合掃描。這在對局中期(幾十顆棋子)呼叫一次仲裁還好,但 MCTS 每個節點展開都要生成合法動作列表,這個複雜度會是效能瓶頸。

請檢查代碼,給出修改方案以解決上述問題。

同時在undo和redo中,是否能夠只對上一次的一兩個棋子進行操作?

棋子目前的id是怎樣確定的?能否用落子順序來確定棋子id?同時一個棋子的哪些面已經被使用,哪些面將無法使用,是否在數據中有標記?

回答要求:

請明確給出所要修改的地方和所要替換的代碼。

代码: 全选

修復和改進
一,應把工具欄放入棋盤區

現在在手機端,工具欄遮到了先手方的控制區,應把工具欄放入棋盤區(div.board-container)並絕對定位在最下方中間。

二,當輪到一方時應為turn-tracker增加active類

當輪到一方時應為當前的turn-tracker增加active類,同時去掉對方的active。

三,改進undo和redo

當後退和前進時,應同時改變雙方的turn-tracker的active狀態。

同時也要更新上回合的兩個或一個白點所標示的棋子。

四,改進數據格式以便未來做MCTS搜尋

在當前的數據格式中,存在以下問題:

1. **貼合關係沒有被持久化**。每次模擬(rollout)都要對所有棋子重新做 `segmentsOverlap` / `pointOnOpenSegment` 的 O(n²) 幾何檢查來找出合法落子與計分路徑(`getGraphPath` 是 BFS,也要重新掃 O(n²) 建圖)。MCTS 動輒成千上萬次 rollout,這種每步都要重新反推鄰接關係的做法會非常慢。
   → 建議在下棋時就直接維護一個**鄰接圖**(`pieceId -> [{neighborId, myEdge, neighborEdge}]`),落子時 O(1) 更新,計分/合法性檢查時直接查表,不用重掃全盤。

2. **狀態複製成本高**。目前 `pieces` 是物件陣列,每個棋子帶 4 個頂點(每個頂點又是 4 元組),`JSON.parse(JSON.stringify(...))` 做深拷貝(`saveState`/`loadState` 已經這樣做了)。MCTS 需要頻繁複製/回退狀態,這種深拷貝比較重。
   → 如果要跑真正的 MCTS,建議把狀態改成更緊湊的表示,例如「一串放置序列 + 增量式 undo/redo」而不是整盤深拷貝,或改用不可變資料結構 + 結構共享。

3. **合法落子生成是暴力窮舉**(對每個對方棋子的每條邊 × 己方每種棋型 × 翻轉 × 4×4 邊配對),沒有利用鄰接圖剪枝,`boardCanAcceptTwo` 更是 O(n × types × 4×4) 的雙重動作組合掃描。這在對局中期(幾十顆棋子)呼叫一次仲裁還好,但 MCTS 每個節點展開都要生成合法動作列表,這個複雜度會是效能瓶頸。

請檢查代碼,給出修改方案以解決上述問題。

同時在undo和redo中,是否能夠只對上一次的一兩個棋子進行操作?

棋子目前的id是怎樣確定的?能否用落子順序來確定棋子id?同時一個棋子的哪些面已經被使用,哪些面將無法使用,是否在數據中有標記?

回答要求:

請明確給出所要修改的地方和所要替換的代碼。
附件
trigolden_game2246.html.7z
(16.12 KiB) 已下载 5 次
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
一枝独秀
帖子: 6446
注册时间: 2022年 11月 18日 17:36
联系:

Re: 一個新的遊戲創意

帖子 ejsoon »

gemini 3.1 pro + thinking

代码: 全选

這裡為你提供完整的修改方案與代碼替換,直接解決 UI 顯示問題,並為了未來的 MCTS 搜尋大幅度優化了資料結構(建立邊界鄰接圖、使用落子順序作為 ID、以及改為增量式的 Undo/Redo)。

### 一、 將工具欄放入棋盤區

**修改位置**:HTML 結構
將 `<div id="toolbar">...</div>` 剪切並移動到 `<div class="board-container">` 的內部,放在 `<svg id="etanitrigolden">` 的下方。

**替換為:**

```html
    <div class="board-container">
      <svg id="etanitrigolden" version="1.1" xmlns="http://www.w3.org/2000/svg" width="480" height="480" viewBox="0 0 480 480" preserveAspectRatio="xMidYMid meet">
        </svg>
      
      <div id="toolbar">
        <svg class="tool-btn disabled" id="btn-undo" viewBox="0 0 24 24" onclick="undo()"><path d="M12.5 8c-2.65 0-5.05.99-6.9 2.6L2 7v9h9l-3.62-3.62c1.39-1.16 3.16-1.88 5.12-1.88 3.54 0 6.55 2.31 7.6 5.5l2.37-.78C21.08 11.03 17.15 8 12.5 8z"/></svg>
        <svg class="tool-btn disabled" id="btn-redo" viewBox="0 0 24 24" onclick="redo()"><path d="M11.5 8C16.15 8 20.08 11.03 21.63 15.22l-2.37.78C18.21 12.81 15.2 10.5 11.66 10.5c-1.96 0-3.73.72-5.12 1.88L10.16 16H1.16V7l3.44 3.44C6.45 8.99 8.85 8 11.5 8z"/></svg>
        <svg class="tool-btn" id="btn-arbitrate" viewBox="0 0 24 24" onclick="requestArbitration()"><path d="M9 11H7v2h2v-2zm4 0h-2v2h2v-2zm4 0h-2v2h2v-2zm2-7h-3V2h-8v2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V6c0-1.1-.9-2-2-2zm0 16H5V6h14v14z"/></svg>
        <svg class="tool-btn" id="btn-restore" viewBox="0 0 24 24" onclick="restoreArbWindow()"><path d="M4 4h16v16H4V4zm2 4v10h12V8H6z"/></svg>
      </div>
    </div>

```

*(注意:因為原有的 CSS 中 `#toolbar` 已經是 `position: absolute; bottom: 10px; left: 50%;`,放到相對定位的 `board-container` 內就會完美貼合棋盤底部居中。)*

---

### 二、 更新回合指示器 (Turn-tracker) 的 active 狀態

**修改位置**:`updateUI()` 函數內部
目前的 `updateUI` 只有更新面板的 `turn-active` 類,我們需要一併更新 `turn-tracker` 的 `active` 類。

**在 `updateUI()` 內部找到這兩行:**

```javascript
        document.getElementById('panel-p1').classList.toggle('turn-active', currentPlayer === 1);
        document.getElementById('panel-p2').classList.toggle('turn-active', currentPlayer === 2);

```

**在下方追加這兩行:**

```javascript
        document.getElementById('turn-tracker1').classList.toggle('active', currentPlayer === 1);
        document.getElementById('turn-tracker2').classList.toggle('active', currentPlayer === 2);

```

---

### 三 & 四、 優化數據格式以利 MCTS 與增量式 Undo/Redo

為了解決暴力搜索 O(n²) 與深拷貝性能太慢的問題,我們需做出以下資料結構調整:

1. **棋子 ID 序列化**:把 ID 從字串 `p_12345` 改成單純的陣列長度 (如 `0, 1, 2...`)。
2. **建立雙向鄰接圖 (edgeLinks)**:在 `generateGhosts` 時就記錄雙方的哪一條邊相連,並在 `actionCheck` 時寫入父棋子的資料。這樣一來,就可以清楚知道每個棋子的哪些邊(0~3)已經被占用。
3. **路徑搜索 (BFS) 改走鄰接圖**:修改 `getGraphPath`,取消幾何計算,直接跑 O(1) 的關係圖。
4. **增量 Undo/Redo**:不再把整盤幾百顆棋子的座標進行 `JSON.parse` 深拷貝,只拷貝「本回合新增的 1~2 顆棋子」,Undo 時執行陣列 `pop()` 並移除父節點關係。

#### 1. 產生合法棋子與標記邊界關係 (O(1) 查表基礎)

**修改位置**:`generateGhosts()`
在裡面為合法生成的幽靈棋子附加 `edgeLinks` 屬性。

**將 `generateGhosts()` 中的 `let gp = { ... };` 替換為:**

```javascript
              let gp = {
                id: pieces.length + tempPieces.length, // 使用落子順序當作唯一 ID
                vertices: res.vertices,
                type: selType,
                owner: currentPlayer,
                svgId: selectedTile,
                isFlipped: selMir,
                edgeLinks: {},         // 記錄本棋子接出去的關係 { 本地邊索引: 目標棋子ID }
                targetId: tPiece.id,   // 依附的目標棋子 ID
                myEdge: j,             // 自己貼合的邊 index (0~3)
                targetEdge: i          // 對方被貼合的邊 index (0~3)
              };
              // 記錄鄰接圖:把自己的這條邊指向上一個棋子
              gp.edgeLinks[j] = tPiece.id;

```

*(可選)* 如果要在 `commitGhost` 或是 `executeArbitration` 中也確保 ID 一致,可以把 `executeArbitration` 裡的 `g1.id = 'p_' + ...` 改成 `g1.id = pieces.length; g2.id = pieces.length + 1;`。

#### 2. 確認落子時,建立雙向圖連結

**修改位置**:`actionCheck(that = false)`
當棋子確定從 `tempPieces` 併入 `pieces` 前,我們要把關係「反向」寫回被貼合的對方棋子上。

**在 `actionCheck` 中,找到 `pieces = allNewP;` 這一行,在它【之前】插入以下代碼:**

```javascript
        // 建立雙向鄰接圖,將暫存棋子連結寫入父節點,MCTS 搜索與路徑計算時可直接查表
        tempPieces.forEach(tp => {
          if (tp.targetId !== undefined) {
            let parent = allNewP.find(p => p.id === tp.targetId);
            if (parent) {
              if (!parent.edgeLinks) parent.edgeLinks = {};
              parent.edgeLinks[tp.targetEdge] = tp.id; // 父節點的該邊標記為已使用
            }
          }
        });
        
        lastTurnPieces = JSON.parse(JSON.stringify(tempPieces)); // 深拷貝保存當回合用以復原與顯示白點

```

#### 3. 大幅優化計分路徑算法 (BFS 查圖)

**修改位置**:`getGraphPath()`
將原本雙重迴圈逐點比對的幾何碰撞拔除,換成直接讀取 `edgeLinks`,速度將提升百倍,直接利好 MCTS。

**將原有的 `getGraphPath` 函數完整替換為:**

```javascript
      function getGraphPath(start, end, allP) {
        let q = [{ node: start, dist: 0, path: [start] }];
        let visited = new Set([start.id]);
        while (q.length > 0) {
          let curr = q.shift();
          if (curr.node.id === end.id) return curr.path;
          
          // MCTS 優化:直接讀取雙向圖 edgeLinks,免除 O(n²) 幾何計算
          if (curr.node.edgeLinks) {
            for (let edgeIdx in curr.node.edgeLinks) {
              let neighborId = curr.node.edgeLinks[edgeIdx];
              if (!visited.has(neighborId)) {
                let nextNode = allP.find(p => p.id === neighborId);
                if (nextNode) {
                  visited.add(neighborId);
                  q.push({ node: nextNode, dist: curr.dist + 1, path: [...curr.path, nextNode] });
                }
              }
            }
          }
        }
        return null;
      }

```

#### 4. 增量操作的 Undo / Redo / SaveState

**修改位置**:覆蓋原有的 `saveState`, `loadState`, `undo`, `redo`。
現在我們不深拷貝整盤 `pieces`,只存儲「這回合下了哪幾步」,撤銷時只對 `pieces` 做 `pop()` 彈出最後的 1~2 個棋子,並清理邊界狀態。白點 (`lastTurnPieces`) 的顯示與 UI 切換也會隨之完美連動。

**將整段 State 管理代碼替換為以下實作:**

```javascript
      function saveState() {
        gameHistory.length = historyIndex + 1; // 截斷未來的歷史
        gameHistory.push({
          // 增量存儲:只拷貝當回合新增的棋子,不存儲整盤
          newPieces: JSON.parse(JSON.stringify(lastTurnPieces)), 
          scores: { ...scores },
          piecesCount: { ...piecesCount },
          turnNumber: turnNumber,
          currentPlayer: currentPlayer,
          scoredVictims: Array.from(scoredVictims)
        });
        historyIndex++;
        updateToolbar();
      }

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

          // 1. 增量移除:只彈出上回合落下的 1~2 個棋子
          let removeCount = currentState.newPieces ? currentState.newPieces.length : 0;
          for (let i = 0; i < removeCount; i++) {
            let p = pieces.pop();
            // 斷開雙向鄰接圖的父節點端
            if (p && p.targetId !== undefined) {
              let parent = pieces.find(x => x.id === p.targetId);
              if (parent && parent.edgeLinks) {
                delete parent.edgeLinks[p.targetEdge];
              }
            }
          }

          // 2. 恢復其他狀態
          scores = { ...prevState.scores };
          piecesCount = { ...prevState.piecesCount };
          turnNumber = prevState.turnNumber;
          currentPlayer = prevState.currentPlayer;
          scoredVictims = new Set(prevState.scoredVictims);
          // 恢復上上回合的白點標記
          lastTurnPieces = prevState.newPieces ? JSON.parse(JSON.stringify(prevState.newPieces)) : [];
          tempPieces = [];
          ghosts = [];
          selectedTile = null;
          targetOpponentPieceId = null;

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

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

          // 1. 增量新增:把該回合的棋子加回來
          let piecesToAdd = nextState.newPieces ? JSON.parse(JSON.stringify(nextState.newPieces)) : [];
          for (let p of piecesToAdd) {
            pieces.push(p);
            // 恢復雙向鄰接圖的父節點端
            if (p.targetId !== undefined) {
              let parent = pieces.find(x => x.id === p.targetId);
              if (parent) {
                if (!parent.edgeLinks) parent.edgeLinks = {};
                parent.edgeLinks[p.targetEdge] = p.id;
              }
            }
          }

          // 2. 恢復其他狀態
          scores = { ...nextState.scores };
          piecesCount = { ...nextState.piecesCount };
          turnNumber = nextState.turnNumber;
          currentPlayer = nextState.currentPlayer;
          scoredVictims = new Set(nextState.scoredVictims);
          lastTurnPieces = piecesToAdd;
          tempPieces = [];
          ghosts = [];
          selectedTile = null;
          targetOpponentPieceId = null;

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

      // 由於改採增量操作,已不再需要全量覆蓋的 loadState 函數,原有的 loadState 可以刪除。

```

### 數據中的「可用面」如何標記?

經過上述修改後,對於任何一個棋子 `P`,其哪些邊已被使用將直接記錄在 **`P.edgeLinks`** 物件中。

* 如果 `P.edgeLinks[0]` 存在,代表它的第 0 條邊(即 `P.vertices[0]` 到 `P.vertices[1]`)已經貼合了另一個棋子,這條邊將**無法再被使用**。
* 未來實作 MCTS 時,只需判斷 `if (!opp.edgeLinks || !opp.edgeLinks[i])`,即可確認這條邊是否還是空著的,省去了大量的幾何碰撞檢查。
附件
trigolden_game2247.html.7z
(16.84 KiB) 已下载 2 次
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
一枝独秀
帖子: 6446
注册时间: 2022年 11月 18日 17:36
联系:

Re: 一個新的遊戲創意

帖子 ejsoon »

修復和改進
一,計分邏輯出錯

現在形成頂鑫結構完全不會計分,查找原因並修復。

二,增加旋轉功能

在工具欄的最後,增加一個意為「旋轉」的svg按鈕。圖案為上下兩個相反的弧形箭頭。當點擊使之激活時,輪到後手方,則把雙方的控制區旋轉180度,工具欄也將旋轉180度後置於頂部,消息框也旋轉180度後置於底部。輪到先手方就再恢復原狀。

三,先手方首回合放置改進

當先手方第一手棋放置時,棋子的中心應對應棋盤的中心(坐標是0,0),且旋轉角度和翻轉狀態都應跟控制區中的棋子一致。

這時如果用手機觸屏或電腦鼠標拖動棋盤,不會平移,而是以棋盤中心為中心旋轉棋子,旋轉角度數值應為整數。如果縮放,應保持棋盤中心不變。

在旋轉的同時,消息框應顯示當前旋轉角度,不是增量,而是當前值。

當回退到棋盤上沒有棋子時,棋盤的平移和縮放將回到初始狀態,以使先手方第一手能把棋子放至棋盤中心。。

四,手機端改小工具欄按鈕

當手機端時,工具按鈕應變小。

五,增加下載功能

當點擊下載按鈕時,應能直接下載當前的棋盤svg,文件名為BraceTheTriGolden_yyyymmdd_hhiiss.svg。

下載按鈕在前面增加的「旋轉按鈕」的左邊。

回答要求:

請明確給出所要修改的地方和所要替換的代碼。

代码: 全选

修復和改進
一,計分邏輯出錯

現在形成頂鑫結構完全不會計分,查找原因並修復。

二,增加旋轉功能

在工具欄的最後,增加一個意為「旋轉」的svg按鈕。圖案為上下兩個相反的弧形箭頭。當點擊使之激活時,輪到後手方,則把雙方的控制區旋轉180度,工具欄也將旋轉180度後置於頂部,消息框也旋轉180度後置於底部。輪到先手方就再恢復原狀。

三,先手方首回合放置改進

當先手方第一手棋放置時,棋子的中心應對應棋盤的中心(坐標是0,0),且旋轉角度和翻轉狀態都應跟控制區中的棋子一致。

這時如果用手機觸屏或電腦鼠標拖動棋盤,不會平移,而是以棋盤中心為中心旋轉棋子,旋轉角度數值應為整數。如果縮放,應保持棋盤中心不變。

在旋轉的同時,消息框應顯示當前旋轉角度,不是增量,而是當前值。

當回退到棋盤上沒有棋子時,棋盤的平移和縮放將回到初始狀態,以使先手方第一手能把棋子放至棋盤中心。。

四,手機端改小工具欄按鈕

當手機端時,工具按鈕應變小。

五,增加下載功能

當點擊下載按鈕時,應能直接下載當前的棋盤svg,文件名為BraceTheTriGolden_yyyymmdd_hhiiss.svg。

下載按鈕在前面增加的「旋轉按鈕」的左邊。

回答要求:

請明確給出所要修改的地方和所要替換的代碼。
gemini 3.1 pro + thinking

代码: 全选

這些修改能大幅提升《頂鑫棋》的遊戲體驗與邏輯嚴謹度。以下為針對你提出的五點需求,具體的代碼修改方案:

### 一、修復頂鑫結構無法計分的問題

問題出在原代碼中 `getGraphPath` 只檢查單向的父子關係圖(`edgeLinks`),導致剛落下的暫存棋子無法正確與其他棋子連通。我們需要將鄰接圖改為雙向查找。

找到 `function getGraphPath(start, end, allP)` 函數,替換為以下代碼:

```javascript
function getGraphPath(start, end, allP) {
  let q = [{ node: start, dist: 0, path: [start] }];
  let visited = new Set([start.id]);
  
  while (q.length > 0) {
    let curr = q.shift();
    if (curr.node.id === end.id) return curr.path;

    // 建立雙向查找,包含自己指向別人的,以及別人指向自己的
    let neighbors = [];
    if (curr.node.edgeLinks) {
      Object.values(curr.node.edgeLinks).forEach(id => neighbors.push(id));
    }
    allP.forEach(p => {
      if (p.edgeLinks && Object.values(p.edgeLinks).includes(curr.node.id)) {
        neighbors.push(p.id);
      }
    });

    for (let nId of neighbors) {
      if (!visited.has(nId)) {
        visited.add(nId);
        let nNode = allP.find(p => p.id === nId);
        if (nNode) {
          q.push({ node: nNode, dist: curr.dist + 1, path: [...curr.path, nNode] });
        }
      }
    }
  }
  return null;
}

```

### 二、增加旋轉與下載功能,並縮小手機端工具按鈕

我們需要在工具欄加入這兩個按鈕,並加入對應的 CSS 樣式及邏輯。

**1. 修改 CSS 樣式**
在 `<style>` 中尋找 `@media (max-width: 768px)`,在裡面加入按鈕縮小的樣式;同時在外面加入控制旋轉的樣式:

```css
      /* 新增的旋轉視角樣式 */
      body.rotated-view .panel {
        transform: rotate(180deg);
      }
      body.rotated-view .board-container {
        transform: rotate(180deg);
      }
      body.rotated-view #toolbar {
        top: 10px;
        bottom: auto;
        transform: translateX(-50%) rotate(180deg);
      }
      body.rotated-view #message-box {
        top: auto;
        bottom: 20px;
        transform: translateX(-50%) rotate(180deg);
      }

      /* Mobile Layout 區塊更新 */
      @media (max-width: 768px) {
        /* ...原有的 mobile 樣式保留,新增以下: */
        .tool-btn {
          width: 24px;
          height: 24px;
        }
        #toolbar {
          gap: 10px;
          padding: 6px 10px;
        }
      }

```

**2. 在工具欄新增按鈕**
找到 `<div id="toolbar">`,在裡面現有的 `<svg>` 按鈕**最右側**(取代原先的空白按鈕),加入下載與旋轉按鈕:

```html
      <div id="toolbar">
        <svg class="tool-btn" id="btn-download" viewBox="0 0 24 24" onclick="downloadSVG()">
          <path fill="currentColor" d="M19 9h-4V3H9v6H5l7 7 7-7zM5 18v2h14v-2H5z" />
        </svg>

        <svg class="tool-btn" id="btn-rotate" viewBox="0 0 24 24" onclick="toggleAutoRotate()">
          <path fill="currentColor" d="M12 4V1L8 5l4 4V6c3.31 0 6 2.69 6 6 0 1.01-.25 1.97-.7 2.8l1.46 1.46C19.54 15.03 20 13.57 20 12c0-4.42-3.58-8-8-8zm0 14c-3.31 0-6-2.69-6-6 0-1.01.25-1.97.7-2.8L5.24 7.74C4.46 8.97 4 10.43 4 12c0 4.42 3.58 8 8 8v3l4-4-4-4v3z"/>
        </svg>
      </div>

```

**3. 添加下載與旋轉的 JavaScript 邏輯**
在 `<script>` 的頂部(全域變數區)加入:

```javascript
      let autoRotate = false;

```

在指令碼末端(`function updateTransform()` 附近)加入這三個函數:

```javascript
      function toggleAutoRotate() {
        autoRotate = !autoRotate;
        document.getElementById('btn-rotate').style.fill = autoRotate ? '#7289da' : '#ccc';
        applyRotation();
      }

      function applyRotation() {
        if (autoRotate && currentPlayer === 2) {
          document.body.classList.add('rotated-view');
        } else {
          document.body.classList.remove('rotated-view');
        }
      }

      function downloadSVG() {
        const svg = document.getElementById('etanitrigolden').cloneNode(true);
        const serializer = new XMLSerializer();
        let source = serializer.serializeToString(svg);
        if(!source.match(/^<svg[^>]+xmlns="http\:\/\/www\.w3\.org\/2000\/svg"/)){
          source = source.replace(/^<svg/, '<svg xmlns="http://www.w3.org/2000/svg"');
        }
        source = '<?xml version="1.0" standalone="no"?>\r\n' + source;
        const url = "data:image/svg+xml;charset=utf-8," + encodeURIComponent(source);

        const a = document.createElement('a');
        a.href = url;
        const now = new Date();
        const pad = n => (n < 10 ? '0'+n : n);
        const filename = `BraceTheTriGolden_${now.getFullYear()}${pad(now.getMonth()+1)}${pad(now.getDate())}_${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}.svg`;
        a.download = filename;
        document.body.appendChild(a);
        a.click();
        document.body.removeChild(a);
      }

```

*注意:你還需要在 `actionCheck`、`undo`、`redo`、`resetGame` 函數改變 `currentPlayer` 後(約函數末尾處),呼叫 `applyRotation();` 以自動觸發視角旋轉。*

### 三、先手方首回合放置中心化與拖動旋轉改進

為了在首回合完美對齊中心,並且支持自定義整數旋轉,我們需要修改數學轉換與鼠標控制邏輯。

**1. 修改 `showMessage` 使其支援直接顯示字串**
找到 `showMessage` 函數並替換:

```javascript
      function showMessage(key, duration = 2400) {
        let box = document.getElementById('message-box');
        box.innerText = i18n[key] ? i18n[key][currentLang] : key;
        box.style.display = 'block';
        clearTimeout(msgTimeout);
        if (duration > 0) {
          msgTimeout = setTimeout(() => {
            box.style.display = 'none';
          }, duration);
        }
      }

```

**2. 加入全域旋轉變數**
在全域變數宣告區加入:

```javascript
      let firstPieceCenter = null;
      let globalRotation = 0;

```

**3. 更新 `exactToScreen` 函數**
替換現有的 `exactToScreen`,讓首步棋的中心絕對歸零,並套用全局旋轉:

```javascript
      function exactToScreen(p) {
        const PI5 = Math.PI / 5;
        let [a, b, c, d] = p;
        if (firstPieceCenter) {
          a -= firstPieceCenter[0];
          b -= firstPieceCenter[1];
          c -= firstPieceCenter[2];
          d -= firstPieceCenter[3];
        }
        const x = a + b * Math.cos(PI5) + c * Math.cos(2 * PI5) + d * Math.cos(3 * PI5);
        const y = b * Math.sin(PI5) + c * Math.sin(2 * PI5) + d * Math.sin(3 * PI5);
        let sx = 36 * x;
        let sy = -36 * y;
        if (globalRotation !== 0) {
          let rad = globalRotation * Math.PI / 180;
          let cos = Math.cos(rad);
          let sin = Math.sin(rad);
          return { x: sx * cos - sy * sin, y: sx * sin + sy * cos };
        }
        return { x: sx, y: sy };
      }

```

**4. 攔截首步棋的選取中心化**
在 `selectTile(player, tileId)` 函數中,找到 `if (pieces.length === 0)` 的區塊,修改 `tempPieces` 的賦值並記錄中心與視角:

```javascript
        if (pieces.length === 0) {
          // ... 前方保留 ...
          tempPieces = [
            {
              id: 'p_' + Date.now(),
              owner: currentPlayer,
              type: SHAPE_MAP[tileId],
              svgId: tileId,
              isFlipped: isFlipped[tileId],
              vertices: vertices
            }
          ];
          
          // 紀錄準確幾何中心
          firstPieceCenter = [
            vertices.reduce((s, v) => s + v[0], 0) / 4,
            vertices.reduce((s, v) => s + v[1], 0) / 4,
            vertices.reduce((s, v) => s + v[2], 0) / 4,
            vertices.reduce((s, v) => s + v[3], 0) / 4
          ];
          globalRotation = 0;
          boardTransform = { tx: 240, ty: 240, scale: 1 };
          updateTransform();

          piecesCount[tileId]--;
        }

```

*提示:在 `actionCross` 和 `resetGame` 當 `pieces.length === 0 && tempPieces.length === 0` 時,記得加上 `firstPieceCenter = null; globalRotation = 0;` 來清除狀態。*

**5. 替換鼠標/觸摸控制邏輯 (Pan, Zoom, 旋轉)**
將 `// Pan & Zoom` 底下的所有 EventListener 替換為以下代碼,這完美結合了縮放置中和第一步旋轉的規則:

```javascript
      // Pan, Zoom & First Piece Rotation
      let isDragging = false;
      let startX, startY;
      let startAngle = 0;
      let initialGlobalRot = 0;

      boardSvg.addEventListener('pointerdown', e => {
        if (pieces.length === 0 && tempPieces.length === 0) return;
        isDragging = true;
        if (pieces.length === 0 && tempPieces.length === 1) {
          let rect = boardSvg.getBoundingClientRect();
          let cx = rect.left + rect.width / 2;
          let cy = rect.top + rect.height / 2;
          startAngle = Math.atan2(e.clientY - cy, e.clientX - cx) * 180 / Math.PI;
          initialGlobalRot = globalRotation;
        } else {
          startX = e.clientX - boardTransform.tx;
          startY = e.clientY - boardTransform.ty;
        }
      });

      boardSvg.addEventListener('pointermove', e => {
        if (!isDragging) return;
        if (pieces.length === 0 && tempPieces.length === 1) {
          let rect = boardSvg.getBoundingClientRect();
          let cx = rect.left + rect.width / 2;
          let cy = rect.top + rect.height / 2;
          let currentAngle = Math.atan2(e.clientY - cy, e.clientX - cx) * 180 / Math.PI;
          globalRotation = Math.round(initialGlobalRot + (currentAngle - startAngle));
          globalRotation = (globalRotation % 360 + 360) % 360; // 確保為 0-359 整數
          
          let rotText = currentLang === 'zh' ? `當前旋轉角度: ${globalRotation}°` : `Current Angle: ${globalRotation}°`;
          showMessage(rotText, 0); // 持續顯示
          renderBoard();
        } else {
          boardTransform.tx = e.clientX - startX;
          boardTransform.ty = e.clientY - startY;
          updateTransform();
        }
      });

      const stopDrag = () => {
        isDragging = false;
        if (pieces.length === 0 && tempPieces.length === 1) {
          document.getElementById('message-box').style.display = 'none';
        }
      };
      boardSvg.addEventListener('pointerup', stopDrag);
      boardSvg.addEventListener('pointerleave', stopDrag);

      boardSvg.addEventListener('wheel', e => {
        e.preventDefault();
        if (pieces.length === 0 && tempPieces.length === 0) return;
        const ds = e.deltaY > 0 ? 0.9 : 1.1;
        const nextScale = boardTransform.scale * ds;
        if (nextScale < 0.2 || nextScale > 5) return;

        const rect = boardSvg.getBoundingClientRect();
        // 如果是第一步,以屏幕中心為縮放點;否則依鼠標位置縮放
        const mx = (pieces.length === 0 && tempPieces.length === 1) ? (rect.width / 2) : (e.clientX - rect.left);
        const my = (pieces.length === 0 && tempPieces.length === 1) ? (rect.height / 2) : (e.clientY - rect.top);

        boardTransform.tx = mx - (mx - boardTransform.tx) * ds;
        boardTransform.ty = my - (my - boardTransform.ty) * ds;
        boardTransform.scale = nextScale;
        updateTransform();
      });

      let pinchStartDist = 0;
      boardSvg.addEventListener('touchstart', e => {
        if (e.touches.length === 2 && (pieces.length > 0 || tempPieces.length > 0)) {
          isDragging = false;
          pinchStartDist = Math.hypot(
            e.touches[0].clientX - e.touches[1].clientX,
            e.touches[0].clientY - e.touches[1].clientY
          );
        }
      });

      boardSvg.addEventListener('touchmove', e => {
        if (e.touches.length === 2 && (pieces.length > 0 || tempPieces.length > 0)) {
          e.preventDefault();
          const dist = Math.hypot(
            e.touches[0].clientX - e.touches[1].clientX,
            e.touches[0].clientY - e.touches[1].clientY
          );
          const ds = dist / pinchStartDist;
          const nextScale = boardTransform.scale * ds;
          if (nextScale > 0.2 && nextScale < 5) {
            const rect = boardSvg.getBoundingClientRect();
            const mx = (pieces.length === 0 && tempPieces.length === 1) ? (rect.width / 2) : ((e.touches[0].clientX + e.touches[1].clientX) / 2 - rect.left);
            const my = (pieces.length === 0 && tempPieces.length === 1) ? (rect.height / 2) : ((e.touches[0].clientY + e.touches[1].clientY) / 2 - rect.top);
            
            boardTransform.tx = mx - (mx - boardTransform.tx) * ds;
            boardTransform.ty = my - (my - boardTransform.ty) * ds;
            boardTransform.scale = nextScale;
            updateTransform();
          }
          pinchStartDist = dist;
        }
      }, { passive: false });

```
附件
trigolden_game2249.html.7z
(17.56 KiB) 已下载 2 次
https://ejsoon.vip/
弈趣極光:享受思維樂趣
回复
  • 相似主题
    回复总数
    阅读次数
    最新帖子

在线用户

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