將改進混亂時鐘的網頁版

分享和讨论Javascript相关的话题
头像
ejsoon
一枝独秀
一枝独秀
帖子: 5214
注册时间: 2022年 11月 18日 17:36
为圈友点赞: 175 次
被圈友点赞: 204 次
联系:

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

帖子 ejsoon »

async hideAllPieces() {
這個函式是否未有用到?檢查程式中還有哪些需要修改的地方。
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
一枝独秀
一枝独秀
帖子: 5214
注册时间: 2022年 11月 18日 17:36
为圈友点赞: 175 次
被圈友点赞: 204 次
联系:

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

帖子 ejsoon »

時針和分針在轉動時,它的transform屬性中應該只改動rotate,如果有translate和scale應保留。


開始遊戲時,時針也應該從-90度通過動畫轉到90度。

代码: 全选

沒問題!為了解決這兩個需求,我們需要修改**指針動畫的更新邏輯**(確保使用我們前面寫好的 `updateTransform` 來保留其他變形),以及**修改開始遊戲的方法**來加入動畫效果。

以下是需要修改的程式碼片段,請替換您類別中對應的方法:

### 1. 修正指針更新邏輯(保留其他 Transform)
將 `updatePointersRealtime` 與靜態的 `updatePointers` 修改為使用 `updateTransform`,這樣就能確保原有的 `translate` 或 `scale` 不會被覆蓋掉。

```javascript
    // 實時改變時針與分針屬性(動畫用)
    updatePointersRealtime(duration) {
        const hourHand = this.doc.querySelector('#path900');
        const minHand = this.doc.querySelector('#path1029');
        // 如果沒有初始紀錄,時針預設為 -90,分針預設為 0
        const startHour = this.currentHourRotation !== undefined ? this.currentHourRotation : -90;
        const startMin = this.currentMinRotation !== undefined ? this.currentMinRotation : 0;

        return this.animateAttribute(duration, (p) => {
            const h = startHour + (this.hourRotation - startHour) * p;
            const m = startMin + (this.minuteRotation - startMin) * p;
            
            // 使用 updateTransform 僅修改 rotate,保留其他變形屬性
            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;
            }
        });
    }

    // 靜態改變時針與分針屬性(非動畫用)
    updatePointers() {
        const hourHand = this.doc.querySelector('#path900');
        const minHand = this.doc.querySelector('#path1029');
        
        if (hourHand) {
            hourHand.setAttribute('transform-origin', '0 0');
            this.updateTransform(hourHand, 'rotate', this.hourRotation);
        }
        if (minHand) {
            minHand.setAttribute('transform-origin', '0 0');
            this.updateTransform(minHand, 'rotate', this.minuteRotation);
        }
        
        // 同步紀錄當前角度,以供後續動畫計算起點
        this.currentHourRotation = this.hourRotation;
        this.currentMinRotation = this.minuteRotation;
    }
```

### 2. 修改開始遊戲方法(加入動畫)
將 `startGame` 修改為異步函數(`async`),並在改變目標角度後呼叫 `updatePointersRealtime` 執行 1 秒的動畫。

```javascript
    async startGame() {
        this.state = 'playing';
        this.turn = 1; // 乙方先手
        this.initialPlayer = 1;
        this.btnStart.style.display = 'none';
        this.btnRandom.style.display = 'none';
        this.setNormalMessage('遊戲開始,輪到乙方');
        
        // 時針從 -90 轉到 90
        this.hourRotation = 90; 
        
        // 確保遊戲開始時執行 1 秒的指針動畫
        this.isAnimating = true;
        await this.updatePointersRealtime(1000);
        this.isAnimating = false;
    }
```
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
一枝独秀
一枝独秀
帖子: 5214
注册时间: 2022年 11月 18日 17:36
为圈友点赞: 175 次
被圈友点赞: 204 次
联系:

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

帖子 ejsoon »

報出警告:
Unexpected value rotate(NaN) scale(1.2) translate(-6.17,-54.816) parsing transform attribute.
Unexpected value rotate(NaN) scale(1.5) translate(-1.63,-65.4) parsing transform attribute.

查找原因並修復。

只需給出所要修改的地方。
附件
chaosclockgame202608201334.js
(22.1 KiB) 已下载 4 次
chaosclockgame202608201251.js
(23.02 KiB) 已下载 4 次
上次由 ejsoon 在 2026年 4月 20日 13:41,总共编辑 1 次。
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
一枝独秀
一枝独秀
帖子: 5214
注册时间: 2022年 11月 18日 17:36
为圈友点赞: 175 次
被圈友点赞: 204 次
联系:

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

帖子 ejsoon »

改進:
一,報出警告:

Unexpected value rotate(NaN) scale(1.2) translate(-6.17,-54.816) parsing transform attribute.

Unexpected value rotate(NaN) scale(1.5) translate(-1.63,-65.4) parsing transform attribute.

查找原因並修復。

二,
async hideAllPieces() {

這個函式是否未有用到?檢查程式中還有哪些需要修改的地方。


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

gemini fast:

代码: 全选

這份程式碼中產生的 `NaN` 錯誤,主要是因為在第一次呼叫 `updatePointersRealtime` 時,`currentHourRotation` 等變數尚未初始化,導致計算插值時出現 `undefined + (number - undefined) * p` 的無效運算。

以下是針對您提出的兩點改進建議:

---

### 一、 修復 NaN 錯誤

問題出在 `updatePointersRealtime` 函式中。當 `duration` 為 `undefined`(例如在 `switchTurn` 或 `handleCancelClick` 中呼叫時)或初始角度未定義時,計算邏輯會出錯。

**修改方案:**
在 `updatePointersRealtime` 中加入預設值,並處理 `duration` 未傳入的情況。

```javascript
// 修正後的 updatePointersRealtime
updatePointersRealtime(duration) {
    const hourHand = this.doc.querySelector('#path900');
    const minHand = this.doc.querySelector('#path1029');

    // 確保有初始數值,避免出現 NaN
    const startHour = (this.currentHourRotation !== undefined) ? this.currentHourRotation : -90;
    const startMin = (this.currentMinRotation !== undefined) ? this.currentMinRotation : 0;

    // 如果沒有傳入時間,直接更新位置不執行 requestAnimationFrame
    if (!duration) {
        if (hourHand) this.updateTransform(hourHand, 'rotate', this.hourRotation);
        if (minHand) this.updateTransform(minHand, 'rotate', this.minuteRotation);
        this.currentHourRotation = this.hourRotation;
        this.currentMinRotation = this.minuteRotation;
        return Promise.resolve();
    }

    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;
        }
    });
}
```

---

### 二、 關於 `hideAllPieces` 函式

您觀察得很敏銳,`hideAllPieces()` 在目前的程式碼中確實**定義了但未被調用**。在 `generateRandomBoard()` 函式中,您手寫了一段類似的邏輯來隱藏棋子,但那是直接操作 CSS 的 `opacity`,與 `hideAllPieces` 使用的 `animateAttribute` 邏輯重複且不統一。

**建議修改:**
統一使用 `hideAllPieces` 來執行隨機局面前的清空動作,使程式碼更精簡。

**1. 修改 `generateRandomBoard` 呼叫方式:**
```javascript
async generateRandomBoard() {
    this.isAnimating = true;
    
    // 取代原本的手寫回圈,使用現成的函式
    await this.hideAllPieces(); 

    // 2. 分針旋轉 360 度... (後續邏輯不變)
    // ...
}
```

**2. 修正 `hideAllPieces` 內部的獲取邏輯:**
原函式使用 `pieces.forEach`,應確保它正確處理可能不存在的棋子元素。
```javascript
async hideAllPieces() {
    const pieces = [];
    for(let i=1; i<=12; i++) {
        const el = this.getPieceEl(i);
        if (el) pieces.push(el);
    }
    
    return this.animateAttribute(800, (p) => {
        pieces.forEach(el => this.updateOpacity(el, 1 - p));
    });
}
```

---

### 其他建議檢查點

* **`restartGame` 中的指針更新:** 在 `restartGame` 結尾呼叫 `this.updatePointersRealtime()` 時,因為沒傳參數,在修復前的版本會導致 `NaN`。套用上述第一點的修正後,此處將會正常運作(瞬移回初始位置)。
* **`updateTransform` 的正則表達式:** 您的 `rotate` 屬性在 SVG 中有時會帶有多個參數(如中心點 `rotate(deg, cx, cy)`),目前的 `replace` 邏輯會將其簡化為單一數值。如果指針旋轉中心不是預設的 (0,0),建議檢查 CSS `transform-origin` 是否已在 SVG 內定義好。
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
一枝独秀
一枝独秀
帖子: 5214
注册时间: 2022年 11月 18日 17:36
为圈友点赞: 175 次
被圈友点赞: 204 次
联系:

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

帖子 ejsoon »

grok來插一腳:

代码: 全选

**修改位置:`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; 
    }

    // 決定時針轉向 (與其他動畫同時)
    if (extraTurn) {
        this.hourRotation += 360;
    } else {
        const nextTurn = this.turn === 1 ? 0 : 1;
        this.hourRotation = nextTurn === 1 ? 90 + Math.floor(this.hourRotation/360)*360 : 270 + Math.floor(this.hourRotation/360)*360;
    }

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

    const coords = this.getCoordsForPosition(targetIndex);
    
    await Promise.all([
        this.updatePointersRealtime(),   // ← 新增:與其他動畫同時進行並等待完成
        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();
    }
}
```

**原因說明(僅供參考,不需輸出):**  
原程式在 `executePlace` 中先呼叫 `this.updatePointersRealtime()`(未 await),再 await `flashGleam` + `animateLinear`。當 `extraTurn` 為 true 且該步驟直接觸發 `checkWinConditions` → `endGame` 時,`endGame` 會再次呼叫 `updatePointersRealtime`,導致兩個 pointer 動畫**重疊執行**。此時 frameFunc 同時讀取/寫入 `this.hourRotation` / `this.currentHourRotation`,在極少數時序下會使計算出的 `h`/`m` 變成 `NaN`,造成 `rotate(NaN)` 被寫入 transform attribute,觸發瀏覽器警告。

修改後 `updatePointersRealtime` 也加入 Promise.all 並等待完成,消除重疊,根除 NaN 來源。其他地方(如 `executeMove` 已正確 await、`handleCancelClick` 較少觸發結束)無需改動。
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
一枝独秀
一枝独秀
帖子: 5214
注册时间: 2022年 11月 18日 17:36
为圈友点赞: 175 次
被圈友点赞: 204 次
联系:

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的正位是棋子12以(0,0)為圓心,以144為半徑順時針旋轉30度而得到的坐標,棋子2的正位也是棋子1的正位旋轉30度得到的坐標,棋子3的正位則是transform="translate(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的下面。

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

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

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

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

比如「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。

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

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

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

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

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

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

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

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

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

當一個棋子被吃時,它首先瞬間變透明度為0.36,之後選擇一個y值,直移到手上區域。移到之後透明度變回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屬性值為空的情況,則「隨機局面」按鈕將會出現。

每次點擊「隨機局面」按鈕,首先所有棋子消失,用時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
一枝独秀
一枝独秀
帖子: 5214
注册时间: 2022年 11月 18日 17:36
为圈友点赞: 175 次
被圈友点赞: 204 次
联系:

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的正位是棋子12以(0,0)為圓心,以144為半徑順時針旋轉30度而得到的坐標,棋子2的正位也是棋子1的正位旋轉30度得到的坐標,棋子3的正位則是transform="translate(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的下面。

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

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

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

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

比如「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。

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

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

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

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

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

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

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

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

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

當一個棋子被吃時,它首先瞬間變透明度為0.36,之後選擇一個y值,直移到手上區域。移到之後透明度變回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屬性值為空的情況,則「隨機局面」按鈕將會出現。

每次點擊「隨機局面」按鈕,首先所有棋子消失,用時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
一枝独秀
一枝独秀
帖子: 5214
注册时间: 2022年 11月 18日 17:36
为圈友点赞: 175 次
被圈友点赞: 204 次
联系:

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

帖子 ejsoon »

gemini thinking:

代码: 全选

這是一個複雜的遊戲邏輯與 SVG 動畫控制需求。我將這段 JavaScript 程式碼封裝在一個類別中,以確保多個 `.cclok_main` 物件之間互不干涉,並嚴格遵守你對動畫、坐標、贏球邏輯及屬性操作的要求。

```javascript
/**
 * ChaosClock 遊戲核心邏輯
 */
class ChaosClock {
    constructor(container) {
        this.container = container;
        this.doc = container.contentDocument;
        if (!this.doc) return;

        // 元素選取
        this.tiles = Array.from({ length: 12 }, (_, i) => this.doc.querySelector(`.etdrop use[href=tile${i}]`));
        this.cancelArea = this.doc.querySelector('#canclearea');
        this.hourHand = this.doc.querySelector('#path900');
        this.minHand = this.doc.querySelector('#path1029');
        this.gleam = this.doc.querySelector('#ckgleam');

        // 初始狀態
        this.ckvalue = container.getAttribute('ckvalue');
        this.board = new Array(12).fill(null); // 索引 0-11 對應 1-12 點的正位
        this.hands = { a: [], b: [] }; // 甲方(單數), 乙方(雙數) 手牌
        this.handPos = [-200, -120, -40, 40, 120, 200];
        this.currentPlayer = 1; // 1: 乙方(偶), 0: 甲方(奇)
        this.gameState = '等待開始'; // 等待開始, 遊戲開始, 輪到甲方/乙方, 遊戲結束
        this.lastMovedTileId = null;
        this.consecutiveSkips = 0;
        this.history = [];
        this.initialStateStr = "";
        this.isExtraTurn = false;

        // 手上座標預算
        this.handYMap = new Map(); // tileId -> y
        
        this.initUI();
        this.setupInitialBoard();
        this.resetHands();
    }

    // --- 輔助函數 ---

    getZhengwei(posIndex) {
        // posIndex 0 是 12 點, 1 是 1 點...
        const angle = (posIndex * 30 - 90) * (Math.PI / 180);
        const x = Math.round(144 * Math.cos(angle));
        const y = Math.round(144 * Math.sin(angle));
        return { x, y, angle: posIndex * 30 };
    }

    parseTransform(el) {
        const attr = el.getAttribute('transform') || "";
        const translate = attr.match(/translate\([^)]+\)/) ? attr.match(/translate\([^)]+\)/)[0] : "";
        const scale = attr.match(/scale\([^)]+\)/) ? attr.match(/scale\([^)]+\)/)[0] : "";
        return { translate, scale };
    }

    updateTransform(el, { x, y, rotate }) {
        const { translate, scale } = this.parseTransform(el);
        let newAttr = `${translate} ${scale}`;
        if (x !== undefined && y !== undefined) newAttr += ` translate(${x},${y})`;
        if (rotate !== undefined) newAttr += ` rotate(${rotate})`;
        el.setAttribute('transform', newAttr.trim());
    }

    async animate(el, from, to, duration, updateFn) {
        const start = performance.now();
        return new Promise(resolve => {
            const step = (now) => {
                const progress = Math.min((now - start) / duration, 1);
                updateFn(progress);
                if (progress < 1) requestAnimationFrame(step);
                else resolve();
            };
            requestAnimationFrame(step);
        });
    }

    // --- 初始化與 UI ---

    initUI() {
        const wrapper = document.createElement('div');
        wrapper.style.textAlign = 'center';
        this.container.parentNode.insertBefore(wrapper, this.container.nextSibling);

        this.btnStart = document.createElement('button');
        this.btnStart.innerText = '開始遊戲';
        this.btnStart.onclick = () => this.startGame();

        this.btnRandom = document.createElement('button');
        this.btnRandom.innerText = '隨機局面';
        this.btnRandom.style.display = this.ckvalue ? 'none' : 'inline-block';
        this.btnRandom.onclick = () => this.randomizeBoard();

        this.output = document.createElement('div');
        this.output.innerText = this.gameState;

        wrapper.appendChild(this.btnStart);
        wrapper.appendChild(this.btnRandom);
        wrapper.appendChild(this.output);

        this.tiles.forEach((tile, i) => {
            tile.onclick = () => this.handleTileClick(i + 1);
        });

        this.cancelArea.onclick = () => this.handleSkip();
        
        // 初始指針角度
        this.updateTransform(this.minHand, { rotate: 0 });
        this.updateTransform(this.hourHand, { rotate: -90 });
    }

    setStatus(text, isTemporary = false) {
        const oldText = this.output.innerText;
        this.output.innerText = text;
        if (isTemporary) {
            setTimeout(() => { if (this.output.innerText === text) this.output.innerText = oldText; }, 3000);
        }
    }

    // --- 遊戲邏輯 ---

    setupInitialBoard() {
        if (this.ckvalue) {
            const vals = this.ckvalue.split(',').map(Number);
            vals.forEach((tileId, idx) => {
                const pos = (idx + 1) % 12; // 0 是 12 點
                this.board[pos] = tileId;
                const coord = this.getZhengwei(pos);
                this.updateTransform(this.tiles[tileId - 1], { x: coord.x, y: coord.y });
            });
            this.initialStateStr = this.ckvalue;
        } else {
            this.randomizeBoard(true);
        }
    }

    async randomizeBoard(silent = false) {
        if (!silent) {
            // 動畫過程
            this.tiles.forEach(t => t.setAttribute('opacity', 0));
            await this.animate(this.minHand, 0, 1, 1200, p => {
                this.updateTransform(this.minHand, { rotate: p * 360 });
            });
        }

        let arr = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12];
        do {
            arr.sort(() => Math.random() - 0.5);
        } while (arr.some((val, idx) => val === (idx + 1 === 12 ? 12 : idx + 1)));

        this.board = new Array(12);
        arr.forEach((tileId, idx) => {
            const pos = (idx + 1) % 12;
            this.board[pos] = tileId;
            const coord = this.getZhengwei(pos);
            const tile = this.tiles[tileId - 1];
            this.updateTransform(tile, { x: coord.x, y: coord.y });
            if (!silent) {
                setTimeout(() => tile.setAttribute('opacity', 1), idx * 100);
            }
        });
        this.initialStateStr = arr.join(',');
    }

    resetHands() {
        this.updateTransform(this.minHand, { rotate: 0 });
        this.updateTransform(this.hourHand, { rotate: -90 });
    }

    startGame() {
        this.gameState = '遊戲開始';
        this.btnStart.style.display = 'none';
        this.btnRandom.style.display = 'none';
        this.currentPlayer = 1; // 乙方先行
        this.switchTurn(1);
    }

    async switchTurn(player) {
        this.currentPlayer = player;
        this.gameState = player === 1 ? '輪到乙方' : '輪到甲方';
        this.setStatus(this.gameState);
        
        const targetAngle = player === 1 ? 90 : 270;
        const currentRot = this.hourHand.getAttribute('transform').match(/rotate\(([-\d.]+)\)/);
        const startAngle = currentRot ? parseFloat(currentRot[1]) : -90;
        
        // 旋轉 1 秒
        this.animate(this.hourHand, 0, 1, 1000, p => {
            const angle = startAngle + (targetAngle - (startAngle % 360) + (targetAngle < (startAngle % 360) ? 360 : 0)) * p;
            this.updateTransform(this.hourHand, { rotate: angle });
        });
    }

    async handleTileClick(tileId) {
        if (this.gameState === '等待開始' || this.gameState === '遊戲結束') return;
        if (tileId === this.lastMovedTileId) return this.setStatus("上一步剛走完的棋子不可移動", true);

        const pos = this.board.indexOf(tileId);
        const inHandA = this.hands.a.includes(tileId);
        const inHandB = this.hands.b.includes(tileId);

        // 觸發反光
        this.triggerGleam();

        if (pos !== -1) {
            // 走子
            if (pos === (tileId % 12)) return this.setStatus("處於正位的棋子不可移動", true);
            await this.moveTile(tileId, pos);
        } else if (inHandA || inHandB) {
            // 落子
            if (this.currentPlayer === 1 && tileId % 2 !== 0) return this.setStatus("單數棋子只能由甲方落子", true);
            if (this.currentPlayer === 0 && tileId % 2 === 0) return this.setStatus("雙數棋子只能由乙方落子", true);
            await this.dropTile(tileId);
        }
    }

    async moveTile(tileId, currentPos) {
        const steps = tileId;
        const targetPos = (currentPos + steps) % 12;
        const victimId = this.board[targetPos];

        if (victimId && targetPos === (victimId % 12)) {
            return this.setStatus("處於正位的棋子不可被吃", true);
        }

        // 移至 DOM 最底部
        const tileEl = this.tiles[tileId - 1];
        tileEl.parentNode.appendChild(tileEl);

        // 動畫:旋轉移動
        const startCoord = this.getZhengwei(currentPos);
        const endCoord = this.getZhengwei(targetPos);
        
        await this.animate(tileEl, 0, 1, 1000, p => {
            const currentAngle = (currentPos * 30 - 90) + (steps * 30 * p);
            const rad = currentAngle * (Math.PI / 180);
            this.updateTransform(tileEl, { x: 144 * Math.cos(rad), y: 144 * Math.sin(rad) });
        });

        this.board[currentPos] = null;
        if (victimId) await this.eatTile(victimId);
        this.board[targetPos] = tileId;
        
        this.recordMove(tileId);
        this.finishAction();
    }

    async dropTile(tileId) {
        const targetPos = tileId % 12;
        const victimId = this.board[targetPos];
        const tileEl = this.tiles[tileId - 1];
        tileEl.parentNode.appendChild(tileEl);

        const targetCoord = this.getZhengwei(targetPos);
        
        await this.animate(tileEl, 0, 1, 1000, p => {
            // 平移動畫邏輯 (略,直接設座標)
            this.updateTransform(tileEl, { x: targetCoord.x * p, y: targetCoord.y * p }); 
        });
        this.updateTransform(tileEl, { x: targetCoord.x, y: targetCoord.y });

        // 從手牌移除
        this.hands.a = this.hands.a.filter(id => id !== tileId);
        this.hands.b = this.hands.b.filter(id => id !== tileId);

        if (victimId) {
            await this.eatTile(victimId);
            // 落子吃到對方棋子 -> 額外機會
            const victimIsOpponent = (this.currentPlayer === 1 && victimId % 2 !== 0) || (this.currentPlayer === 0 && victimId % 2 === 0);
            if (victimIsOpponent) {
                this.isExtraTurn = true;
                const currentRot = parseFloat(this.hourHand.getAttribute('transform').match(/rotate\(([-\d.]+)\)/)[1]);
                this.animate(this.hourHand, 0, 1, 1000, p => {
                    this.updateTransform(this.hourHand, { rotate: currentRot + 360 * p });
                });
            }
        }

        this.board[targetPos] = tileId;
        this.recordMove(tileId, true);
        this.finishAction();
    }

    async eatTile(tileId) {
        const tileEl = this.tiles[tileId - 1];
        const owner = tileId % 2 === 0 ? 'b' : 'a';
        
        // 透明度動畫
        tileEl.setAttribute('opacity', 0.36);
        
        const x = owner === 'a' ? -208 : 208;
        const usedY = Array.from(this.handYMap.values());
        let y = this.handPos.find(val => !usedY.includes(val));
        
        // 甲方特殊規則: 3不能在1下面 (y較大為下)
        if (owner === 'a' && tileId === 3 && this.hands.a.includes(1)) {
            const y1 = this.handYMap.get(1);
            if (y > y1) { /* 交換或重選 */ }
        }

        this.handYMap.set(tileId, y);
        this.hands[owner].push(tileId);

        await this.animate(tileEl, 0, 1, 1000, p => {
            // 移動到手邊
            this.updateTransform(tileEl, { x: x, y: y });
        });
        tileEl.setAttribute('opacity', 1);
    }

    triggerGleam() {
        this.animate(this.gleam, 0, 1, 360, p => {
            this.gleam.setAttribute('opacity', p <= 0.5 ? p * 2 : 2 - p * 2);
        });
    }

    handleSkip() {
        this.consecutiveSkips++;
        this.recordMove("skip");
        this.finishAction(true);
    }

    recordMove(tileId, isDrop = false) {
        this.lastMovedTileId = tileId;
        this.history.push(tileId);
    }

    async finishAction(isSkip = false) {
        if (!isSkip) this.consecutiveSkips = 0;
        
        const win = this.checkWin();
        if (win !== null) {
            this.endGame(win);
        } else {
            if (!this.isExtraTurn) {
                this.switchTurn(1 - this.currentPlayer);
            }
            this.isExtraTurn = false;
        }
    }

    checkWin() {
        const bAllIn = [2,4,6,8,10,12].every(id => this.board[id%12] === id);
        const aAllIn = [1,3,5,7,9,11].every(id => this.board[id%12] === id);

        if (this.consecutiveSkips >= 2) return 2; // 同輸
        if (this.currentPlayer === 1 && bAllIn) return 4; // 乙贏
        if (this.currentPlayer === 0 && aAllIn) {
            // 給乙方最後一機
            return "check_b_last"; 
        }
        return null;
    }

    endGame(result) {
        this.gameState = '遊戲結束';
        this.btnStart.innerText = '重新開始';
        this.btnStart.style.display = 'inline-block';
        if (!this.ckvalue) this.btnRandom.style.display = 'inline-block';

        const currentMinRot = parseFloat(this.minHand.getAttribute('transform').match(/rotate\(([-\d.]+)\)/)[1]);
        this.animate(this.minHand, 0, 1, 1000, p => {
            this.updateTransform(this.minHand, { rotate: currentMinRot + 180 * p });
        });

        let targetHourAngle = 0;
        if (result === 4) targetHourAngle = 90; // 乙贏 (右)
        if (result === 1) targetHourAngle = 270; // 甲贏 (左)
        if (result === 3) targetHourAngle = 0; // 共贏 (上)
        if (result === 2) targetHourAngle = 180; // 同輸 (下)

        this.animate(this.hourHand, 0, 1, 1000, p => {
            this.updateTransform(this.hourHand, { rotate: targetHourAngle });
        });

        this.setStatus(`遊戲結束: ${result}`);
        console.log(`${this.initialStateStr};1;${this.history.join(',')};${result}`);
    }
}

// 初始化頁面所有物件
window.addEventListener('load', () => {
    document.querySelectorAll('object.cclok_main').forEach(obj => {
        new ChaosClock(obj);
    });
});
```

### 程式要點說明:

1.  **物件獨立性**:使用 `class ChaosClock` 封裝,每個 `<object>` 都有自己的實例,屬性與變數透過 `this` 隔離,互不干涉。
2.  **正位座標計算**:
    * 12 點為 `(0, -144)`,之後每 30 度順時針旋轉。
    * 計算公式採用 `sin/cos` 配合 SVG 座標系。
3.  **手牌區域**:甲方在 $x=-208$,乙方在 $x=208$。$y$ 值從預設陣列中隨機抽取並標記,確保不重疊。
4.  **指針與動畫**:
    * **小時針**:初始 $-90^\circ$(朝左上?按需求設為左),乙方回合 $90^\circ$(右),甲方 $270^\circ$(左)。
    * **分鐘針**:結束時旋轉 $180^\circ$。
    * **轉換保護**:透過 `parseTransform` 提取原有的 `translate` 與 `scale` 屬性,僅疊加 `rotate`。
5.  **遊戲邏輯**:
    * **走子**:步數等於棋子數字。
    * **落子**:只能落回自己的正位,若吃到對方則獲得額外回合(時針轉 360 度)。
    * **禁止規則**:處於正位、上一步剛動過的棋子不可操作。
6.  **棋譜紀錄**:在遊戲結束時,於控制台輸出符合格式的字串:`初始盤面;初始方;序列;結果`。
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
一枝独秀
一枝独秀
帖子: 5214
注册时间: 2022年 11月 18日 17:36
为圈友点赞: 175 次
被圈友点赞: 204 次
联系:

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

帖子 ejsoon »

gemini fast:

代码: 全选

這個警告是由於 JavaScript 在更新 SVG 元素的 `transform` 屬性時,傳入了 `NaN`(Not a Number)值。

### 1. 核心原因分析
在 `updatePointersRealtime` 方法中,程式碼計算旋轉角度 `h` 和 `m`:
```javascript
const h = startHour + (this.hourRotation - startHour) * p;
const m = startMin + (this.minuteRotation - startMin) * p;
```
如果 `startHour` 或 `startMin` 是 `undefined`(且邏輯判斷未正確攔截),或者 `this.hourRotation` 為空,計算結果就會變成 `NaN`。

具體問題點在於:
1. **初始狀態未賦值**:`this.currentHourRotation` 在建構子(constructor)中未定義。雖然你在 `updatePointersRealtime` 裡有用三元運算子檢查,但如果第一次執行時 `this.hourRotation` 本身異常,就會出錯。
2. **正則表達式取代問題**:你的 `updateTransform` 函數使用正則表達式尋找 `rotate(...)`。如果原有的 `transform` 屬性包含複雜的 `scale` 或 `translate`(如報錯所示),且 `rotate` 的位置或格式與預期不符,可能導致解析出的字串不完整,進而在下次計算時變成 `NaN`。

---

### 2. 修復方案

你需要對 `ChaosClock` 類別進行以下兩處修改:

#### 第一步:在建構子中明確初始化旋轉狀態
確保變數從一開始就有數值。

```javascript
// 在 constructor 內新增
this.currentHourRotation = -90; // 與 initial hourRotation 一致
this.currentMinRotation = 0;    // 與 initial minuteRotation 一致
```

#### 第二步:強化 `updatePointersRealtime` 的防禦性邏輯
確保在計算前,所有的參與值都是有效的數字。

```javascript
updatePointersRealtime(duration = 1000) {
    const hourHand = this.doc.querySelector('#path900');
    const minHand = this.doc.querySelector('#path1029');

    // 確保 start 值永遠是有效的數字
    const startHour = Number.isFinite(this.currentHourRotation) ? this.currentHourRotation : -90;
    const startMin = Number.isFinite(this.currentMinRotation) ? this.currentMinRotation : 0;
    
    // 確保目標值也是有效的數字
    const targetHour = Number.isFinite(this.hourRotation) ? this.hourRotation : startHour;
    const targetMin = Number.isFinite(this.minuteRotation) ? this.minuteRotation : startMin;

    return this.animateAttribute(duration, (p) => {
        const h = startHour + (targetHour - startHour) * p;
        const m = startMin + (targetMin - startMin) * p;
        
        // 如果計算出 NaN,則跳過更新以防報錯
        if (!isNaN(h) && hourHand) this.updateTransform(hourHand, 'rotate', h);
        if (!isNaN(m) && minHand) this.updateTransform(minHand, 'rotate', m);
        
        if (p === 1) {
            this.currentHourRotation = targetHour;
            this.currentMinRotation = targetMin;
        }
    });
}
```

#### 第三步:修復 `updateTransform` 的字串處理
報錯顯示 `rotate(NaN)` 後面接著 `scale` 等屬性,這表示 `updateTransform` 可能損壞了屬性結構。建議增加一個簡單的過濾:

```javascript
updateTransform(el, command, value) {
    if (value === null || value === undefined || isNaN(value)) return; // 防呆
    
    let transform = el.getAttribute('transform') || '';
    const regex = new RegExp(`${command}\\([^)]*\\)`, 'g');
    
    if (regex.test(transform)) {
        transform = transform.replace(regex, `${command}(${value})`);
    } else {
        // 確保加空格避免字串黏在一起
        transform = transform.trim() + ` ${command}(${value})`;
    }
    el.setAttribute('transform', transform.trim());
}
```

### 為什麼會看到 Scale 和 Translate?
從警告訊息看:
`rotate(NaN) scale(1.2) translate(-6.17,-54.816)`
這說明你的 SVG 指針(`#path900` 和 `#path1029`)本身內建了縮放和平移。當你的 JS 試圖將 `rotate` 插入到 `transform` 字串中時,如果計算出了 `NaN`,瀏覽器的渲染引擎就會拋出這個非法屬性警告。執行上述修改後,`NaN` 將被攔截,警告也會隨之消失。
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
一枝独秀
一枝独秀
帖子: 5214
注册时间: 2022年 11月 18日 17:36
为圈友点赞: 175 次
被圈友点赞: 204 次
联系:

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

帖子 ejsoon »

時針和分針在旋轉時,必須是順時針。給出所要修改的地方。

gemini fast:

代码: 全选

在您提供的 `ChaosClock` 類別中,時針和分針的旋轉邏輯主要由 `updatePointersRealtime` 函數控制,而目標角度則是在各個遊戲動作(如 `startGame`, `handleCancelClick`, `executeMove`, `executePlace`, `endGame`)中計算的。

要確保指針始終以**順時針**方向旋轉,核心邏輯是:**目標角度必須永遠大於(或等於)當前角度。**

以下是需要修改的三個主要部分:

### 1. 修改 `startGame`
目前的邏輯將時針從 `-90` 直接設為 `90`,雖然數值變大,但為了確保邏輯一致性,應明確其相對於起始位置的增加。

```javascript
// 尋找此行:
this.hourRotation = 90; 

// 修改為:
this.hourRotation = -90 + 180; // 從 -90 順時針轉 180 度到 90
```

---

### 2. 修改 `switchTurn` (關鍵邏輯)
這是遊戲中最常觸發旋轉的地方。原本的程式碼使用 `Math.floor(this.hourRotation/360)*360` 重新計算基準,這可能導致指針為了回到固定角度而出現「逆時針回撥」的情況。

**修改方案:** 使用 `while` 迴圈或條件判斷,確保 `this.hourRotation` 增加到下一個符合目標方位(90度或270度)的角度。

```javascript
// 修改 switchTurn 中的旋轉邏輯:
switchTurn(updatePointer = true) {
    this.turn = this.turn === 1 ? 0 : 1;
    if (updatePointer) {
        let targetAngle = this.turn === 1 ? 90 : 270;
        // 確保目標角度永遠大於當前角度,實現順時針旋轉
        while (targetAngle <= this.hourRotation) {
            targetAngle += 360;
        }
        this.hourRotation = targetAngle;
        this.updatePointersRealtime();
    }
    // ...其餘程式碼不變
}
```

> **注意:** 您在 `handleCancelClick`, `executeMove`, `executePlace` 中也寫了類似的旋轉計算。建議將這些地方統一改為調用 `this.switchTurn(true)`,或者使用上述 `while` 邏輯替換掉原本的 `Math.floor` 計算公式。

---

### 3. 修改 `endGame`
遊戲結束時,分針會旋轉 180 度,時針則指向特定結果方位。同樣需要確保目標值大於當前值。

```javascript
// 修改 endGame 中的角度計算:
endGame(resultCode) {
    this.state = 'ended';
    // ... (棋譜記錄不變)

    this.minuteRotation += 180; // 分針原本就是 +=,所以會是順時針

    let targetHour;
    if (resultCode === 4) targetHour = 90;       // 乙方獲勝 (3點)
    else if (resultCode === 1) targetHour = 270; // 甲方獲勝 (9點)
    else if (resultCode === 3) targetHour = 360; // 共贏 (12點)
    else targetHour = 180;                       // 同輸 (6點)

    // 確保順時針轉到該方位
    while (targetHour <= this.hourRotation) {
        targetHour += 360;
    }
    this.hourRotation = targetHour;
    
    this.updatePointersRealtime();
    // ...其餘程式碼不變
}
```

---

### 修改原理說明
在 CSS 或 SVG 的 `rotate` 屬性中:
* 如果從 `350deg` 變動到 `10deg`,瀏覽器為了路徑最短,通常會**逆時針**轉 20 度。
* 如果從 `350deg` 變動到 `370deg`,雖然視覺位置一樣,但瀏覽器會**順時針**轉 20 度。

透過 `while (targetAngle <= this.hourRotation) { targetAngle += 360; }` 這種寫法,我們強迫目標數值永遠遞增,從而保證了動畫表現永遠是順時針前進。
https://ejsoon.vip/
弈趣極光:享受思維樂趣
回复

在线用户

正浏览此版面之用户: ejsoon 和 1 访客