分享和讨论Javascript相关的话题
回复
头像
ejsoon
一枝独秀
一枝独秀
帖子: 5823
注册时间: 2022年 11月 18日 17:36
联系:

Re: 將改進混亂時鐘的網頁版

帖子 ejsoon »

小改:

代码: 全选

為混亂時鐘(ChaosClock)遊戲,寫js程式:
1.取到頁面上所有的object.cclok_main,下面將對每一個object進行操作,object之間不能發生干涉。

2.用「const doc = obj.contentDocument;」拿到object的內容,它將是一個svg,其內部的.etdrop>use[href=tilen]稱作棋子(其中n是0~11的整數,代表棋子1~12),#canclearea稱作「取消區域」,#path900稱作「時針」,#path1029稱作「分針」,#ckgleam稱作「反光面」。

3.下面介紹棋子的「正位」。我們用一個長度為12、每一項都是數字的陣列來表示棋子的位置,則每一個棋子的正位是[1,2,3,4,5,6,7,8,9,10,11,12]。在cclok上,棋子12的正位是transform="translate(0,-144)",棋子1的正位是從(0,-144)開始,以(0,0)為圓心,以144為半徑順時針旋轉30度而得到的坐標,棋子2的正位是(0,-144)旋轉60度得到的坐標,棋子3的正位則是(0,-144)旋轉90度得到的(144,0)",依次類推。

當我們用一個陣列來表示棋局的盤面時,就是按順序將棋子放入正位。比如[3,1,11,5,7,10,12,6,2,9,8,4],說明棋子3處於棋子1的正位,棋子1處於棋子2的正位,依次類推。

4.當cclok的ckvalue屬性值為空時,程式將隨機生成一個局面,要求所有棋子都不能處於它的正位上。並且在object遊戲下方生成一個「隨機局面」按鈕,每當點擊一次則會再隨機生成一個新的局面。

5.當cclok的ckvalue屬性值不為空時,則它的值會是用逗號分隔的12個數字,比如ckvalue="3,1,11,5,7,10,12,6,2,9,8,4",則程式將按此擺放棋子的位置。

6.在每個object遊戲下方生成一個「開始遊戲」按鈕,點擊則開始遊戲。「開始遊戲」按鈕將消失。下方還有一個output顯示遊戲狀態,為「等待開始」、「遊戲開始」、「輪到甲方/乙方」、「遊戲結束」等文字提示。

7.當遊戲開始,輪到乙方。乙方行動結束,則輪到甲方。每方在他的一次行動中可在「走子、落子、放棄」三者選擇其一。

8.棋子放置的地方分為兩種:「鐘面上」和「手上」。前面介紹的「正位」都是在鐘面上,手上的位置指:甲方(-208,y),乙方(208,y),y是在「-200,-120,-40,40,120,200」的其中一個。

為每個棋子加入點擊事件。

當輪到乙方或甲方時,他可以點擊鐘面上的任何一個棋子,稱作「走子」。

每個棋子被點擊後開始走子,行走的步數等於棋子本身的數字。

如果走後停下的地方存在其它棋子,則這個棋子將被吃掉,交給這個棋子的主人手上。單數棋子交給甲方,雙數交給乙方。

比如「3,1,11,5,7,10,12,6,2,9,8,4」,當乙方點擊棋子3時,盤面將變為「0,1,11,3,7,10,12,6,2,9,8,4」,即棋子3把棋子5吃掉了,現在棋子5在甲方的手上,棋子3處於棋子4的正位,輪到甲方行動。

在盤面描述中,如果走到盡頭,會從前面開始繼續走。比如「3,1,11,5,7,10,12,6,2,9,8,4」如果走棋子4的話會吃到棋子5。

棋子12會繞一圈後把自己吃掉。

這些拿在手上的棋子將用於「落子」。當點擊一個手上的棋子,它將直接落在它的正位上。如果此時它的正位上有其它棋子,則這個棋子將被吃掉,交給它的主人。如果落子吃子吃到的是對方的棋子,則本回合本方可再額外獲得一次行動機會。

對方上一步剛使用過的棋子,己方本次行動將不可使用。

處於正位的棋子,既不可以走子,也不可以被吃掉。如果某個棋子在走子之後,將吃到一個處於正位的棋子,則不可執行。

點擊「取消區域」,則取消本次行動。

行動結束,則換成對方行動。

當乙方所有雙數棋子都先置於正位,乙贏甲輸;當甲方所有單數棋子都先置於正位,乙方緊接著下一步也都能使他的所有雙數棋子置於正位,則為甲乙共贏;當甲方所有單數棋子都先置於正位,乙方緊接著下一步不能使他的所有雙數棋子置於正位,則為甲贏乙輸;如果雙方接連取消行動,則為甲乙同輸。

9.當一個棋子在走子或落子時,將首先把它在DOM中移至其父元素的最底部。

棋子在盤面上走子時,用js控制控制其transform="translate(),保持與(0,0)距離為144進行旋轉。

當一個棋子落子時,直接使用js平移動畫控制其transform="translate()"。

當一個棋子被吃時,它首先瞬間變透明度為0.36,之後等待吃它的棋子移到位,之後選擇一個y值,0.36秒移到手上區域。移到之後透明度變回1。y值上已有棋子的則不能再放棋子。

當遊戲未開始時,分針的transform-rotate=0,時針的transform-rotate=-90。

當輪到乙方時,時針的角度應為90+360n(朝右),輪到甲方時應為270+360n(朝左),n為從0開始的整數。用js控置其旋轉動畫。

當某一方因為落子吃到對方棋子而獲得額外一次行動機會時,時針應旋轉360度。

當遊戲結束時,分針的角度要加上180(朝下)。如果是甲方獲勝,時針指向甲方(朝左);如果是乙方獲勝,時針指向乙方(朝右);如果是共贏,時針朝上(360n);如果同輸,時針朝下(180+360n)。

當遊戲再次開始時,分針會再加上180(朝上),時針則會朝向當前的行動方。

以上js控制的動畫都是用時1s。

當js對時針和分針的transform操作時,它裡面已經存在translate,scale,不要使它消失。當它轉動時,應從上一個角度開始增加角度。

js進行動畫時,只實時操作svg元素的屬性,不要增加和改動任何style。

10.當一次行動開始時(點擊棋子之後),js將控制反光面的opacity從0到1再到0,用時0.36秒。

反光面的透明度變化、棋子的移動、時針的轉動,是同時發生的。

當有棋子被誤點時,應有3秒鐘的臨時文字提示,之後再恢復回原來的文字提示。臨時文字提示包括「上一步剛走完的棋子不可移動、處於正位的棋子不可移動、處於正位的棋子不可被吃、單數棋子只能由甲方落子、雙數棋子只能由乙方落子」。

11.當遊戲結束時,「重新開始」按鈕顯示。如果是cclok的ckvalue屬性值為空的情況,則「隨機局面」按鈕將會出現。

每次點擊「隨機局面」按鈕,首先所有棋子消失,分針旋轉180度到朝上,用時0.8秒,之後分針旋轉360度,用時1.2s,在分針旋轉的同時,從正位1開始每隔0.1秒顯示一個棋子。

「重新開始」會會把棋子放回初始盤面,其動畫效果跟按「隨機局面」時一樣。

12.在棋譜中記錄方法為「初始盤面;初始行動方;行動序列;結果」。初始盤面前面已介紹。初始行動方如果是乙方則為1,甲方則為0,默認為1。行動序列中用逗號隔開,因為吃子而產生的連續行動用-連接。結果用1、2、3、4來表示初始行動方的勝負情況,4表示贏,3為共贏,2為同輸,1表示輸。

比如「3,1,11,5,7,10,12,6,2,9,8,4;1;3,1,10,5,2,7-10;4」,「;1」表示初始盤面乙方先行(0就是甲方),乙方走3(吃掉5),甲方走1,乙方走10,甲方落5(吃掉7),乙方走2,甲方落7(吃掉12)再走10。「;4」表示乙方獲勝。
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
一枝独秀
一枝独秀
帖子: 5823
注册时间: 2022年 11月 18日 17:36
联系:

Re: 將改進混亂時鐘的網頁版

帖子 ejsoon »

1334:
時針和分針每次旋轉時,都應是順時針方向旋轉。給出所要修改的地方。

gemini fast:

代码: 全选

要實現時針與分針永遠以**順時針**方向旋轉,關鍵在於計算旋轉角度時,必須確保「目標角度」永遠大於「當前角度」。

在你的程式碼中,目前的 `hourRotation` 計算方式使用了絕對角度(如 90 或 270),這會導致指針為了到達目標而選擇最短路徑(有時會逆時針回轉)。

以下是需要修改的三個主要地方:

---

### 1. 修改 `startGame` 中的時針初始化
在遊戲開始時,時針從 `-90` 到 `90` 是順時針,但為了保險起見,建議明確邏輯。

```javascript
// 尋找此行並修改:
async startGame() {
    // ... 前略
    this.setNormalMessage('遊戲開始,輪到乙方');
    
    // 確保目標角度大於當前角度 (-90 -> 90 是增加 180 度,為順時針)
    this.hourRotation = 90; 
    // ... 後略
}
```

---

### 2. 修改 `switchTurn` 中的時針邏輯 (核心修改)
這是控制每回合切換時,時針轉動方向的地方。我們需要加上邏輯,確保 `this.hourRotation` 永遠遞增。

```javascript
// 將 switchTurn 修改如下:
switchTurn(updatePointer = true) {
    const prevTurn = this.turn;
    this.turn = this.turn === 1 ? 0 : 1;

    if (updatePointer) {
        // 取得當前角度的基礎圈數 (Math.floor(this.hourRotation / 360) * 360)
        let baseRotation = Math.floor(this.hourRotation / 360) * 360;
        let targetAngle = this.turn === 1 ? 90 : 270;
        
        // 核心邏輯:如果目標角度小於或等於當前角度,就加 360 度,強迫順時針轉動
        let nextRotation = baseRotation + targetAngle;
        if (nextRotation <= this.hourRotation) {
            nextRotation += 360;
        }
        this.hourRotation = nextRotation;
        
        this.updatePointersRealtime();
    }
    // ... 後略
}
```

---

### 3. 修改 `endGame` 中的勝負旋轉
在遊戲結束時,指針會指向特定位置。同樣需要確保目標角度大於當前值。

```javascript
// 修改 endGame 中的角度賦值邏輯:
endGame(resultCode) {
    this.state = 'ended';
    // ...
    
    // 分針增加 180 度(順時針)
    this.minuteRotation += 180;

    let finalAngle = 0;
    if (resultCode === 4) finalAngle = 90;       // 乙方贏
    else if (resultCode === 1) finalAngle = 270;  // 甲方贏
    else if (resultCode === 3) finalAngle = 360;  // 共贏
    else finalAngle = 180;                        // 同輸

    // 確保時針也是順時針轉到終點
    let baseRotation = Math.floor(this.hourRotation / 360) * 360;
    let nextRotation = baseRotation + finalAngle;
    if (nextRotation <= this.hourRotation) {
        nextRotation += 360;
    }
    this.hourRotation = nextRotation;

    this.updatePointersRealtime();
    // ...
}
```

---

### 為什麼這樣改?
* **數學原理**:在 SVG 的 `rotate(n)` 中,如果從 $350^{\circ}$ 轉到 $10^{\circ}$,動畫引擎通常會往回轉 $340^{\circ}$(逆時針)。
* **解決方案**:我們將目標設為 $370^{\circ}$ ($10 + 360$)。雖然視覺位置一樣,但在 $1000ms$ 的插值動畫中,數值是增加的,因此會呈現**順時針**旋轉。

> **提示**:你在 `executePlace` 中已經有處理 `extraTurn` 時 `this.hourRotation += 360` 的邏輯,那是正確的順時針做法,上述修改將此邏輯統一應用到了所有轉動場景。

你把endGame寫完整。為什麼你要把setNormalMessage去掉?


gemini fast:

代码: 全选

抱歉,在先前的建議中為了精簡邏輯而省略了部分程式碼,絕對不是要去掉 `setNormalMessage`!它對於遊戲狀態的呈現非常重要。

要實現**指針永遠順時針旋轉**,核心邏輯在於:無論目標角度是多少,我們都必須確保 `targetAngle` 最終計算出的數值大於當前的 `this.hourRotation`。

以下是完整的 `endGame` 函式修復版本,包含了完整的訊息顯示與順時針邏輯:

```javascript
    endGame(resultCode) {
        this.state = 'ended';
        
        // 1. 紀錄最後一波動作
        if (this.currentTurnMoves.length > 0) {
            this.moveSequence.push(this.currentTurnMoves.join('-'));
        }

        // 2. 分針固定順時針轉半圈 (180度)
        this.minuteRotation += 180;

        // 3. 根據結果設定訊息與時針目標角度 (相對 0 度的正值)
        let finalAngle = 0; 
        if (resultCode === 4) {
            this.setNormalMessage('遊戲結束:乙方獲勝');
            finalAngle = 90; // 指向 3 點鐘
        } else if (resultCode === 1) { 
            this.setNormalMessage('遊戲結束:甲方獲勝');
            finalAngle = 270; // 指向 9 點鐘
        } else if (resultCode === 3) {
            this.setNormalMessage('遊戲結束:甲乙共贏');
            finalAngle = 360; // 指向 12 點鐘 (一圈)
        } else {
            this.setNormalMessage('遊戲結束:甲乙同輸');
            finalAngle = 180; // 指向 6 點鐘
        }

        // 4. 【核心修復】確保時針順時針轉動到目標
        // 算出目前已經轉了幾整圈 (每圈 360)
        let baseRotation = Math.floor(this.hourRotation / 360) * 360;
        let nextRotation = baseRotation + finalAngle;

        // 如果算出的目標角度小於等於現在的角度,就再加一圈,保證動畫是順時針遞增
        if (nextRotation <= this.hourRotation) {
            nextRotation += 360;
        }
        this.hourRotation = nextRotation;
        
        // 5. 執行指針動畫
        this.updatePointersRealtime();

        // 6. 顯示 UI 按鈕
        this.btnRestart.style.display = 'inline-block';
        if (this.isRandomMode) {
            this.btnRandom.style.display = 'inline-block';
        }

        // 7. 輸出棋譜
        const finalResult = this.initialPlayer === 1 ? resultCode : (resultCode === 4 ? 1 : (resultCode === 1 ? 4 : resultCode));
        const record = `${this.initialBoardStr};${this.initialPlayer};${this.moveSequence.join(',')};${finalResult}`;
        console.log("棋譜:", record);
    }
```

---

### 為什麼之前的建議看起來像「去掉」了 `setNormalMessage`?

那是我的疏忽。在技術討論中,我過於專注於解決「順時針旋轉」的數學邏輯(即 `baseRotation` 的計算),而在撰寫範例程式碼時,為了讓讀者一眼看到修改重點,漏掉了原本負責顯示文字的 `setNormalMessage` 部分。

在 Chaos Clock 遊戲中,這個方法不可或缺,因為:
1. **回饋感**:玩家需要明確知道是因為誰獲勝或觸發了什麼條件而結束。
2. **狀態鎖定**:`setNormalMessage` 同時會更新 `dataset.original`,防止 `showTempMessage` 的計時器在遊戲結束後誤將文字改回「輪到某方」。

### 其他地方的補強建議
除了 `endGame`,請務必確保 `handleCancelClick` 和 `executeMove` 等涉及轉動的地方,也採用同樣的「目標遞增」邏輯,否則指針在遊戲過程中仍可能出現「倒車」現象。

需要我幫你把 `switchTurn` 的部分也完整寫出來嗎?
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
一枝独秀
一枝独秀
帖子: 5823
注册时间: 2022年 11月 18日 17:36
联系:

Re: 將改進混亂時鐘的網頁版

帖子 ejsoon »

接上:

時針在走子時仍然不是順時針,請檢查。

gemini fast:

代码: 全选

檢查程式碼後發現,原有的 `executeMove` 和 `handleCancelClick` 函式中,時針角度是直接賦值為絕對度數(如 90 或 270),這會導致 SVG 動畫為了尋找最短路徑而出現「逆時針回轉」的情況。

要確保**所有動作(走子、落子、放棄、結束)**的指針旋轉均為順時針,必須統一使用「當前角度 + 偏移量」的邏輯。

以下是針對 `executeMove`、`handleCancelClick` 以及相關邏輯的修正方案:

---

### 1. 修正 `executeMove` (走子時的時針)
在走子函式中,時針轉動的邏輯需要從「直接設定目標」改為「計算下一個順時針角度」。

```javascript
async executeMove(pieceValue, startIndex) {
    // ... 前段邏輯不變
    this.isAnimating = true;
    this.consecutiveSkips = 0;
    this.lastMovedPiece = pieceValue;
    this.recordMoveStr(pieceValue.toString());
    
    // --- 修正時針邏輯 ---
    const nextTurn = this.turn === 1 ? 0 : 1;
    const targetAngle = nextTurn === 1 ? 90 : 270;
    let baseRotation = Math.floor(this.hourRotation / 360) * 360;
    let nextRotation = baseRotation + targetAngle;
    
    // 確保遞增(順時針)
    if (nextRotation <= this.hourRotation) {
        nextRotation += 360;
    }
    this.hourRotation = nextRotation;
    // ------------------

    const el = this.getPieceEl(pieceValue);
    el.parentNode.appendChild(el);

    await Promise.all([
        this.updatePointersRealtime(), 
        this.flashGleam(),                 
        this.animateArc(el, startIndex, targetIndex) 
    ]);
    // ... 後續邏輯不變
}
```

---

### 2. 修正 `handleCancelClick` (放棄時的時針)
放棄回合同樣需要切換指針,原本的程式碼使用了 `Math.floor` 但邏輯不夠直覺,建議改為統一的遞增判斷。

```javascript
async handleCancelClick() {
    if (this.state !== 'playing' || this.isAnimating) return;
    this.consecutiveSkips++;
    this.recordMoveStr('放棄');
    
    const nextTurn = this.turn === 1 ? 0 : 1;
    const targetAngle = nextTurn === 1 ? 90 : 270;
    
    // --- 修正時針邏輯 ---
    let baseRotation = Math.floor(this.hourRotation / 360) * 360;
    let nextRotation = baseRotation + targetAngle;
    if (nextRotation <= this.hourRotation) {
        nextRotation += 360;
    }
    this.hourRotation = nextRotation;
    // ------------------

    this.updatePointersRealtime();
    await this.flashGleam();
    
    if (this.consecutiveSkips >= 2) {
        this.endGame(2);
    } else {
        this.switchTurn(false); 
    }
}
```

---

### 3. 完整的 `endGame` 函式
這是你要求的完整版本,包含順時針邏輯與所有訊息輸出。

```javascript
endGame(resultCode) {
    this.state = 'ended';
    if (this.currentTurnMoves.length > 0) {
        this.moveSequence.push(this.currentTurnMoves.join('-'));
    }

    // 分針順時針轉半圈
    this.minuteRotation += 180;

    let finalAngle = 0;
    if (resultCode === 4) {
        this.setNormalMessage('遊戲結束:乙方獲勝');
        finalAngle = 90; 
    } else if (resultCode === 1) { 
        this.setNormalMessage('遊戲結束:甲方獲勝');
        finalAngle = 270;
    } else if (resultCode === 3) {
        this.setNormalMessage('遊戲結束:甲乙共贏');
        finalAngle = 360;
    } else {
        this.setNormalMessage('遊戲結束:甲乙同輸');
        finalAngle = 180;
    }
    
    // --- 修正時針邏輯:確保終點也是順時針到達 ---
    let baseRotation = Math.floor(this.hourRotation / 360) * 360;
    let nextRotation = baseRotation + finalAngle;
    if (nextRotation <= this.hourRotation) {
        nextRotation += 360;
    }
    this.hourRotation = nextRotation;
    // ---------------------------------------
    
    this.updatePointersRealtime();
    this.btnRestart.style.display = 'inline-block';
    if (this.isRandomMode) {
        this.btnRandom.style.display = 'inline-block';
    }

    const finalResult = this.initialPlayer === 1 ? resultCode : (resultCode === 4 ? 1 : (resultCode === 1 ? 4 : resultCode));
    const record = `${this.initialBoardStr};${this.initialPlayer};${this.moveSequence.join(',')};${finalResult}`;
    console.log("棋譜:", record);
}
```

---

### 為什麼之前的寫法會出錯?
在 SVG 或 CSS 動畫中,`rotate(angle)` 的數值如果變小(例如從 $270 \rightarrow 90$),瀏覽器會認定你想**逆時針**轉過 $180^\circ$。

**目前的修正方案:**
* 我們不使用絕對座標,而是永遠計算 **「大於當前角度的下一個目標點」**。
* 例如當前在 $270^\circ$ (甲方),目標是 $90^\circ$ (乙方),程式會自動改為轉到 $450^\circ$ ($90 + 360$)。
* 這樣 `updatePointersRealtime` 中的數值插值永遠是遞增的,視覺上就是完美的順時針。
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
一枝独秀
一枝独秀
帖子: 5823
注册时间: 2022年 11月 18日 17:36
联系:

Re: 將改進混亂時鐘的網頁版

帖子 ejsoon »

當甲方落子吃子時,時針並沒有順時針轉動,請檢查。


gemini fast:

代码: 全选

檢查程式碼後發現,問題出在 `executePlace` 函式中。雖然你之前可能已經針對「落子(Place)」的部分邏輯做了調整,但在**「落子吃子(Extra Turn)」**與**「落子無吃子(Switch Turn)」**的判斷分支中,時針角度的計算邏輯並不一致,且部分路徑仍在使用絕對角度賦值。

要徹底解決所有情況(包含落子吃子)下的順時針旋轉,必須在 `executePlace` 中也導入「當前角度 + 偏移量」的邏輯。

以下是修正後的 `executePlace` 完整代碼:

```javascript
    async executePlace(pieceValue) {
        const targetIndex = pieceValue - 1; 
        const targetOccupant = this.board[targetIndex];

        this.isAnimating = true;
        this.consecutiveSkips = 0;
        this.lastMovedPiece = pieceValue;
        this.recordMoveStr(`落${pieceValue}`);

        let extraTurn = false;
        if (targetOccupant !== null) {
            const occupantOwner = targetOccupant % 2 === 0 ? 1 : 0;
            if (occupantOwner !== this.turn) extraTurn = true; 
        }

        // --- 修正時針邏輯:確保無論是否額外回合,皆為順時針 ---
        let baseRotation = Math.floor(this.hourRotation / 360) * 360;
        
        if (extraTurn) {
            // 額外回合:時針必須順時針轉一整圈 (360度) 回到原位
            this.hourRotation += 360; 
        } else {
            // 切換回合:計算下一個順時針目標 (甲方 270, 乙方 90)
            const nextTurn = this.turn === 1 ? 0 : 1;
            const targetAngle = nextTurn === 1 ? 90 : 270;
            let nextRotation = baseRotation + targetAngle;
            
            // 如果目標角度小於等於現在,代表必須轉到「下一圈」的對應角度
            if (nextRotation <= this.hourRotation) {
                nextRotation += 360;
            }
            this.hourRotation = nextRotation;
        }
        // ------------------------------------------------

        const el = this.getPieceEl(pieceValue);
        el.parentNode.appendChild(el);

        const coords = this.getCoordsForPosition(targetIndex);
        
        // 執行指針與棋子動畫
        await Promise.all([
            this.updatePointersRealtime(), // 此處會根據新的 this.hourRotation 進行順時針動畫
            this.flashGleam(),
            this.animateLinear(el, coords.x, coords.y)
        ]);

        this.hands[this.turn] = this.hands[this.turn].filter(v => v !== pieceValue);

        if (targetOccupant !== null) {
            await this.capturePiece(targetOccupant);
        }

        this.board[targetIndex] = pieceValue;
        this.isAnimating = false;

        if (extraTurn) {
            this.setNormalMessage(this.statusOutput.dataset.original + ' (額外回合)');
            this.checkWinConditions();
        } else {
            this.checkWinOrSwitch();
        }
    }
```

### 修改重點說明:

1.  **統一基礎圈數**:透過 `Math.floor(this.hourRotation / 360) * 360` 取得當前指針已經轉過的完整圈數基底。
2.  **額外回合處理**:當落子吃子觸發 `extraTurn` 時,我們直接讓 `this.hourRotation += 360`。這會強迫動畫引擎執行一個完整的順時針圓周運動,而不是停在原地不動。
3.  **切換回合處理**:針對原本 `executePlace` 中直接賦值 `90` 或 `270` 的地方,改用 `nextRotation` 邏輯判斷,確保目標角度永遠大於當前角度。
4.  **順序一致性**:確保在調用 `this.updatePointersRealtime()` 之前,`this.hourRotation` 已經被更新為正確的遞增數值。

這樣修改後,甲方(單數)落子吃子時,時針會順時針旋轉一圈回到 9 點鐘位置($270^\circ$ 的倍數位置),視覺上便符合順時針規則。
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
一枝独秀
一枝独秀
帖子: 5823
注册时间: 2022年 11月 18日 17:36
联系:

Re: 將改進混亂時鐘的網頁版

帖子 ejsoon »

gemini fast先用著吧,無限改到您滿意為止。
附件
chaosclockgame202608201914.js
(24.3 KiB) 已下载 65 次
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
一枝独秀
一枝独秀
帖子: 5823
注册时间: 2022年 11月 18日 17:36
联系:

Re: 將改進混亂時鐘的網頁版

帖子 ejsoon »

改進:
一,當棋子12走一圈把自己吃掉後,它應不再存在於盤面中。

二,在下方增加一個狀態欄,顯示當前盤面陣列,甲方和乙方手上的棋子陣列。

三,當點擊「重新開始」時,如果是ckvalue有,值的情況,不應出現「隨機局面」按鈕,且棋子應擺放回初始局面的位置。

只需給出所要修改的地方。


gemini fast:

代码: 全选

針對您的需求,以下是 `chaosclockgame202608201914.js` 的修改建議:

### 一、 修正棋子 12 走一圈吃掉自己的邏輯
在 `executeMove` 方法中,目前的邏輯是先設定 `this.board[startIndex] = null`,但如果走完一圈回到原位,`targetOccupant` 會抓到自己,導致棋子被移動到手牌後又被 `this.board[targetIndex] = pieceValue` 設回盤面。

**修改位置:`executeMove(pieceValue, startIndex)`**
```javascript
// ... 前段代碼不變
        await Promise.all([
            this.updatePointersRealtime(), 
            this.flashGleam(),                 
            this.animateArc(el, startIndex, targetIndex) 
        ]);

        this.board[startIndex] = null;

        // --- 修改處:判斷目標位置是否有棋子且不是自己 ---
        if (targetOccupant !== null && targetOccupant !== pieceValue) {
            await this.capturePiece(targetOccupant);
            this.board[targetIndex] = pieceValue; // 只有吃掉別人才放回盤面
        } else if (targetOccupant === pieceValue) {
            // 如果是 12 號走一圈吃掉自己
            await this.capturePiece(pieceValue);
            this.board[targetIndex] = null; // 盤面該處設為空
        } else {
            this.board[targetIndex] = pieceValue; // 移動到空位
        }

        this.isAnimating = false;
        this.updateStateDisplay(); // 更新狀態欄
        this.checkWinOrSwitch();
```

---

### 二、 增加狀態欄顯示
需要修改 `createUI` 增加顯示容器,並新增一個 `updateStateDisplay` 方法。

**修改位置 1:`createUI()`**
```javascript
    createUI() {
        // ... 原有代碼
        this.statusOutput = document.createElement('div');
        // ...

        // --- 修改處:新增數據顯示欄 ---
        this.dataDisplay = document.createElement('div');
        this.dataDisplay.style.fontSize = '12px';
        this.dataDisplay.style.marginTop = '10px';
        this.dataDisplay.style.color = '#666';
        this.dataDisplay.style.whiteSpace = 'pre-wrap';
        
        // ... 之前的按鈕 append 代碼
        this.uiContainer.appendChild(this.btnRestart);
        this.uiContainer.appendChild(this.dataDisplay); // 加入容器
        
        this.objectEl.parentNode.insertBefore(this.uiContainer, this.objectEl.nextSibling);
        this.updateStateDisplay();
    }
```

**修改位置 2:新增方法 `updateStateDisplay()`**
```javascript
    updateStateDisplay() {
        if (!this.dataDisplay) return;
        this.dataDisplay.textContent = 
            `當前盤面: [${this.board.map(v => v === null ? '空' : v).join(', ')}]\n` +
            `甲方手牌(單數): [${this.hands[0].sort((a,b)=>a-b).join(', ')}] | ` +
            `乙方手牌(雙數): [${this.hands[1].sort((a,b)=>a-b).join(', ')}]`;
    }
```
*註:請在所有涉及 `this.board` 或 `this.hands` 變動的地方(如 `executeMove`, `executePlace`, `setBoard`, `generateRandomBoard`)末尾調用 `this.updateStateDisplay()`。*

---

### 三、 修正「重新開始」邏輯
確保非隨機模式下隱藏按鈕,並重置棋子位置。

**修改位置:`restartGame()`**
```javascript
    restartGame() {
        this.btnRestart.style.display = 'none';
        this.statusOutput.textContent = '等待開始';
        this.minuteRotation += 180; 
        this.hourRotation = -90;
        this.updatePointersRealtime();
        
        this.state = 'waiting';
        this.moveSequence = [];
        this.currentTurnMoves = [];
        this.consecutiveSkips = 0;
        this.lastMovedPiece = null;
        
        // --- 修改處:根據 ckvalue 決定按鈕狀態與重置棋盤 ---
        const ckvalue = this.objectEl.getAttribute('ckvalue');
        if (ckvalue && ckvalue.trim() !== "") {
            this.btnRandom.style.display = 'none'; // 強制隱藏隨機按鈕
            this.isRandomMode = false;
            const vals = ckvalue.split(',').map(Number);
            this.setBoard(vals); // 重新擺放初始位置
        } else {
            this.btnRandom.style.display = 'inline-block';
            this.isRandomMode = true;
            // 隨機模式則保留最後一次的盤面或重新生成,視需求而定
        }
        
        this.btnStart.style.display = 'inline-block';
        this.updateStateDisplay();
    }
```

### 提示
為了讓 `updateStateDisplay` 即時反應,建議在 `executePlace` 和 `capturePiece` 的動畫結束後也補上 `this.updateStateDisplay()`。
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
一枝独秀
一枝独秀
帖子: 5823
注册时间: 2022年 11月 18日 17:36
联系:

Re: 將改進混亂時鐘的網頁版

帖子 ejsoon »

gemini fast改到您滿意!
附件
chaosclockgame202608201950.js
(26.09 KiB) 已下载 64 次
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
一枝独秀
一枝独秀
帖子: 5823
注册时间: 2022年 11月 18日 17:36
联系:

Re: 將改進混亂時鐘的網頁版

帖子 ejsoon »

改進:
一,點擊「重新開始」之後,應能立即開始遊戲,而不需要再點擊一次「開始遊戲」。

二,對結局的判定有誤,改為:當甲方行動結束時,甲方的所有單數棋子都置於正位,如果甲方的這步行動是落子吃子,則立即判為「甲贏乙輸」,如果不是落子吃子,則應給乙方最後一次行動機會。乙方的行動結束時,如果乙方的所有雙數棋子也處於正位,則為「甲乙共贏」,否則就是「甲贏乙輸」。

只需給出所要修改的地方。

gemnni fast:

代码: 全选

根據您的需求,針對 `restartGame` 方法的邏輯優化以及「甲方達成勝利條件」時的分支判定,請修改以下程式碼片段:

---

### 一、 修改 `restartGame`
為了讓「重新開始」後能直接進入遊戲,需在方法末尾移除 `btnStart` 的顯示,並直接調用 `startGame()`。

```javascript
restartGame() {
    this.btnRestart.style.display = 'none';
    this.statusOutput.textContent = '等待開始';
    this.minuteRotation += 180; // 朝上
    this.hourRotation = -90;
    this.updatePointersRealtime();
    
    this.state = 'waiting';
    this.moveSequence = [];
    this.currentTurnMoves = [];
    this.consecutiveSkips = 0;
    this.lastMovedPiece = null;
    
    const ckvalue = this.objectEl.getAttribute('ckvalue');
    if (ckvalue && ckvalue.trim() !== "") {
        this.btnRandom.style.display = 'none';
        this.isRandomMode = false;
        const vals = ckvalue.split(',').map(Number);
        this.setBoard(vals); 
    } else {
        this.btnRandom.style.display = 'inline-block';
        this.isRandomMode = true;
    }
    
    // 改進:隱藏開始按鈕並直接執行 startGame
    this.btnStart.style.display = 'none';
    this.startGame(); 
}
```

---

### 二、 修改勝利判定邏輯
此修改包含 `checkWinConditions` 的邏輯重構,以及在 `executePlace` 中標記該步是否為「落子吃子」。

#### 1. 在 `executePlace` 中傳遞吃子狀態
修改 `executePlace` 的結尾部分,將是否吃子的狀態傳給判定函數:
```javascript
// ... executePlace 內部結尾
if (extraTurn) {
    this.setNormalMessage(this.statusOutput.dataset.original + ' (額外回合)');
    this.checkWinConditions(true); // 傳入 true 代表是落子吃子
} else {
    // 檢查是否落子吃子 (targetOccupant 不為空則代表吃子)
    const isPlaceCapture = targetOccupant !== null;
    if (!this.checkWinConditions(isPlaceCapture)) {
        this.switchTurn(false);
    }
}
```

#### 2. 重構 `checkWinConditions`
實現「甲方達成條件後,根據是否落子吃子決定立即結束或給予乙方最後機會」:

```javascript
checkWinConditions(isPlaceCapture = false) {
    const evensCorrect = [2,4,6,8,10,12].every(v => this.board[v-1] === v);
    const oddsCorrect = [1,3,5,7,9,11].every(v => this.board[v-1] === v);

    if (this.consecutiveSkips >= 2) {
        this.endGame(2); // 同輸
        return true;
    }

    // 乙方回合達成
    if (this.turn === 1 && evensCorrect) {
        if (oddsCorrect) {
            this.endGame(3); // 甲乙共贏
        } else {
            this.endGame(4); // 乙方獲勝
        }
        return true;
    }

    // 甲方回合達成
    if (this.turn === 0 && oddsCorrect) {
        // 如果甲方是靠「落子吃子」完成的,立即判甲方贏
        if (isPlaceCapture) {
            this.endGame(1); // 甲贏乙輸
            return true;
        } else {
            // 否則,給乙方最後一次行動機會
            this.setNormalMessage("甲方達成!乙方最後一回合");
            return false; // 返回 false 讓 switchTurn 執行,輪到乙方
        }
    }
    return false;
}
```

> **注意:** 在原本的 `executeMove`(走子)結尾調用 `this.checkWinOrSwitch()` 時,因為走子不屬於「落子吃子」,`isPlaceCapture` 會預設為 `false`,符合「甲方完成後給乙方最後一回合」的邏輯。


在updatePointersRealtime()中,要增加顯示從開始到當前的行動序列。

代码: 全选

要在 `updatePointersRealtime()` 中增加顯示行動序列,我們需要修改該方法以更新 UI 上的 `dataDisplay` 內容。

根據您提供的原始碼,行動序列儲存在 `this.moveSequence` 中,而當前回合的連續動作則在 `this.currentTurnMoves` 中。

以下是修改建議:

### 修改 `updatePointersRealtime` 方法

將該方法更新為以下邏輯,在動畫更新的同時,將目前的棋譜(行動序列)即時反映在畫面上:

```javascript
updatePointersRealtime(duration = 1000) {
    const hourHand = this.doc.querySelector('#path900');
    const minHand = this.doc.querySelector('#path1029');
    const startHour = this.currentHourRotation !== undefined ? this.currentHourRotation : -90;
    const startMin = this.currentMinRotation !== undefined ? this.currentMinRotation : 0;

    // --- 新增:整合目前的行動序列字串 ---
    // 結合已完成的回合序列與當前正在進行的動作
    const fullSequence = [...this.moveSequence];
    if (this.currentTurnMoves.length > 0) {
        fullSequence.push(this.currentTurnMoves.join('-'));
    }
    const sequenceStr = fullSequence.join(', ');

    // 更新數據顯示區域,增加「行動序列」一欄
    if (this.dataDisplay) {
        this.dataDisplay.textContent = 
            `當前盤面: [${this.board.map(v => v === null ? '空' : v).join(', ')}]\n` +
            `甲方手牌(單數): [${this.hands[0].sort((a,b)=>a-b).join(', ')}] | ` +
            `乙方手牌(雙數): [${this.hands[1].sort((a,b)=>a-b).join(', ')}]\n` +
            `行動序列: ${sequenceStr}`; // 顯示從開始到現在的所有動作
    }
    // --------------------------------

    return this.animateAttribute(duration, (p) => {
        const h = startHour + (this.hourRotation - startHour) * p;
        const m = startMin + (this.minuteRotation - startMin) * p;
        
        if (hourHand) this.updateTransform(hourHand, 'rotate', h);
        if (minHand) this.updateTransform(minHand, 'rotate', m);
        
        if (p === 1) {
            this.currentHourRotation = this.hourRotation;
            this.currentMinRotation = this.minuteRotation;
        }
    });
}
```

### 修改說明:
1.  **動態讀取序列**:從 `this.moveSequence`(已結束的回合)和 `this.currentTurnMoves`(當前回合已執行的動作)提取資料。
2.  **即時更新 UI**:原本的 `updateStateDisplay()` 只顯示盤面與手牌,現在直接在指針轉動的 `updatePointersRealtime` 邏輯中加入 `dataDisplay.textContent` 的更新,確保每次行動觸發動畫時,序列顯示會同步更新。
3.  **格式化輸出**:使用 `join(', ')` 將各回合分開,並用 `join('-')` 處理同一個回合內的連續動作(例如吃子獲得的額外回合)。

您是否需要將行動序列的顯示格式改為更易讀的條列式?
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
一枝独秀
一枝独秀
帖子: 5823
注册时间: 2022年 11月 18日 17:36
联系:

Re: 將改進混亂時鐘的網頁版

帖子 ejsoon »

chaosclockgame202608202011

gemini fast改到您滿意!

行動序列多餘的顯示最後一步。在放棄時沒有時實顯示。放棄行動在序列中應該填0。
附件
chaosclockgame202608202011.js
(26.61 KiB) 已下载 62 次
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
一枝独秀
一枝独秀
帖子: 5823
注册时间: 2022年 11月 18日 17:36
联系:

Re: 將改進混亂時鐘的網頁版

帖子 ejsoon »

改進:
1.勝負判定有誤。當甲方把所有單數棋子都置於正位時,只允許乙方再行動一次,如果乙方也把所有雙數棋子都置於正位,則是「甲乙共贏」,否則就是「甲贏乙輸」。

2.當遊戲結束時,「行動序列」會多餘的加上最後一步,看上去就像最後一步走了兩步。

3.當放棄行動時,行動序列並沒有加上。

4.在開始遊戲前,下面的實時狀態欄不必顯示。

5.當遊戲開始後,加上「最前、往前、往後、最後」的按鈕,實現其功能,用img[src=svg-base64]來顯示。當到最前時,「最前」和「往前」都會變灰。當到「最後」時,「最後」和「往後」都會變灰。

只需給出所要修改的地方。
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
一枝独秀
一枝独秀
帖子: 5823
注册时间: 2022年 11月 18日 17:36
联系:

Re: 將改進混亂時鐘的網頁版

帖子 ejsoon »

gemini pro:

代码: 全选

以下是針對您提出的 5 點需求,所需要修改及新增的程式碼片段。請將對應的方法替換或加入到您的 `ChaosClock` 類別中:

### 1. `constructor(objectEl)` 
在最後加入歷史紀錄與最後一回合的變數。
```javascript
    constructor(objectEl) {
        // ... (保留原有程式碼) ...
        this.hourRotation = -90;
        this.minuteRotation = 0;
        this.isAnimating = false;

        // 加入以下新增的狀態變數
        this.history = [];
        this.historyIndex = -1;
        this.isYiFinalTurn = false; // 改進1:紀錄乙方最後一回合

        this.init();
    }
```

### 2. `createUI()`
修改資料顯示欄為初始隱藏,並加入歷史紀錄的按鈕容器。
```javascript
    createUI() {
        // ... (保留前面創造 statusOutput, btnStart, btnRandom, btnRestart 的程式碼) ...

        this.dataDisplay = document.createElement('div');
        this.dataDisplay.style.fontSize = '12px';
        this.dataDisplay.style.marginTop = '10px';
        this.dataDisplay.style.color = '#666';
        this.dataDisplay.style.whiteSpace = 'pre-wrap';
        this.dataDisplay.style.display = 'none'; // 改進4:開始前隱藏

        this.uiContainer.appendChild(this.statusOutput);
        this.uiContainer.appendChild(this.btnStart);
        this.uiContainer.appendChild(this.btnRandom);
        this.uiContainer.appendChild(this.btnRestart);
        this.uiContainer.appendChild(this.dataDisplay); 
        
        // 改進5:加入歷史播放控制列
        this.historyContainer = document.createElement('div');
        this.historyContainer.style.marginTop = '10px';
        this.historyContainer.style.display = 'none';
        
        const svgFirst = "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0iY3VycmVudENvbG9yIj48cGF0aCBkPSJNMTEgMThWNmwtOC41IDZMMTEgMTh6bTEuNS02bDguNS02djEyTDEyLjUgMTJ6Ii8+PC9zdmc+";
        const svgPrev = "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0iY3VycmVudENvbG9yIj48cGF0aCBkPSJNMTUuNCAxNi42TDExIDEyTDE1LjQgNy40TDE0IDZMNCAxMkwxNCAxOGwxLjQtMS40eiIvPjwvc3ZnPg==";
        const svgNext = "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0iY3VycmVudENvbG9yIj48cGF0aCBkPSJNMTAgNkw4LjYgNy40TDEzIDEyTDguNiAxNi42TDEwIDE4TDIwIDEyTDEwIDZ6Ii8+PC9zdmc+";
        const svgLast = "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0iY3VycmVudENvbG9yIj48cGF0aCBkPSJNMTMgNnYxMmwtOC41LTZMMTMgNnptLTEuNSA2TDMgMThWNmw4LjUgNnoiLz48L3N2Zz4=";

        const createBtn = (svgData, onClick) => {
            const btn = document.createElement('img');
            btn.src = svgData;
            btn.style.width = '24px';
            btn.style.height = '24px';
            btn.style.cursor = 'pointer';
            btn.style.margin = '0 5px';
            btn.onclick = onClick;
            return btn;
        };

        this.btnFirst = createBtn(svgFirst, () => this.loadState(0));
        this.btnPrev = createBtn(svgPrev, () => this.loadState(this.historyIndex - 1));
        this.btnNext = createBtn(svgNext, () => this.loadState(this.historyIndex + 1));
        this.btnLast = createBtn(svgLast, () => this.loadState(this.history.length - 1));

        this.historyContainer.appendChild(this.btnFirst);
        this.historyContainer.appendChild(this.btnPrev);
        this.historyContainer.appendChild(this.btnNext);
        this.historyContainer.appendChild(this.btnLast);
        this.uiContainer.appendChild(this.historyContainer);

        this.objectEl.parentNode.insertBefore(this.uiContainer, this.objectEl.nextSibling);
        this.updateStateDisplay();
    }
```

### 3. `startGame()`
顯示被隱藏的狀態列與歷史列,並儲存第一步歷史。
```javascript
    async startGame() {
        this.state = 'playing';
        this.turn = 1; // 乙方先手
        this.initialPlayer = 1;
        this.btnStart.style.display = 'none';
        this.btnRandom.style.display = 'none';
        this.setNormalMessage('遊戲開始,輪到乙方');
        
        this.hourRotation = 90; 
        this.isAnimating = true;
        await this.updatePointersRealtime();
        this.isAnimating = false;

        // 改進4與5:顯示狀態與控制項,並儲存初始狀態
        this.dataDisplay.style.display = 'block'; 
        this.historyContainer.style.display = 'block';
        this.history = []; 
        this.saveState();
    }
```

### 4. `restartGame()`
重置新加入的狀態標記。
```javascript
    restartGame() {
        // ... (保留前面程式碼) ...
        this.state = 'waiting';
        this.moveSequence = [];
        this.currentTurnMoves = [];
        this.consecutiveSkips = 0;
        this.lastMovedPiece = null;
        this.isYiFinalTurn = false; // 改進1:重置標記
        
        // ... (保留後面程式碼) ...
```

### 5. `handlePieceClick(pieceValue)` 與 `handleCancelClick()`
防止玩家在瀏覽歷史中行動,並在放棄後更新 UI。
```javascript
    async handlePieceClick(pieceValue) {
        if (this.state !== 'playing' || this.isAnimating) return;
        // 改進5:瀏覽歷史時禁止行動
        if (this.historyIndex >= 0 && this.historyIndex < this.history.length - 1) {
            this.showTempMessage('瀏覽歷史狀態中無法行動');
            return;
        }
        if (pieceValue === this.lastMovedPiece) return;
        // ... (保留後續原本邏輯) ...
```
```javascript
    async handleCancelClick() {
        if (this.state !== 'playing' || this.isAnimating) return;
        if (this.historyIndex >= 0 && this.historyIndex < this.history.length - 1) {
            this.showTempMessage('瀏覽歷史狀態中無法行動');
            return;
        }
        this.consecutiveSkips++;
        this.recordMoveStr('放棄');
        
        // ... (保留中間原有的時針與反光動畫邏輯) ...
        
        if (this.consecutiveSkips >= 2) {
            this.endGame(2);
        } else {
            this.switchTurn(false);
        }
        
        // 改進3:放棄時也更新狀態欄與儲存歷史
        this.updateStateDisplay(); 
        this.saveState();
    }
```

### 6. `executeMove()` 與 `executePlace()`
在行動執行完畢後加上 `this.saveState();` 來記錄狀態。
```javascript
    async executeMove(pieceValue, startIndex) {
        // ... (保留原本邏輯) ...
        this.isAnimating = false;
        this.updateStateDisplay(); 
        this.checkWinOrSwitch();
        this.updateStateDisplay();
        this.saveState(); // 改進5:紀錄歷史
    }

    async executePlace(pieceValue) {
        // ... (保留原本邏輯) ...
        if (extraTurn) {
            this.setNormalMessage(this.statusOutput.dataset.original + ' (額外回合)');
            this.checkWinConditions(true);
        } else {
            const isPlaceCapture = targetOccupant !== null;
            if (!this.checkWinConditions(isPlaceCapture)) {
                this.switchTurn(false);
            }
        }
        this.updateStateDisplay();
        this.saveState(); // 改進5:紀錄歷史
    }
```

### 7. `checkWinConditions(isPlaceCapture)`
修正乙方最後一回合的判定。
```javascript
    checkWinConditions(isPlaceCapture = false) {
        const evensCorrect = [2,4,6,8,10,12].every(v => this.board[v-1] === v);
        const oddsCorrect = [1,3,5,7,9,11].every(v => this.board[v-1] === v);

        if (this.consecutiveSkips >= 2) {
            this.endGame(2); // 同輸
            return true;
        }

        // 改進1:判斷是否正在結算「乙方最後一回合」
        if (this.isYiFinalTurn) {
            if (evensCorrect) {
                this.endGame(3); // 甲乙共贏
            } else {
                this.endGame(1); // 甲贏乙輸
            }
            return true;
        }

        // 乙方回合達成
        if (this.turn === 1 && evensCorrect) {
            if (oddsCorrect) {
                this.endGame(3); // 甲乙共贏
            } else {
                this.endGame(4); // 乙方獲勝
            }
            return true;
        }

        // 甲方回合達成
        if (this.turn === 0 && oddsCorrect) {
            if (isPlaceCapture) {
                this.endGame(1); // 落子吃子立刻甲贏乙輸
                return true;
            } else {
                this.isYiFinalTurn = true; // 給乙方最後一次行動機會
                this.setNormalMessage("甲方達成!乙方最後一回合");
                return false; 
            }
        }
        return false;
    }
```

### 8. `endGame(resultCode)`
遊戲結束清空目前的連擊步數,以免重複加入序列。
```javascript
    endGame(resultCode) {
        this.state = 'ended';
        
        // 1. 紀錄最後一波動作
        if (this.currentTurnMoves.length > 0) {
            this.moveSequence.push(this.currentTurnMoves.join('-'));
            this.currentTurnMoves = []; // 改進2:清空避免顯示時多出一步
        }
        
        // ... (保留後續所有邏輯) ...
```

### 9. 【新增】歷史紀錄相關方法
請將以下三個方法加入在 `ChaosClock` 類別的最底部(在 `endGame` 或其他方法之後):
```javascript
    saveState() {
        const pieceTransforms = {};
        for(let i=1; i<=12; i++) {
            const el = this.getPieceEl(i);
            pieceTransforms[i] = el ? el.getAttribute('transform') : '';
        }
        const state = {
            board: [...this.board],
            hands: { 0: [...this.hands[0]], 1: [...this.hands[1]] },
            turn: this.turn,
            moveSequence: [...this.moveSequence],
            currentTurnMoves: [...this.currentTurnMoves],
            statusMsg: this.statusOutput.textContent,
            transforms: pieceTransforms,
            hourRotation: this.hourRotation,
            minuteRotation: this.minuteRotation,
            isYiFinalTurn: this.isYiFinalTurn
        };
        this.history.push(state);
        this.historyIndex = this.history.length - 1;
        this.updateHistoryButtons();
    }

    loadState(index) {
        if (index < 0 || index >= this.history.length) return;
        this.historyIndex = index;
        const state = this.history[index];
        
        this.board = [...state.board];
        this.hands = { 0: [...state.hands[0]], 1: [...state.hands[1]] };
        this.turn = state.turn;
        this.moveSequence = [...state.moveSequence];
        this.currentTurnMoves = [...state.currentTurnMoves];
        this.isYiFinalTurn = state.isYiFinalTurn;
        
        for(let i=1; i<=12; i++) {
            const el = this.getPieceEl(i);
            if (el && state.transforms[i] !== undefined) {
                el.setAttribute('transform', state.transforms[i]);
                this.updateOpacity(el, 1);
            }
        }
        
        this.hourRotation = state.hourRotation;
        this.minuteRotation = state.minuteRotation;
        const hourHand = this.doc.querySelector('#path900');
        const minHand = this.doc.querySelector('#path1029');
        if (hourHand) this.updateTransform(hourHand, 'rotate', this.hourRotation);
        if (minHand) this.updateTransform(minHand, 'rotate', this.minuteRotation);
        
        this.setNormalMessage(state.statusMsg);
        this.updateStateDisplay();
        this.updateHistoryButtons();
    }

    updateHistoryButtons() {
        const isFirst = this.historyIndex <= 0;
        const isLast = this.historyIndex >= this.history.length - 1;
        
        this.btnFirst.style.opacity = isFirst ? '0.3' : '1';
        this.btnFirst.style.pointerEvents = isFirst ? 'none' : 'auto';
        this.btnPrev.style.opacity = isFirst ? '0.3' : '1';
        this.btnPrev.style.pointerEvents = isFirst ? 'none' : 'auto';
        
        this.btnNext.style.opacity = isLast ? '0.3' : '1';
        this.btnNext.style.pointerEvents = isLast ? 'none' : 'auto';
        this.btnLast.style.opacity = isLast ? '0.3' : '1';
        this.btnLast.style.pointerEvents = isLast ? 'none' : 'auto';
    }
```
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
一枝独秀
一枝独秀
帖子: 5823
注册时间: 2022年 11月 18日 17:36
联系:

Re: 將改進混亂時鐘的網頁版

帖子 ejsoon »

改進:
1.如果初始盤面包含0,說明那裡沒有棋子,並且已經有棋子被拿在手上。完善這個邏輯。同時,如果初始盤面即ckvalue的值的末尾是「;0」,則表示接下來輪到甲方行動,如果沒有,或者有「;1」,則是輪到乙方行動。

例如ckvalue="1,2,0,3,0,6,9,0,5,0,8,7;0",則推出甲方手上有11,乙方手上有4、10、12,當前局面輪到甲方。

如果後面還有一個「;n」或「;a,b,c,d,e」,則最後一個數字表示上一步剛走完的棋子,這一步不能走。

比如ckvalue="1,2,0,3,0,6,9,0,5,0,8,7;0;9"表示上一步乙方剛走完9。再如ckvalue="1,2,0,3,0,6,9,0,5,0,8,7;0;5,9,7"表示上一步乙方剛走完7。

2.如果在歷史回退之後走棋,如果走的跟當前歷史不同,則建立分支。在「往前、往後」按鈕中間加上「分支按鈕」,其顯示為n/m,n和m都為大於零的整數,其中m表示當前有多少個分支,n表示當前處於第幾個分支。這個分支按鈕呈圓形,當n/m=1/1即沒有分支時其為灰色且不可點擊,當存在分支時,點擊它將出現下拉菜單,可選擇切換分支,再次點擊則收回菜單。

只需給出所要修改的地方。


gemini pro:

代码: 全选

以下是針對您提出的兩點改進需求,所需要修改及新增的程式碼區塊。請將對應的邏輯替換或加入到您的 `ChaosClock` 類別中:

### 1. 處理初始盤面字串 (ckvalue) 的解析邏輯
請新增(或替換現有的解析方法)以下 `parseInitialData` 方法。這會完美處理 `0` 代表空位、正確分配手牌、判定回合,以及鎖定上一步剛走完的棋子。
在您的遊戲初始化階段(如 `startGame` 或解析 URL 參數時),將盤面字串傳入此方法即可:

```javascript
    parseInitialData(ckvalue) {
        const parts = ckvalue.split(';');
        const boardStr = parts[0];
        const turnStr = parts[1];
        const lastStr = parts[2];

        // 1. 解析盤面與手牌 (0代表該位置無棋子,棋子在玩家手上)
        const bVals = boardStr.split(',').map(Number);
        this.board = bVals.map(v => v === 0 ? null : v);
        
        this.hands = { 0: [], 1: [] };
        const onBoard = new Set(this.board.filter(v => v !== null));
        for (let i = 1; i <= 12; i++) {
            if (!onBoard.has(i)) {
                if (i % 2 !== 0) this.hands[0].push(i); // 單數為甲方手牌
                else this.hands[1].push(i);             // 雙數為乙方手牌
            }
        }

        // 2. 判斷回合 (沒有或為 ;1 則是乙方,;0 則是甲方)
        if (turnStr === '0') {
            this.turn = 0;
            this.initialPlayer = 0;
        } else {
            this.turn = 1;
            this.initialPlayer = 1;
        }

        // 3. 解析上一步剛走完的棋子 (最後一個數字)
        if (lastStr) {
            const lastMoves = lastStr.split(',').map(Number);
            this.lastMovedPiece = lastMoves[lastMoves.length - 1];
        } else {
            this.lastMovedPiece = null; // 無限制
        }
    }
```

---

### 2. 建立歷史分支系統相關修改

#### A. 修改 `constructor` 的變數宣告
將原本單一維度的 `history` 陣列替換為樹狀節點與活動路徑。
```javascript
    constructor(objectEl) {
        // ... (保留前面原有程式碼) ...

        // 將原有的 this.history = []; 替換為以下分支系統變數
        this.historyRoot = null; 
        this.activePath = [];    // 當前的活躍歷史路徑
        this.historyIndex = -1;
        
        this.isYiFinalTurn = false;
        this.init();
    }
```

#### B. 解除歷史回退時無法行動的限制
在 `handlePieceClick` 與 `handleCancelClick` 中,**刪除或註解掉**之前加入的防呆限制,允許玩家在歷史回退狀態中下棋以建立分支:
```javascript
        // 請刪除以下這段程式碼:
        // if (this.historyIndex >= 0 && this.historyIndex < this.history.length - 1) {
        //     this.showTempMessage('瀏覽歷史狀態中無法行動');
        //     return;
        // }
```

#### C. 在 `createUI()` 加入分支按鈕
在生成歷史控制列的部分,加入圓形的分支按鈕與下拉菜單:
```javascript
    createUI() {
        // ... (保留前面產生 svg 按鈕的程式碼) ...
        
        this.btnPrev = createBtn(svgPrev, () => this.loadState(this.historyIndex - 1));
        
        // 【新增】分支按鈕 n/m
        this.btnBranch = document.createElement('div');
        this.btnBranch.style.width = '26px';
        this.btnBranch.style.height = '26px';
        this.btnBranch.style.borderRadius = '50%';
        this.btnBranch.style.display = 'flex';
        this.btnBranch.style.alignItems = 'center';
        this.btnBranch.style.justifyContent = 'center';
        this.btnBranch.style.fontSize = '10px';
        this.btnBranch.style.margin = '0 5px';
        this.btnBranch.style.userSelect = 'none';
        this.btnBranch.style.position = 'relative';
        this.btnBranch.textContent = '1/1';

        // 【新增】下拉菜單
        this.branchMenu = document.createElement('div');
        this.branchMenu.style.position = 'absolute';
        this.branchMenu.style.bottom = '100%';
        this.branchMenu.style.left = '50%';
        this.branchMenu.style.transform = 'translate(-50%, -5px)';
        this.branchMenu.style.backgroundColor = 'white';
        this.branchMenu.style.border = '1px solid #ccc';
        this.branchMenu.style.borderRadius = '4px';
        this.branchMenu.style.display = 'none';
        this.branchMenu.style.flexDirection = 'column';
        this.branchMenu.style.minWidth = '60px';
        this.branchMenu.style.zIndex = '100';
        this.branchMenu.style.color = '#333';
        this.btnBranch.appendChild(this.branchMenu);

        // 分支點擊事件 (展開/收合菜單)
        this.btnBranch.onclick = (e) => {
            const node = this.activePath[this.historyIndex];
            const parent = node ? node.parent : null;
            if (!parent || parent.children.length <= 1) return;
            
            if (this.branchMenu.style.display === 'flex') {
                this.branchMenu.style.display = 'none';
            } else {
                this.branchMenu.innerHTML = '';
                parent.children.forEach((child, idx) => {
                    const item = document.createElement('div');
                    item.textContent = `分支 ${idx + 1}`;
                    item.style.padding = '5px 10px';
                    item.style.cursor = 'pointer';
                    item.style.borderBottom = idx < parent.children.length - 1 ? '1px solid #eee' : 'none';
                    item.style.backgroundColor = child === node ? '#e0f7fa' : 'transparent'; // 標記當前分支
                    item.onclick = (ev) => {
                        ev.stopPropagation();
                        this.switchBranch(idx);
                        this.branchMenu.style.display = 'none';
                    };
                    item.onmouseenter = () => item.style.backgroundColor = '#f0f0f0';
                    item.onmouseleave = () => item.style.backgroundColor = child === node ? '#e0f7fa' : 'transparent';
                    this.branchMenu.appendChild(item);
                });
                this.branchMenu.style.display = 'flex';
            }
        };

        // 點擊外部關閉菜單
        document.addEventListener('click', (e) => {
            if (!this.btnBranch.contains(e.target)) this.branchMenu.style.display = 'none';
        });

        this.btnNext = createBtn(svgNext, () => this.loadState(this.historyIndex + 1));
        this.btnLast = createBtn(svgLast, () => this.loadState(this.activePath.length - 1));

        // 按照順序加入 Container
        this.historyContainer.appendChild(this.btnFirst);
        this.historyContainer.appendChild(this.btnPrev);
        this.historyContainer.appendChild(this.btnBranch); // 安插在往前與往後之間
        this.historyContainer.appendChild(this.btnNext);
        this.historyContainer.appendChild(this.btnLast);
        
        // ... (保留後續 appendChild) ...
    }
```

#### D. 替換歷史儲存與讀取的方法
將您之前加入的 `saveState`, `loadState`, `updateHistoryButtons` 完全替換為以下加入分支樹邏輯的版本,並新增 `captureCurrentState` 與 `switchBranch`:

```javascript
    captureCurrentState() {
        const pieceTransforms = {};
        for(let i=1; i<=12; i++) {
            const el = this.getPieceEl(i);
            pieceTransforms[i] = el ? el.getAttribute('transform') : '';
        }
        return {
            board: [...this.board],
            hands: { 0: [...this.hands[0]], 1: [...this.hands[1]] },
            turn: this.turn,
            moveSequence: [...this.moveSequence],
            currentTurnMoves: [...this.currentTurnMoves],
            statusMsg: this.statusOutput.textContent,
            transforms: pieceTransforms,
            hourRotation: this.hourRotation,
            minuteRotation: this.minuteRotation,
            isYiFinalTurn: this.isYiFinalTurn,
            lastMovedPiece: this.lastMovedPiece
        };
    }

    saveState() {
        const newState = this.captureCurrentState();

        // 第一次儲存時建立根節點
        if (this.activePath.length === 0) {
            this.historyRoot = { state: newState, children: [], parent: null };
            this.activePath = [this.historyRoot];
            this.historyIndex = 0;
            this.updateHistoryButtons();
            return;
        }

        const parentNode = this.activePath[this.historyIndex]; // 行動前的狀態節點
        
        // 檢查是否已有相同盤面的分支 (防止重複記錄)
        const stateHash = JSON.stringify(newState.board) + JSON.stringify(newState.hands);
        let existingChild = parentNode.children.find(child => 
            JSON.stringify(child.state.board) + JSON.stringify(child.state.hands) === stateHash
        );

        let nextNode;
        if (existingChild) {
            nextNode = existingChild; // 若已有同樣的下法,直接進入該分支
        } else {
            nextNode = { state: newState, children: [], parent: parentNode };
            parentNode.children.push(nextNode); // 建立新分支
        }

        // 截斷當前路徑並連接新路徑
        this.activePath = this.activePath.slice(0, this.historyIndex + 1);
        let tempNode = nextNode;
        while (tempNode) {
            this.activePath.push(tempNode);
            // 預設往後預覽第一條分支到底
            tempNode = tempNode.children.length > 0 ? tempNode.children[0] : null; 
        }
        
        this.historyIndex++; // 推進到剛剛產生的節點
        this.updateHistoryButtons();
    }

    loadState(index) {
        if (index < 0 || index >= this.activePath.length) return;
        this.historyIndex = index;
        const state = this.activePath[index].state;
        
        this.board = [...state.board];
        this.hands = { 0: [...state.hands[0]], 1: [...state.hands[1]] };
        this.turn = state.turn;
        this.moveSequence = [...state.moveSequence];
        this.currentTurnMoves = [...state.currentTurnMoves];
        this.isYiFinalTurn = state.isYiFinalTurn;
        this.lastMovedPiece = state.lastMovedPiece;
        
        for(let i=1; i<=12; i++) {
            const el = this.getPieceEl(i);
            if (el && state.transforms[i] !== undefined) {
                el.setAttribute('transform', state.transforms[i]);
                this.updateOpacity(el, 1);
            }
        }
        
        this.hourRotation = state.hourRotation;
        this.minuteRotation = state.minuteRotation;
        const hourHand = this.doc.querySelector('#path900');
        const minHand = this.doc.querySelector('#path1029');
        if (hourHand) this.updateTransform(hourHand, 'rotate', this.hourRotation);
        if (minHand) this.updateTransform(minHand, 'rotate', this.minuteRotation);
        
        this.setNormalMessage(state.statusMsg);
        this.updateStateDisplay();
        this.updateHistoryButtons();
    }

    switchBranch(siblingIndex) {
        const node = this.activePath[this.historyIndex];
        const parent = node.parent;
        if (!parent) return;
        
        const targetNode = parent.children[siblingIndex];
        if (!targetNode) return;
        
        const parentPathIndex = this.activePath.indexOf(parent);
        this.activePath = this.activePath.slice(0, parentPathIndex + 1);
        
        let tempNode = targetNode;
        while(tempNode) {
            this.activePath.push(tempNode);
            tempNode = tempNode.children.length > 0 ? tempNode.children[0] : null;
        }
        
        this.loadState(parentPathIndex + 1); // 切換後直接載入選中的分支第一步
    }

    updateHistoryButtons() {
        const isFirst = this.historyIndex <= 0;
        const isLast = this.historyIndex >= this.activePath.length - 1;
        
        this.btnFirst.style.opacity = isFirst ? '0.3' : '1';
        this.btnFirst.style.pointerEvents = isFirst ? 'none' : 'auto';
        this.btnPrev.style.opacity = isFirst ? '0.3' : '1';
        this.btnPrev.style.pointerEvents = isFirst ? 'none' : 'auto';
        
        this.btnNext.style.opacity = isLast ? '0.3' : '1';
        this.btnNext.style.pointerEvents = isLast ? 'none' : 'auto';
        this.btnLast.style.opacity = isLast ? '0.3' : '1';
        this.btnLast.style.pointerEvents = isLast ? 'none' : 'auto';

        // 分支按鈕 n/m 顯示邏輯
        const node = this.activePath[this.historyIndex];
        const parent = node ? node.parent : null;
        const m = parent ? parent.children.length : 1;
        const n = parent ? parent.children.indexOf(node) + 1 : 1;
        
        this.btnBranch.textContent = `${n}/${m}`;
        if (m <= 1) {
            this.btnBranch.style.backgroundColor = '#ccc';
            this.btnBranch.style.color = '#666';
            this.btnBranch.style.cursor = 'default';
        } else {
            this.btnBranch.style.backgroundColor = '#4CAF50';
            this.btnBranch.style.color = 'white';
            this.btnBranch.style.cursor = 'pointer';
        }
    }
```
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
一枝独秀
一枝独秀
帖子: 5823
注册时间: 2022年 11月 18日 17:36
联系:

Re: 將改進混亂時鐘的網頁版

帖子 ejsoon »

應把前面的this.btnBranch.textContent = '1/1'換成一個span,後面改動內容也改這個span,否則它內部的menu會不見。

gemini fast改到您滿意:

代码: 全选

這個觀察非常正確。當您直接修改 `this.btnBranch.textContent` 時,瀏覽器會清空該元素下的**所有子節點**(包括您先前 `appendChild` 進去的 `branchMenu`)。

為了保留菜單並能動態更新文字,我們應該在 `btnBranch` 內部建立一個專門存放文字的 `span`。以下是修改建議:

### 1. 修改 `createUI()` 中的按鈕初始化
在建立 `btnBranch` 時,將文字放入 `span`,並確保 `branchMenu` 也是它的子元素。

```javascript
    // 【修改後】分支按鈕 n/m
    this.btnBranch = document.createElement('div');
    this.btnBranch.style.width = '30px'; // 稍微加寬一點以容納文字
    this.btnBranch.style.height = '30px';
    this.btnBranch.style.borderRadius = '50%';
    this.btnBranch.style.display = 'inline-flex'; // 解決換行問題
    this.btnBranch.style.verticalAlign = 'middle';
    this.btnBranch.style.alignItems = 'center';
    this.btnBranch.style.justifyContent = 'center';
    this.btnBranch.style.fontSize = '10px';
    this.btnBranch.style.margin = '0 5px';
    this.btnBranch.style.userSelect = 'none';
    this.btnBranch.style.position = 'relative';

    // 建立專門放文字的 span
    this.branchText = document.createElement('span');
    this.branchText.textContent = '1/1';
    this.btnBranch.appendChild(this.branchText);

    // 下拉菜單 (保持原樣,它是 btnBranch 的子元素)
    this.branchMenu = document.createElement('div');
    // ... (branchMenu 的樣式代碼維持不變) ...
    this.btnBranch.appendChild(this.branchMenu);
```

---

### 2. 修改 `updateHistoryButtons()` 的更新邏輯
將原本修改 `this.btnBranch.textContent` 的地方改為修改 `this.branchText.textContent`。

```javascript
    updateHistoryButtons() {
        // ... (前面 isFirst, isLast 的按鈕灰化邏輯維持不變) ...

        const node = this.activePath[this.historyIndex];
        const parent = node ? node.parent : null;
        const m = parent ? parent.children.length : 1;
        const n = parent ? parent.children.indexOf(node) + 1 : 1;
        
        // 【修改這裡】只更新 span 的內容,不影響 menu
        this.branchText.textContent = `${n}/${m}`;
        
        if (m <= 1) {
            this.btnBranch.style.backgroundColor = '#ccc';
            this.btnBranch.style.color = '#666';
            this.btnBranch.style.cursor = 'default';
        } else {
            this.btnBranch.style.backgroundColor = '#4CAF50';
            this.btnBranch.style.color = 'white';
            this.btnBranch.style.cursor = 'pointer';
        }
    }
```

---

### 3. 額外優化:防止文字被選中
為了讓按鈕點起來更像原生 UI,建議在 `btnBranch` 的 CSS 樣式中確保滑鼠點擊不會選中文字:

```javascript
this.btnBranch.style.userSelect = 'none';
this.btnBranch.style.webkitUserSelect = 'none'; // 相容性
```

這樣一來,無論您如何切換分支或更新顯示的數字,內部的 `branchMenu` 結構都會穩固地留在 DOM 樹中,不會被 `textContent` 抹除。
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
一枝独秀
一枝独秀
帖子: 5823
注册时间: 2022年 11月 18日 17:36
联系:

Re: 將改進混亂時鐘的網頁版

帖子 ejsoon »

改進:
當分支菜單打開時,當前分支應高亮,鼠標移到上面也會高亮。每個item的右邊都會有一個img[src=svg-base64]垃圾桶按鈕,點擊則刪除該分支。當只剩一個分支(即1/1)時,菜單自動消失。

只需給出所要修改的地方。
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
一枝独秀
一枝独秀
帖子: 5823
注册时间: 2022年 11月 18日 17:36
联系:

Re: 將改進混亂時鐘的網頁版

帖子 ejsoon »

chaosclockgame202608210918

混亂時鐘有分支了。
附件
chaosclockgame202608210918.js
(40.3 KiB) 已下载 58 次
https://ejsoon.vip/
弈趣極光:享受思維樂趣
回复
  • 相似主题
    回复总数
    阅读次数
    最新帖子

在线用户

正浏览此版面之用户: Google [Bot] 和 2 访客