將改進混亂時鐘的網頁版

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

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

帖子 ejsoon »

當有圖片處於「自動保存」狀態時,點擊一個圖標,它應該是上載,而不是切換所保存的圖片。
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
一枝独秀
一枝独秀
帖子: 5210
注册时间: 2022年 11月 18日 17:36
为圈友点赞: 175 次
被圈友点赞: 204 次
联系:

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

帖子 ejsoon »

改進ejtilelocal:

代码: 全选

改進程式:
1.把add、delete、update、undo、redo、dataarea、minimize等這些過於簡單的id加上前綴ejlcl。

2.當點擊update時,將進入「伸手」狀態。當處於「伸手」狀態時,點擊其中一個dataarea>img.ejtilesvgdata,則「握手」成功,程式將每隔7秒就把數據保存至img所指向的地方。

3.當「握手成功」後,再次點擊任意一個dataarea>img.ejtilesvgdata,包括「握手成功」的那個,它的作用都是將此data上載。

4.當「握手」之後再次點擊update,它將會「放手」,即取消每隔7秒保存數據。當只處於「伸手」狀態,再次點擊update也會取消此狀態。

5.當點擊delete、undo或redo時,如果處於「握手」狀態,則先「放手」,再執行相應操作。

6.當點擊delete時,它應始終保持「刪除狀態」,點擊其中一個dataarea>img.ejtilesvgdata則刪除相應數據,刪除完後不會退出該狀態,只有再次點擊delete才會退出。

7.只有在「握手、放手」時才會把當前數據存進undo stack中,每隔7秒保存不會。

只需給出所要修改的地方。所有的代碼和注釋都要使用英文。
附件
ejtilelocalstorage202604181010.js
(11.6 KiB) 已下载 5 次
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
一枝独秀
一枝独秀
帖子: 5210
注册时间: 2022年 11月 18日 17:36
为圈友点赞: 175 次
被圈友点赞: 204 次
联系:

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

帖子 ejsoon »

發現gemoni fast已經夠用了:

代码: 全选

To improve the script while maintaining its functionality and structure, here are the specific modifications. These changes update IDs with the `ejlcl` prefix and implement the new "Handshake" logic for auto-saving.

### 1. Updated HTML String and Element Selection
Change the IDs in the `htmlString` and the corresponding variable assignments to use the `ejlcl` prefix.

```javascript
// Updated HTML with ejlcl prefixes
const htmlString = `
    <style>
        /* ... existing styles ... */
        .functionbuttons .select svg { border: 1px solid #a72; }
    </style>
    <div class="savetolocalstorage">
        <div id="ejlclminimize">
            <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><line x1="4" y1="12" x2="20" y2="12"></line></svg>
        </div>
        <h4 class="savetolocalstorageh4">Local Storage</h4>
        <div class="functionbuttons">
            <span id="ejlcladd"><svg ...></span>
            <span id="ejlcldelete"><svg ...></span>
            <span id="ejlclupdate"><svg ...></span>
            <span id="ejlclundo"><svg ...></span>
            <span id="ejlclredo"><svg ...></span>
        </div>
        <div id="ejlcldataarea" class="dataarea" style="position: relative;"></div>
    </div>
`;

// Updated Element Selectors
const dataarea = document.getElementById('ejlcldataarea');
const addButton = document.getElementById('ejlcladd');
const deleteButton = document.getElementById('ejlcldelete');
const updateButton = document.getElementById('ejlclupdate');
const undoButton = document.getElementById('ejlclundo');
const redoButton = document.getElementById('ejlclredo');
const minimizeButton = document.getElementById('ejlclminimize');
```

---

### 2. Implementation of Handshake and Persistent Delete Logic
The following modifications replace the existing click listeners and logic for `dataarea`, `updateButton`, and `deleteButton`.

```javascript
// New state variable for "Handshake" status
let isHandshaking = false; 

function releaseHand() {
    if (autoSaveInterval) {
        clearInterval(autoSaveInterval);
        autoSaveInterval = null;
    }
    if (autoSaveTarget) {
        autoSaveTarget.style.border = '';
        autoSaveTarget = null;
        isHandshaking = false;
        saveState(); // Push to undo stack only on "release"
    }
}

// Logic for dataarea click (Handshake and Upload)
dataarea.addEventListener('click', function(event) {
    const target = event.target;
    if (target.classList.contains('ejtilesvgdata')) {
        if (deleteMode) {
            deleteSvg(target);
            // No longer exiting deleteMode here; remains active until toggled
        } else if (updateMode && !isHandshaking) {
            // Reaching out -> Handshake Success
            saveState(); // Save state before starting handshake
            autoSaveTarget = target;
            autoSaveTarget.style.border = '2px solid #a72';
            isHandshaking = true;
            autoSaveInterval = setInterval(autoSaveTick, 7000);
        } else {
            // Normal click OR click during active Handshake: Upload data
            etmain.outerHTML = atob(target.src.split(',')[1]);
            etinit();
            recordHistory();
        }
        setButtonBorder();
    }
});

// Update Button: Toggle Handshake
updateButton.addEventListener('click', function() {
    if (updateMode) {
        if (isHandshaking) releaseHand();
        updateMode = false;
    } else {
        updateMode = true;
        deleteMode = false; // Disable delete if update is turned on
    }
    setButtonBorder();
});

// Delete Button: Toggle Delete Mode and release Hand if active
deleteButton.addEventListener('click', function() {
    if (isHandshaking) releaseHand();
    deleteMode = !deleteMode;
    if (deleteMode) updateMode = false;
    setButtonBorder();
});

// Undo/Redo: Release Hand before operation
undoButton.addEventListener('click', () => {
    if (isHandshaking) releaseHand();
    undols();
});

redoButton.addEventListener('click', () => {
    if (isHandshaking) releaseHand();
    redols();
});
```

---

### 3. Updated `autoSaveTick`
Ensure the auto-save function updates storage but skips the `saveState()` history stack as requested.

```javascript
function autoSaveTick() {
    if (!autoSaveTarget) return;
    
    let etmainclone = etmain.cloneNode(true);
    let etmaincwait = etmainclone.querySelector(".etwait");
    if (etmaincwait) etmaincwait.remove();
    
    let data = etmainclone.outerHTML;
    autoSaveTarget.src = 'data:image/svg+xml;base64,' + btoa(data);
    
    // Update storage without pushing to undo stack
    updateLocalStorage();
    showSaveIndicator(autoSaveTarget);
}
```

---

### 4. Modified `deleteSvg`
Update the `deleteSvg` function to ensure it doesn't accidentally interfere with the persistent delete mode.

```javascript
function deleteSvg(img) {
    // If we happen to delete the auto-save target, stop the interval
    if (img === autoSaveTarget) {
        clearInterval(autoSaveInterval);
        autoSaveInterval = null;
        autoSaveTarget = null;
        isHandshaking = false;
    }
    img.remove();
    saveState();
    updateLocalStorage();
}
```
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
一枝独秀
一枝独秀
帖子: 5210
注册时间: 2022年 11月 18日 17:36
为圈友点赞: 175 次
被圈友点赞: 204 次
联系:

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

帖子 ejsoon »

算了還是用pro吧。

代码: 全选

改進程式:
1.把add、delete、update、undo、redo、dataarea、minimize等這些過於簡單的id加上前綴ejlcl。

2.當點擊update時,將進入「伸手」狀態。當處於「伸手」狀態時,點擊其中一個dataarea>img.ejtilesvgdata,則「握手」成功,程式將每隔7秒就把數據保存至img所指向的地方。

3.當「握手」之後再次點擊update,它將會「放手」,即取消每隔7秒保存數據。當只處於「伸手」狀態,再次點擊update也會取消此狀態。

4.當「握手成功」後,再次點擊任意一個dataarea>img.ejtilesvgdata,包括「握手成功」的那個,它的作用都是將此data上載。同時「放手」。

5.當點擊add、delete、undo或redo時,如果處於「握手」狀態,則先「放手」,再執行相應操作。

6.當點擊delete時,它應始終保持「刪除狀態」,點擊其中一個dataarea>img.ejtilesvgdata則刪除相應數據,刪除完後不會退出該狀態,只有再次點擊delete才會退出,或者點擊add、update、undo、redo也會先退出再執行相應功能。。

7.只有在「握手、放手」時才會把當前數據存進undo stack中,每隔7秒保存不會。

只需給出所要修改的地方。所有的代碼和注釋都要使用英文。
gemini pro:

代码: 全选

Here are the exact modifications needed to implement your requirements. Replace the corresponding sections in your original script with the code blocks below.

### 1. Update the `htmlString` (Prefixing IDs)
Replace the `<style>` block and HTML template to update `dataarea`, `minimize`, and the function buttons with the `ejlcl` prefix.

```javascript
// Replace the htmlString declaration
const htmlString = `
    <style>
        .savetolocalstorage {
            width: 80%;
            margin: 20px auto;
            border: 1px solid #ccc;
            padding: 10px;
            position: relative;
        }
        .functionbuttons > span {
            display: inline-block;
            width: 24px;
            height: 24px;
            box-sizing: content-box;
            margin-right: 7px;
        }
        .functionbuttons svg {
            border: 1px solid #ccc;
            cursor: pointer;
        }
        .functionbuttons .select svg {
            border: 1px solid #a72;
        }
        #ejlcldataarea {
            display: flex;
            flex-wrap: wrap;
        }
        .savetolocalstorage img {
            width: 120px;
            height: 120px;
            border: 1px solid #777;
            margin: 5px;
            cursor: pointer;
        }
        .savetolocalstorageh4 {
            margin: 4px 0;
        }
        #ejlclminimize {
            background: transparent;
            border: none;
            cursor: pointer;
            position: absolute;
            top: 10px;
            right: 10px;
        }
        #ejlclminimize svg {
            width: 24px;
            height: 24px;
            fill: none;
            stroke: currentColor;
            stroke-width: 2;
            stroke-linecap: round;
            stroke-linejoin: round;
            border: 1px solid #777;
        }
        #localStorageButton {
            margin-top: 7px;
        }
    </style>
    <div class="savetolocalstorage">
        <div id="ejlclminimize">
            <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
                <line x1="4" y1="12" x2="20" y2="12"></line>
            </svg>
        </div>
        <h4 class="savetolocalstorageh4">Local Storage</h4>
        <div class="functionbuttons">
            <span id="ejlcladd"><svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="24" height="24"><path d="M 5,12 h 14 M 12,5 v 14" stroke="#333" fill="none" stroke-width="2"></path></svg></span>
            <span id="ejlcldelete"><svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="24" height="24"><path d="M 5,12 h 14" stroke="#333" fill="none" stroke-width="2"></path></svg></span>
            <span id="ejlclupdate"><svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="24" height="24"><path d="M 12,2 L 12,20 M 7,15 L 12,20 L 17,15" stroke="#333" fill="none" stroke-width="2"></path></svg></span>
            <span id="ejlclundo"><svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="24" height="24"><path d="M 12,20 A 8,8 0 1 0 4,12" stroke="#333" stroke-width="2" fill="none"></path><path d="M 8,11 L 4,16 L 0,11 Z" stroke="none" fill="#333"></path></svg></span>
            <span id="ejlclredo"><svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="24" height="24"><path d="M 12,20 A 8,8 0 1 1 20,12" stroke="#333" stroke-width="2" fill="none"></path><path d="M 16,11 L 20,16 L 24,11 Z" stroke="none" fill="#333"></path></svg></span>
        </div>
        <div id="ejlcldataarea" style="position: relative;"></div>
    </div>
`;
```

### 2. Update Element Selectors
Replace the `querySelector` and `getElementById` assignments to match the newly prefixed IDs.

```javascript
// Replace the DOM query assignments
const dataarea = document.getElementById('ejlcldataarea');
const addButton = document.getElementById('ejlcladd');
const deleteButton = document.getElementById('ejlcldelete');
const updateButton = document.getElementById('ejlclupdate');
const undoButton = document.getElementById('ejlclundo');
const redoButton = document.getElementById('ejlclredo');
const minimizeButton = document.getElementById('ejlclminimize');
```

### 3. Add State Management Functions
Insert these helper functions right before your event listeners. They manage the "let go" functionality and clean up states before executing new commands.

```javascript
// Helper function to "let go" (cancel handshake or reaching out state)
function cancelHandshake() {
    if (updateMode) {
        updateMode = false;
        if (autoSaveInterval) {
            clearInterval(autoSaveInterval);
            autoSaveInterval = null;
        }
        if (autoSaveTarget) {
            autoSaveTarget.style.border = '';
            autoSaveTarget = null;
            saveState(); // Push to undo stack when letting go
        }
    }
}

// Helper function to let go AND cancel delete mode
function cancelHandshakeAndModes() {
    cancelHandshake();
    deleteMode = false;
    setButtonBorder();
}
```

### 4. Update Event Listeners
Replace the event listeners for `dataarea`, `add`, `delete`, `update`, `undo`, and `redo` with the following block to fulfill requirements #2, #3, #4, #5, and #6.

```javascript
// Replace existing event listeners for dataarea and buttons
dataarea.addEventListener('click', function(event) {
    const target = event.target;
    if (target.classList.contains('ejtilesvgdata')) {
        if (deleteMode) {
            // Persistent delete mode: execute delete, DO NOT turn off deleteMode
            deleteSvg(target);
        } else if (updateMode) {
            if (!autoSaveTarget) {
                // Currently "reaching out" -> "Handshake" successful
                saveState(); // Push to undo stack when turning on auto-save
                autoSaveTarget = target;
                autoSaveTarget.style.border = '2px solid #a72';
                
                if (autoSaveInterval) clearInterval(autoSaveInterval);
                autoSaveInterval = setInterval(autoSaveTick, 7000);
            } else {
                // Currently in "Handshake" -> load data and "let go"
                etmain.outerHTML = atob(target.src.split(',')[1]);
                etinit();
                recordHistory();
                cancelHandshake(); // Let go (cancels 7s interval & pushes to stack)
                setButtonBorder();
            }
        } else {
            // Normal click
            etmain.outerHTML = atob(target.src.split(',')[1]);
            etinit();
            recordHistory();
        }
    }
});

addButton.addEventListener('click', function() {
    cancelHandshakeAndModes(); // "Let go" first
    addSvg();
});

deleteButton.addEventListener('click', function() {
    if (deleteMode) {
        deleteMode = false; // Exit persistent delete mode
    } else {
        cancelHandshakeAndModes(); // "Let go" first
        deleteMode = true; // Enter persistent delete mode
    }
    setButtonBorder();
});

updateButton.addEventListener('click', function() {
    if (updateMode) {
        // "Let go" (cancel handshake or reaching out)
        cancelHandshake();
    } else {
        // "Reach out"
        cancelHandshakeAndModes(); // Cancel delete mode if active
        updateMode = true;
    }
    setButtonBorder();
});

undoButton.addEventListener('click', function() {
    cancelHandshakeAndModes(); // "Let go" first
    undols();
});

redoButton.addEventListener('click', function() {
    cancelHandshakeAndModes(); // "Let go" first
    redols();
});
```

### 5. Update the `showSaveIndicator` Function
Because the `add` button ID was changed, the selector used to clone the SVG inside `showSaveIndicator` must also be updated.

```javascript
// Replace the querySelector inside showSaveIndicator
function showSaveIndicator(img) {
    const addSvg = document.querySelector('#ejlcladd svg'); // Updated ID
    const plusIndicator = addSvg.cloneNode(true);
    
    // Position the SVG over the target image
    plusIndicator.style.position = 'absolute';
    plusIndicator.style.top = img.offsetTop + 'px';
    plusIndicator.style.left = img.offsetLeft + 'px';
    plusIndicator.style.width = '24px';
    plusIndicator.style.height = '24px';
    plusIndicator.style.borderRadius = '2px';
    plusIndicator.style.zIndex = '10';
    
    // Change the SVG path stroke to green
    const paths = plusIndicator.querySelectorAll('path');
    paths.forEach(p => p.setAttribute('stroke', '#0f0'));
    
    dataarea.appendChild(plusIndicator);
    
    // Remove the indicator after 500 milliseconds
    setTimeout(() => {
        plusIndicator.remove();
    }, 500);
}
```
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
一枝独秀
一枝独秀
帖子: 5210
注册时间: 2022年 11月 18日 17:36
为圈友点赞: 175 次
被圈友点赞: 204 次
联系:

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

帖子 ejsoon »

改進:
當點擊center時,如果svg中不存在「viewBox="0 0 480 480" preserveAspectRatio="xMidYMid meet"」屬性,則加上。當upload ejtile時,如果存在則要在上載的內容中去掉。

只需給出所要修改的地方,所有的代碼和注釋都要使用英文。
附件
ejtileanimate202511172231.js
(137.7 KiB) 已下载 8 次
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
一枝独秀
一枝独秀
帖子: 5210
注册时间: 2022年 11月 18日 17:36
为圈友点赞: 175 次
被圈友点赞: 204 次
联系:

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

帖子 ejsoon »

gemini pro:

代码: 全选

根據您的需求,以下是針對 `etaniCenterClick` 函數(處理 **center** 點擊)以及 `handleContentHTMLClick` 函數(處理 **upload ejtile**)的修改建議:

### 1. 修改 `etaniCenterClick` 函數
在設置 `etdrop` 的變換屬性之前,檢查 `etani`(SVG 對象)是否具有 `viewBox` 和 `preserveAspectRatio` 屬性,若無則添加。

```javascript
// Handle the 'Center' button click to reset the .etdrop transform
function etaniCenterClick() {
    const etdrop = etani.querySelector('.etdrop');
    if (!etdrop) return;

    // --- Add these lines to handle viewBox and preserveAspectRatio ---
    if (!etani.getAttribute('viewBox')) {
        etani.setAttribute('viewBox', '0 0 480 480');
    }
    if (!etani.getAttribute('preserveAspectRatio')) {
        etani.setAttribute('preserveAspectRatio', 'xMidYMid meet');
    }
    // ----------------------------------------------------------------

    // toggle center or current transform
    if (this.innerHTML === 'center') {
        this.innerHTML = 'current';
        etdrop.setAttribute('transform', 'translate(240,240) scale(1,1)');
        updateEtaniResult();
    } else {
        this.innerHTML = 'center';
        const originalSvg = document.getElementById('etmain');
        const originalDrop = originalSvg.querySelector('.etdrop');
        const originalTransform = originalDrop.getAttribute('transform');
        if (originalTransform) {
            etdrop.setAttribute('transform', originalTransform);
            updateEtaniResult();
        }
    }
}
```

### 2. 修改 `handleContentHTMLClick` 函數中的上載邏輯
在執行 `etaniEjtileClick()` 之前,先移除 `etani` 中存在的 `viewBox` 和 `preserveAspectRatio` 屬性。

```javascript
// Handle the HTML popup window
function handleContentHTMLClick() {
    if (!etani) return;
    // ... (existing code for UI setup)

    etaniWindow(htmlDiv, () => {
        const parser = new DOMParser();
        const eDom = parser.parseFromString(textarea.value, 'image/svg+xml');
        etani = eDom.documentElement;

        // ... (existing code for mode setting)

        // upload ejtile
        if (ejtileSpan.classList.contains('selected')) {
            // --- Add these lines to remove attributes before uploading ---
            etani.removeAttribute('viewBox');
            etani.removeAttribute('preserveAspectRatio');
            // -----------------------------------------------------------
            etaniEjtileClick();
        }

        // update UI and result
        updateUIandResult();
    });
}
```
gemini fast:

代码: 全选

// 修改 handleContentHTMLClick 函數內部的確認邏輯
etaniWindow(htmlDiv, () => {
    const parser = new DOMParser();
    const eDom = parser.parseFromString(textarea.value, 'image/svg+xml');
    
    // Only remove attributes from the content to be uploaded, not changing current etani attributes directly here
    const uploadContent = eDom.documentElement;

    // set repeat or freeze mode for the new etani state
    etani = uploadContent; // Update current state
    let eMode = document.querySelector('.etaniMode');
    eMode.querySelector('span.active').classList.remove('active');
    if (etani.getAttribute('rfm') === 'repeat') {
        eMode.querySelector('.etaniModeRepeat').classList.add('active');
    } else if (etani.getAttribute('rfm') === 'freeze') {
        eMode.querySelector('.etaniModeFreeze').classList.add('active');
    } else if (etani.getAttribute('rfm') === 'mixed') {
        eMode.querySelector('.etaniModeMixed').classList.add('active');
    }

    // upload ejtile
    if (ejtileSpan.classList.contains('selected')) {
        // --- Remove attributes only from the temporary upload document ---
        uploadContent.removeAttribute('viewBox');
        uploadContent.removeAttribute('preserveAspectRatio');
        // -----------------------------------------------------------------
        etaniEjtileClick();
    }

    // update UI and result
    updateUIandResult();
});
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
一枝独秀
一枝独秀
帖子: 5210
注册时间: 2022年 11月 18日 17:36
为圈友点赞: 175 次
被圈友点赞: 204 次
联系:

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

帖子 ejsoon »

開始重寫chaosclock了:

代码: 全选

重寫該程式:
1.保留取到svg的部分,以及區分兩種模式的部分。注意頁面上可能出現多個svg,它們之間不要發生干涉。

2.當取到svg之後,將其稱作棋盤,其內部的.etdrop>use[href=tilen]稱作棋子,其中n是0~11的整數,代表棋子1~12。

3.當button模式要產生隨機盤面時,將把每個棋子隨機放在非正位的地方(更改它的translate)。

棋子12的正位是(0,-144),從棋子1開始,以(0,0)為圓心,以144為半徑,每隔30度順時針偏移,就是它的正位坐標。

計算出的坐標值保留最多三位小數,要用「*1000,round(),/1000」。

4.當number模式時,把每個棋子按順序從棋子1的正位開始放。

比如[chaosclock]3,1,11,5,7,10,12,6,2,9,8,4[/chaosclock],則是把棋子3放在棋子1的正位,把棋子1放在棋子2的正位。

如果中間出現0,則說明有棋子是不在鐘面上的,這些棋子如果是單數,則把它移到(-208,y)的坐標上,雙數棋子則移到(208,y),y的值隨機從「-200,-120,-40,40,120,200」中取,一個位置只能放一個棋子。

比如[chaosclock]0,2,9,0,0,0,3,0,4,7,11,0[/chaosclock],則棋子1和棋子6等都不在鐘面上,最終隨機把棋子1移到了(-208,40),棋子6移到了(208,120)。

5.當完成以上步驟時,遊戲開始。拿到cclok中的#path1029和#path900,分別稱作分針和時針。

起初時針的rotate為90,表示輪到乙方,每當行動方切換,它就會增加180。

遊戲一開始是輪到乙方,當輪到任何一方時,他都可以點擊鐘面上的棋子,則這個棋子將會繞(0,0)坐標順時針旋轉1秒至它要到達的地方。

用js實現旋轉動畫,只包括對translate的改動。

棋子所要到達的地方,是它的棋子的數值乘以30度。如果所到達的地方有其它棋子,則這個棋子將會被吃掉。

被吃掉的棋子,如果是單數則移到「x=-208,y在六個數隨機取」的坐標上,雙數則是x=208。移動時先把透明度設為0.5,再用js實現移動動畫,用時1s,移完後再把透明度去掉。

比如[chaosclock]3,1,11,5,7,10,12,6,2,9,8,4[/chaosclock],這時輪到乙方,當點擊棋子3時,它將走到棋子5所在的位置,棋子5將變透明度0.5並移到左邊x為-208的某個地方。
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
一枝独秀
一枝独秀
帖子: 5210
注册时间: 2022年 11月 18日 17:36
为圈友点赞: 175 次
被圈友点赞: 204 次
联系:

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

帖子 ejsoon »

在用蒙特卡洛樹對一款弈棋進行搜尋時,一些棋子會無限循環走子,比如a棋子會走三步回到原地,b棋子走4步回到原地,這樣就會產生很多冗餘的走法。

對此有何解決辦法?
在博弈搜尋(尤其是蒙特卡洛樹搜尋,MCTS)中,**「循環走子」(Cycles/Lops)**確實是個令人頭疼的問題。它不僅浪費計算資源(讓樹變得虛胖),還可能導致搜尋陷入局部死循環,無法探索更有價值的路徑。

要解決這個問題,我們可以從**規則層面**、**數據結構層面**以及**搜尋策略層面**入手。

---

## 1. 狀態表示與重複檢測(基礎方案)

最直觀的方法是讓搜尋引擎「記住」走過的路。

* **使用 Zobrist Hashing(置換表):**
為棋盤上的每個位置和每種棋子分配一個隨機的 64 位整數。通過異或運算(XOR)快速計算當前局面的 Hash 值。
* **路徑記錄(Path History):**
在 MCTS 的單次模擬(Simulation)或選擇(Selection)過程中,維護一個當前路徑的 Hash 集合。
* **操作:** 如果下一步產生的局面 Hash 已經存在於當前路徑中,說明發生了循環。

---

## 2. 規則層面的約束(強制手段)

仿照經典棋類的規則,在「合法動作生成器」裡直接掐斷循環的可能性:

* **長跟/長打禁手(類似圍棋的「打劫」規則):**
禁止走回上一步或前幾步出現過的完全相同的局面。
* **和棋判定(類似象棋、國際象棋):**
* **三重複現(Threefold Repetition):** 如果同一個局面在同一局中出現三次,直接判定為和棋(在 MCTS 中給予評分 $V = 0$ 或一個中性值)。
* **步數限制(Max Steps):** 設定一個步數上限,超過後強制結束並根據當前子力估值。

---

## 3. MCTS 演算法的優化(進階方案)

如果你的遊戲本質上允許循環(例如某些特定的博弈),可以通過調整 MCTS 的行為來應對:

### 變「樹」為「圖」(Transposition Tables in MCTS)
標準 MCTS 是樹狀結構,同一個局面可能出現在不同的分支。
* **做法:** 使用**置換表(Transposition Table)**將具有相同 Hash 值的節點指向同一個記憶體位址。
* **效果:** 這樣搜尋結構就從「樹」變成了「有向圖」。當出現循環時,搜尋會自然地在圖中繞圈,而不是無窮無盡地向下生長新節點。

### 循環懲罰(Loop Penalty)
在 **Selection(選擇)** 階段,如果發現某個動作會導致回到祖先節點:
* **做法:** 人為降低該動作的 UCB 權重,或者在 **Backpropagation(回傳)** 時,給予該路徑一個極低的獎勵值(Penalty)。
* **邏輯:** 「既然走這步會回到原地,那這步棋顯然沒有進展,不如去探索別的。」

---

## 4. 針對「子循環」的啟發式剪枝

你提到 a 棋子 3 步回到原地,b 棋子 4 步回到原地。如果這些動作對全域局勢**毫無貢獻**,可以採用以下技巧:

* **空步檢測(Null Move Observation):** 如果一系列操作後,棋盤狀態與 $N$ 步前完全一致,且當前玩家沒有獲得任何額外優勢,則在展開節點時直接過濾掉這類「無效循環」動作。
* **進度計數器:** 在局面狀態中加入一個「進度因子」(例如:棋子向前推進的距離、吃子數量)。如果連續 $X$ 步進度因子沒有變化且發生了重複,則判定該分支價值為零。

---

## 總結建議

1. **首選:** 引入 **Zobrist Hash** 並實施 **「三重複現和棋」** 規則。這是代價最低、效果最好的方法。
2. **次選:** 如果你希望搜尋更聰明,實現 **置換表(Transposition Table)**,讓 MCTS 具備處理圖結構的能力。
3. **輔助:** 在 Rollout(隨機模擬)階段,設定硬性的步數上限,防止模擬階段掉進無限循環的黑洞。



您目前的棋類遊戲是否有明確的「和棋」規則?或者您是希望 AI 能夠自動學會避免這種無意義的循環?
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
一枝独秀
一枝独秀
帖子: 5210
注册时间: 2022年 11月 18日 17:36
为圈友点赞: 175 次
被圈友点赞: 204 次
联系:

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

帖子 ejsoon »

寫全新混亂時鐘js程式:

代码: 全选

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

2.用「const doc = obj.contentDocument;」拿到object遊戲的內容,它將是一個svg,下面將稱其為「cclok」。cclok內部的.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的正位,輪到甲方行動。

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

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

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

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

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

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

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

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

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

當一個棋子被吃時,它首先瞬間變透明度為0.36,之後隨機選擇一個y值,直移到手上區域。移到之後透明度變回1。

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

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

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

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

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

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

10.當一次行動開始時,js將控制反光面的opacity從0到1再到0,用時0.36秒。

11.當遊戲結束時,「重新開始」按鈕顯示。如果是cclok的ckvalue屬性值為空的情況,則「隨機局面」按鈕將會出現。每次點擊「隨機局面」按鈕,分針都會旋轉360度,用時1s。

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」表示乙方獲勝。

代码: 全选

<svg id="etmain" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg" width="480" height="480"> <defs mode="1" rotatestep="1" rotatestart="0" stepvalue="180" snapto="0" displaywait="1">   <g id="tile0" frontfill="#700" backfill="#2691c0"><path display="none" d="M 21,21 L -22,22 L -23,-23 Z"></path>

<circle r="24" cx="0" cy="0" fill="url(#lightBlueGrad)" class="bcenter0"></circle>

<text x="-7" y="18" stroke="none" fill="#1B5E20" font-size="60" font-family="Jena Gotisch">1</text>

</g><g id="tile1" frontfill="#700" backfill="#2691c0"><path display="none" d="M 21,21 L -22,22 L -23,-23 Z"></path>

<circle r="24" cx="0" cy="0" fill="url(#lightBlueGrad)" class="bcenter0"></circle>

<text x="-10" y="18" stroke="none" fill="#1B5E20" font-size="60" font-family="Jena Gotisch">2</text>

</g><g id="tile2" frontfill="#700" backfill="#2691c0"><path display="none" d="M 21,21 L -22,22 L -23,-23 Z"></path>

<circle r="24" cx="0" cy="0" fill="url(#lightBlueGrad)" class="bcenter0"></circle>

<text x="-10" y="18" stroke="none" fill="#1B5E20" font-size="60" font-family="Jena Gotisch">3</text>

</g><g id="tile3" frontfill="#700" backfill="#2691c0"><path display="none" d="M 21,21 L -22,22 L -23,-23 Z"></path>

<circle r="24" cx="0" cy="0" fill="url(#lightBlueGrad)" class="bcenter0"></circle>

<text x="-10" y="19" stroke="none" fill="#1B5E20" font-size="60" font-family="Jena Gotisch">4</text>

</g><g id="tile4" frontfill="#700" backfill="#2691c0"><path display="none" d="M 21,21 L -22,22 L -23,-23 Z"></path>

<circle r="24" cx="0" cy="0" fill="url(#lightBlueGrad)" class="bcenter0"></circle>

<text x="-10" y="18" stroke="none" fill="#1B5E20" font-size="60" font-family="Jena Gotisch">5</text>

</g><g id="tile5" frontfill="#700" backfill="#2691c0"><path display="none" d="M 21,21 L -22,22 L -23,-23 Z"></path>

<circle r="24" cx="0" cy="0" fill="url(#lightBlueGrad)" class="bcenter0"></circle>

<text x="-10" y="19" stroke="none" fill="#1B5E20" font-size="60" font-family="Jena Gotisch">6</text>

</g><g id="tile6" frontfill="#700" backfill="#2691c0"><path display="none" d="M 21,21 L -22,22 L -23,-23 Z"></path>

<circle r="24" cx="0" cy="0" fill="url(#lightBlueGrad)" class="bcenter0"></circle>

<text x="-10" y="19" stroke="none" fill="#1B5E20" font-size="60" font-family="Jena Gotisch">7</text>

</g><g id="tile7" frontfill="#700" backfill="#2691c0"><path display="none" d="M 21,21 L -22,22 L -23,-23 Z"></path>

<circle r="24" cx="0" cy="0" fill="url(#lightBlueGrad)" class="bcenter0"></circle>

<text x="-10" y="19" stroke="none" fill="#1B5E20" font-size="60" font-family="Jena Gotisch">8</text>

</g><g id="tile8" frontfill="#700" backfill="#2691c0"><path display="none" d="M 21,21 L -22,22 L -23,-23 Z"></path>

<circle r="24" cx="0" cy="0" fill="url(#lightBlueGrad)" class="bcenter0"></circle>

<text x="-9" y="18" stroke="none" fill="#1B5E20" font-size="60" font-family="Jena Gotisch">9</text>

</g><g id="tile9" frontfill="#700" backfill="#2691c0"><path display="none" d="M 21,21 L -22,22 L -23,-23 Z"></path>

<circle r="24" cx="0" cy="0" fill="url(#lightBlueGrad)" class="bcenter0"></circle>

<text x="-17" y="18" stroke="none" fill="#1B5E20" font-size="60" font-family="Jena Gotisch">10</text>

</g><g id="tile10" frontfill="#700" backfill="#2691c0"><path display="none" d="M 21,21 L -22,22 L -23,-23 Z"></path>

<circle r="24" cx="0" cy="0" fill="url(#lightBlueGrad)" class="bcenter0"></circle>

<text x="-16" y="19" stroke="none" fill="#1B5E20" font-size="60" font-family="Jena Gotisch">11</text>

</g><g id="tile11" frontfill="#700" backfill="#2691c0"><path display="none" d="M 21,21 L -22,22 L -23,-23 Z"></path>

<circle r="24" cx="0" cy="0" fill="url(#lightBlueGrad)" class="bcenter0"></circle>

<text x="-17" y="19" stroke="none" fill="#1B5E20" font-size="60" font-family="Jena Gotisch">12</text>

</g></defs> <g class="etdrop" stroke="#777" stroke-width="2" stroke-linejoin="round" transform="translate(240,240) scale(1,1)"><g class="etboard">
<path d="M -240,-240 L -240,240 L 240,240 L 240,-240 Z" stroke="none" fill="#E0E0E0" class="cclokbg"></path>
<circle r="180" cx="0" cy="0" fill="#e7e7e7" stroke="purple" stroke-width="7" class="bcenter0"></circle>
<circle r="172" cx="0" cy="0" fill="#decfaa" stroke="purple" stroke-width="2" class="bcenter0"></circle>

<g id="canclearea">
  <circle r="72" cx="0" cy="0" fill="#decfaa" stroke="none"></circle>
  <circle id="ckgleam" r="72" cx="0" cy="0" fill="#DCEDC8" opacity="0" stroke="none"></circle>
  <path transform="rotate(0) scale(1.5) translate(-1.63,-65.4)" stroke-linejoin="round" stroke="#000000" stroke-width="0" fill="#e43" d="M 1.68,-9.55 1.71,-10 c 0,0 0.01,0.05 0.03,0.14 C 1.76,-9.94 1.78,-10 1.78,-10 l 0.03,0.44 c 0.47,2.23 2.78,12.20 6.69,13.96 0,0 -2.05,0.43 -5.16,4.38 1.85,-1.32 3.68,-1.71 4.30,-1.80 0.39,-0.05 0.39,0.04 0.61,0.37 0,0 -0.07,0.33 -0.07,0.34 -0.15,0.22 -0.19,0.18 -0.44,0.26 -0.86,0.28 -3.27,1.31 -4.74,3.87 v 0.55 c 1.02,-0.61 2.01,-0.86 2.62,-0.94 0.01,0 0.14,-0.01 0.14,-0.01 0.12,0.02 0.25,0.01 0.35,0.08 0.00,0.00 0.19,0.31 0.19,0.31 -0.02,0.12 -0.01,0.25 -0.07,0.35 -0.01,0.03 -0.07,0.11 -0.39,0.23 -0.88,0.33 -2.12,1.13 -2.85,2.41 l -0.06,21.21 c 0.79,0.43 1.32,1.28 1.32,2.25 0,1.01 -0.58,1.89 -1.44,2.30 0.01,0.08 0.03,0.17 0.03,0.27 -0.07,4.24 3.26,8.87 3.39,9.05 l 0.00,0.00 c 0.28,0.38 0.28,0.90 0,1.29 0,0 -0.89,1.23 -1.76,3.02 -0.72,1.47 -1.44,3.33 -1.59,5.19 2.77,0.56 4.86,3.02 4.86,5.96 0,3.35 -2.72,6.08 -6.08,6.08 -3.35,0 -6.08,-2.72 -6.08,-6.08 0,-2.97 2.13,-5.45 4.95,-5.98 -0.32,-3.98 -3.24,-8.02 -3.36,-8.19 l -0.00,-0.00 c -0.28,-0.38 -0.28,-0.90 0,-1.29 l 0.00,-0.00 c 0.12,-0.17 3.47,-4.81 3.39,-9.05 0,-0.09 0.00,-0.18 0.03,-0.27 -0.85,-0.41 -1.44,-1.29 -1.44,-2.30 0,-0.96 0.53,-1.81 1.32,-2.25 l 0.06,-21.21 c -0.09,-0.17 -0.22,-0.36 -0.36,-0.55 -1.10,-1.44 -2.52,-1.87 -2.57,-1.90 -0.10,-0.06 -0.23,-0.10 -0.30,-0.19 -0.00,-0.00 -0.08,-0.35 -0.07,-0.36 0.06,-0.10 0.09,-0.23 0.19,-0.30 0.19,-0.13 0.21,-0.09 0.44,-0.07 0.73,0.08 1.74,0.39 2.68,0.95 v -0.55 c -0.46,-0.81 -1.62,-2.51 -4.04,-3.60 -0.19,-0.08 -0.50,-0.21 -0.76,-0.29 -0.22,-0.07 -0.25,-0.05 -0.38,-0.24 -0.00,-0.00 -0.07,-0.34 -0.07,-0.34 0.06,-0.09 0.10,-0.22 0.19,-0.29 0.19,-0.13 0.20,-0.10 0.43,-0.07 0.95,0.14 2.42,0.59 3.77,1.44 0.16,0.10 0.33,0.21 0.51,0.34 -3.11,-3.94 -5.17,-4.38 -5.17,-4.38 3.91,-1.75 6.22,-11.73 6.69,-13.96 z m 2.32,60.15 c -0.56,-0.87 -1.56,-2.57 -2.33,-4.57 -0.76,2.00 -1.77,3.70 -2.33,4.58 0.56,0.87 1.55,2.56 2.32,4.56 0.25,-0.65 0.52,-1.28 0.80,-1.85 0.57,-1.16 1.15,-2.12 1.53,-2.71 z m -2.38,11.78 c -1.67,0 -3.04,1.36 -3.04,3.04 0,1.67 1.36,3.04 3.04,3.04 1.67,0 3.04,-1.36 3.04,-3.04 0,-1.67 -1.36,-3.04 -3.04,-3.04 z" fill-rule="evenodd" id="path1029"></path>
  <path transform="rotate(-90) scale(1.2) translate(-6.17,-54.816)" stroke-linejoin="round" stroke="#000000" stroke-width="0" fill="#2ad" d="m 2.23,48.31 c -0.60,-1.23 -2.04,-3.40 -5.12,-3.94 -0.71,-0.12 -1.22,-0.10 -1.22,-0.10 -0.19,-0.05 -0.41,-0.07 -0.58,-0.19 -0.00,-0.00 -0.30,-0.47 -0.29,-0.48 0.17,-0.62 0.10,-0.78 0.75,-0.89 1.86,-0.31 6.15,-0.16 9.12,4.30 l 0.01,-1.07 c 0,0 -0.41,-4.26 -5.38,-6.53 0,0 3.81,-2.24 5.28,-6.70 -1.03,-0.50 -1.75,-1.57 -1.75,-2.81 0,-3.17 1.74,-12.86 2.13,-15.19 0.04,-0.32 0.07,-0.52 0.07,-0.55 0.00,-0.19 -0.01,-0.37 -0.03,-0.51 -0.03,-0.23 -0.09,-0.41 -0.12,-0.50 -0.00,-0.01 -0.00,-0.02 -0.01,-0.03 -0.01,-0.05 -0.81,-2.69 -2.25,-4.98 -0.84,-1.34 -1.88,-2.61 -3.20,-2.87 -0.37,-0.07 -0.80,0.02 -1.19,-0.03 -0.50,-0.07 -0.91,-0.47 -0.98,-0.98 -0.01,-0.11 -0.01,-0.23 0,-0.34 0.07,-0.50 0.47,-0.91 0.98,-0.98 0.39,-0.05 0.82,0.03 1.19,-0.03 1.32,-0.26 2.35,-1.53 3.20,-2.87 1.44,-2.28 2.24,-4.92 2.25,-4.98 0.64,-3.00 1.12,-4.94 1.12,-4.94 0,0 0.48,1.94 1.12,4.94 0.01,0.05 0.81,2.69 2.25,4.98 0.84,1.34 1.88,2.61 3.20,2.87 0.37,0.07 0.80,-0.02 1.19,0.03 0.50,0.07 0.91,0.47 0.98,0.98 0.01,0.11 0.01,0.23 0,0.34 -0.07,0.50 -0.47,0.91 -0.98,0.98 -0.39,0.05 -0.82,-0.03 -1.19,0.03 -1.32,0.26 -2.35,1.53 -3.20,2.87 -1.44,2.28 -2.24,4.92 -2.25,4.98 -0.00,0.01 -0.00,0.02 -0.01,0.03 -0.02,0.08 -0.08,0.26 -0.12,0.50 -0.02,0.14 -0.03,0.31 -0.03,0.51 0,0.03 0.02,0.23 0.07,0.55 0.39,2.33 2.13,12.02 2.13,15.19 0,1.23 -0.71,2.30 -1.75,2.81 1.46,4.45 5.28,6.70 5.28,6.70 -4.96,2.27 -5.38,6.53 -5.38,6.53 l 0.01,1.07 c 2.28,-3.44 5.22,-4.10 6.42,-4.30 1.48,-0.23 2.75,-0.00 2.77,0.00 0.17,0.09 0.39,0.16 0.51,0.32 0.00,0.00 0.15,0.56 0.15,0.56 -0.09,0.15 -0.14,0.35 -0.29,0.47 -0.37,0.28 -0.41,0.17 -0.88,0.19 -1.64,0.06 -4.47,0.81 -6.05,4.05 2.19,1.33 3.65,3.74 3.65,6.49 0,3.39 -2.22,6.26 -5.29,7.24 -1.46,0.53 -3.00,0.49 -4.59,0 -3.07,-0.97 -5.29,-3.85 -5.29,-7.24 0,-2.75 1.46,-5.16 3.66,-6.49 z m 3.93,2.69 c -2.09,0 -3.80,1.70 -3.80,3.80 0,2.09 1.70,3.80 3.80,3.80 2.09,0 3.80,-1.70 3.80,-3.80 0,-2.09 -1.70,-3.80 -3.80,-3.80 z m 0,-52.46 c -0.37,0.83 -0.84,1.76 -1.39,2.63 -0.69,1.09 -1.51,2.09 -2.45,2.82 0.93,0.72 1.76,1.73 2.45,2.82 0.55,0.87 1.02,1.80 1.39,2.63 0.37,-0.83 0.84,-1.76 1.39,-2.63 0.69,-1.09 1.51,-2.09 2.45,-2.82 C 9.08,3.28 8.26,2.28 7.57,1.19 c -0.55,-0.87 -1.02,-1.80 -1.39,-2.63 z" fill-rule="evenodd" id="path900"></path>
  <circle r="5" cx="0" cy="0" fill="purple" stroke="none" stroke-width="0" class="bcenter"></circle>
</g>

<circle r="6" cx="-72" cy="-124.708" fill="purple" stroke="none" stroke-width="0" class="bcenter"></circle><circle r="6" cx="-124.708" cy="-72" fill="purple" stroke="none" stroke-width="0" class="bcenter"></circle><circle r="6" cx="-144" cy="0" fill="purple" stroke="none" stroke-width="0" class="bcenter"></circle><circle r="6" cx="-124.708" cy="72" fill="purple" stroke="none" stroke-width="0" class="bcenter"></circle><circle r="6" cx="-72" cy="124.708" fill="purple" stroke="none" stroke-width="0" class="bcenter"></circle><circle r="6" cx="0" cy="144" fill="purple" stroke="none" stroke-width="0" class="bcenter"></circle><circle r="6" cx="72" cy="124.708" fill="purple" stroke="none" stroke-width="0" class="bcenter"></circle><circle r="6" cx="124.708" cy="72" fill="purple" stroke="none" stroke-width="0" class="bcenter"></circle><circle r="6" cx="124.708" cy="-72" fill="purple" stroke="none" stroke-width="0" class="bcenter"></circle><circle r="6" cx="144" cy="0" fill="purple" stroke="none" stroke-width="0" class="bcenter"></circle><circle r="6" cx="72" cy="-124.708" fill="purple" stroke="none" stroke-width="0" class="bcenter"></circle><circle r="6" cx="0" cy="-144" fill="purple" stroke="none" stroke-width="0" class="bcenter"></circle>

 <style>
@font-face {
    font-family: 'Jena Gotisch';
    src: url('data:font/woff2;charset=utf-8;base64,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') format('woff2');
    font-weight: normal;
    font-style: normal;
    font-display: swap;
}
</style>

<defs>
<radialGradient id="lightBlueGrad" cx="90%" cy="10%">
  <stop offset="0%" style="stop-color: rgb(255,255,255);"></stop>
  <stop offset="100%" style="stop-color: #add8e6;"></stop>
</radialGradient>
</defs>

</g>
<use href="#tile6" fill="#700" stroke="#777" transform="translate(-72,124.708) scale(1,1) rotate(0)" class="tiledropped"></use><use href="#tile8" fill="#700" stroke="#777" transform="translate(-144,0) scale(1,1) rotate(0)" class="tiledropped"></use><use href="#tile9" fill="#700" stroke="#777" transform="translate(-124.708,-72) scale(1,1) rotate(0)" class="tiledropped"></use><use href="#tile10" fill="#700" stroke="#777" transform="translate(-72,-124.708) scale(1,1) rotate(0)" class="tiledropped"></use><use href="#tile11" fill="#700" stroke="#777" transform="translate(0,-144) scale(1,1) rotate(0)" class="tiledropped"></use><use href="#tile0" fill="#700" stroke="#777" transform="translate(72,-124.708) scale(1,1) rotate(0)" class="tiledropped"></use><use href="#tile1" fill="#700" transform="translate(124.708,-72) scale(1,1) rotate(0)" class="tiledropped" stroke="#777"></use><use href="#tile2" fill="#700" stroke="#777" transform="translate(144,0) scale(1,1) rotate(0)" class="tiledropped"></use><use href="#tile3" fill="#700" stroke="#777" transform="translate(124.708,72) scale(1,1) rotate(0)" class="tiledropped"></use><use href="#tile4" fill="#700" stroke="#777" transform="translate(72,124.708) scale(1,1) rotate(0)" class="tiledropped"></use><use href="#tile5" fill="#700" stroke="#777" transform="translate(0,144) scale(1,1) rotate(0)" class="tiledropped"></use><use href="#tile7" fill="#700" stroke="#777" transform="translate(-124.708,72) scale(1,1) rotate(0)" class="tilesnapped tiledropped"></use>
</g>
</svg>
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
一枝独秀
一枝独秀
帖子: 5210
注册时间: 2022年 11月 18日 17:36
为圈友点赞: 175 次
被圈友点赞: 204 次
联系:

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

帖子 ejsoon »

稍改:

代码: 全选

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

2.用「const doc = obj.contentDocument;」拿到object的內容,它將是一個svg,下面將稱其為「cclok」。cclok內部的.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。

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

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

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

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

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

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

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

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
一枝独秀
一枝独秀
帖子: 5210
注册时间: 2022年 11月 18日 17:36
为圈友点赞: 175 次
被圈友点赞: 204 次
联系:

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

帖子 ejsoon »

gemini pro:

代码: 全选

這是一個非常有趣且邏輯嚴密的遊戲設計!為了解決這個需求,我們需要為每一個 `<object class="cclok_main">` 建立獨立的遊戲實例(Game Instance),以確保多個遊戲在同一頁面上不會互相干涉。

以下是完整的 JavaScript 實作程式碼。程式碼採用物件導向(Class)的架構,將每個時鐘的邏輯封裝起來,並嚴格按照您提出的 12 點規則進行撰寫。

### 核心功能實作重點:
1. **獨立作用域**:透過 `ChaosClock` 類別,確保每個 object 互相獨立。
2. **SVG DOM 操作**:處理 `contentDocument` 的載入狀態,綁定 `use` 標籤與時針、分針。
3. **動畫控制**:利用 `requestAnimationFrame` 處理精確的圓弧軌跡走子、直線落子與反光面動畫。
4. **遊戲規則判定**:嚴格計算「正位保護」、「吃子判定」、「額外回合」與「勝負條件(共贏/同輸)」。

### JavaScript 程式碼:

```javascript
class ChaosClock {
    constructor(objectEl) {
        this.objectEl = objectEl;
        this.doc = null;
        this.uiContainer = null;
        this.statusOutput = null;
        this.btnStart = null;
        this.btnRandom = null;
        this.btnRestart = null;

        // 遊戲狀態資料
        this.state = 'waiting'; // waiting, playing, ended
        this.turn = 1; // 1: 乙方(後手/雙數), 0: 甲方(先手/單數)
        this.board = new Array(12).fill(null); // 0~11 對應正位 1~12
        this.hands = { 0: [], 1: [] }; // 0: 甲方手牌, 1: 乙方手牌
        this.lastMovedPiece = null; // 對方上一步使用的棋子
        this.consecutiveSkips = 0;
        
        // 棋譜記錄
        this.initialBoardStr = "";
        this.initialPlayer = 1;
        this.moveSequence = [];
        this.currentTurnMoves = []; // 紀錄連續行動 (例如吃子獲額外回合)

        // 動畫狀態
        this.hourRotation = -90;
        this.minuteRotation = 0;
        this.isAnimating = false;

        this.init();
    }

    init() {
        // 確保 SVG 載入完成
        if (this.objectEl.contentDocument && this.objectEl.contentDocument.readyState === 'complete') {
            this.setupGame();
        } else {
            this.objectEl.addEventListener('load', () => this.setupGame());
        }
    }

    setupGame() {
        this.doc = this.objectEl.contentDocument;
        if (!this.doc) return;

        this.createUI();
        this.parseInitialState();
        this.updatePointers(true);
    }

    createUI() {
        this.uiContainer = document.createElement('div');
        this.uiContainer.style.textAlign = 'center';
        this.uiContainer.style.marginTop = '10px';
        this.uiContainer.style.fontFamily = 'sans-serif';

        this.statusOutput = document.createElement('div');
        this.statusOutput.style.marginBottom = '10px';
        this.statusOutput.style.fontWeight = 'bold';
        this.statusOutput.textContent = '等待開始';

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

        this.btnRandom = document.createElement('button');
        this.btnRandom.textContent = '隨機局面';
        this.btnRandom.style.display = 'none';
        this.btnRandom.onclick = () => this.generateRandomBoard();

        this.btnRestart = document.createElement('button');
        this.btnRestart.textContent = '重新開始';
        this.btnRestart.style.display = 'none';
        this.btnRestart.onclick = () => this.restartGame();

        this.uiContainer.appendChild(this.statusOutput);
        this.uiContainer.appendChild(this.btnStart);
        this.uiContainer.appendChild(this.btnRandom);
        this.uiContainer.appendChild(this.btnRestart);
        
        this.objectEl.parentNode.insertBefore(this.uiContainer, this.objectEl.nextSibling);
    }

    parseInitialState() {
        const svgEl = this.doc.querySelector('svg');
        const ckvalue = svgEl.getAttribute('ckvalue');

        if (ckvalue && ckvalue.trim() !== "") {
            const vals = ckvalue.split(',').map(Number);
            this.setBoard(vals);
        } else {
            this.btnRandom.style.display = 'inline-block';
            this.setBoard(this.getRandomDerangement());
        }
    }

    getRandomDerangement() {
        let arr = [1,2,3,4,5,6,7,8,9,10,11,12];
        let valid = false;
        while (!valid) {
            for (let i = arr.length - 1; i > 0; i--) {
                const j = Math.floor(Math.random() * (i + 1));
                [arr[i], arr[j]] = [arr[j], arr[i]];
            }
            valid = true;
            for (let i = 0; i < 12; i++) {
                if (arr[i] === i + 1) { valid = false; break; }
            }
        }
        return arr;
    }

    setBoard(vals) {
        this.board = [...vals];
        this.initialBoardStr = vals.join(',');
        this.hands = { 0: [], 1: [] };
        
        for (let i = 0; i < 12; i++) {
            if (this.board[i] !== 0) {
                this.renderPiecePosition(this.board[i], i);
            }
        }
        this.bindEvents();
    }

    getPieceEl(n) {
        // n 是 1~12,尋找 href 結尾為 tile{n-1} 的 use 標籤
        const uses = Array.from(this.doc.querySelectorAll('.etdrop > use'));
        return uses.find(el => {
            const href = el.getAttribute('href') || el.getAttribute('xlink:href');
            return href && href.endsWith(`tile${n-1}`);
        });
    }

    getCoordsForPosition(index) {
        // index 0 對應正位1 (30度), index 11 對應正位12 (360度)
        const angleDeg = (index + 1) * 30;
        const angleRad = angleDeg * Math.PI / 180;
        return {
            x: 144 * Math.sin(angleRad),
            y: -144 * Math.cos(angleRad)
        };
    }

    renderPiecePosition(pieceValue, boardIndex) {
        const el = this.getPieceEl(pieceValue);
        if (!el) return;
        const coords = this.getCoordsForPosition(boardIndex);
        el.setAttribute('transform', `translate(${coords.x}, ${coords.y})`);
        el.style.opacity = 1;
    }

    bindEvents() {
        for (let i = 1; i <= 12; i++) {
            const el = this.getPieceEl(i);
            if (el) {
                el.style.cursor = 'pointer';
                el.onclick = () => this.handlePieceClick(i);
            }
        }
        const cancelArea = this.doc.querySelector('#canclearea');
        if (cancelArea) {
            cancelArea.style.cursor = 'pointer';
            cancelArea.onclick = () => this.handleCancelClick();
        }
    }

    startGame() {
        this.state = 'playing';
        this.turn = 1; // 乙方先
        this.initialPlayer = 1;
        this.btnStart.style.display = 'none';
        this.btnRandom.style.display = 'none';
        this.statusOutput.textContent = '遊戲開始,輪到乙方';
        this.hourRotation = 90; // 乙方朝右
        this.updatePointers();
    }

    async handlePieceClick(pieceValue) {
        if (this.state !== 'playing' || this.isAnimating) return;
        
        // 檢查是否為上一步對方用過的棋子
        if (pieceValue === this.lastMovedPiece) return;

        const boardIndex = this.board.indexOf(pieceValue);
        const owner = pieceValue % 2 === 0 ? 1 : 0; // 1:乙方雙數, 0:甲方單數

        if (boardIndex !== -1) {
            // 在鐘面上 -> 走子
            // 處於正位不可走子
            if (boardIndex === pieceValue - 1) return;
            await this.executeMove(pieceValue, boardIndex);
        } else {
            // 在手上 -> 落子 (只能點擊己方手上的)
            if (owner !== this.turn) return;
            await this.executePlace(pieceValue);
        }
    }

    async handleCancelClick() {
        if (this.state !== 'playing' || this.isAnimating) return;
        this.consecutiveSkips++;
        this.recordMoveStr('放棄');
        await this.flashGleam();
        this.switchTurn();
    }

    async executeMove(pieceValue, startIndex) {
        const steps = pieceValue;
        const targetIndex = (startIndex + steps) % 12;
        const targetOccupant = this.board[targetIndex];

        // 規則:不可吃掉處於正位的棋子
        if (targetOccupant !== null && targetOccupant === targetIndex + 1) return;

        this.isAnimating = true;
        this.consecutiveSkips = 0;
        this.lastMovedPiece = pieceValue;
        this.recordMoveStr(pieceValue.toString());
        await this.flashGleam();

        // 移至最底部 (Z-index 提升)
        const el = this.getPieceEl(pieceValue);
        el.parentNode.appendChild(el);

        // 動畫:走子
        await this.animateArc(el, startIndex, targetIndex);
        this.board[startIndex] = null;

        // 處理吃子
        if (targetOccupant !== null) {
            await this.capturePiece(targetOccupant);
        }

        this.board[targetIndex] = pieceValue;
        this.isAnimating = false;
        this.checkWinOrSwitch();
    }

    async executePlace(pieceValue) {
        const targetIndex = pieceValue - 1; // 正位
        const targetOccupant = this.board[targetIndex];

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

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

        // 動畫:落子 (直線)
        const coords = this.getCoordsForPosition(targetIndex);
        await this.animateLinear(el, coords.x, coords.y);

        // 移出手牌
        this.hands[this.turn] = this.hands[this.turn].filter(v => v !== pieceValue);

        let extraTurn = false;
        if (targetOccupant !== null) {
            const occupantOwner = targetOccupant % 2 === 0 ? 1 : 0;
            if (occupantOwner !== this.turn) extraTurn = true; // 吃到對方棋子
            await this.capturePiece(targetOccupant);
        }

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

        if (extraTurn) {
            this.hourRotation += 360;
            this.updatePointers();
            this.statusOutput.textContent += ' (額外回合)';
            // 保持當前 turn
            this.checkWinConditions();
        } else {
            this.checkWinOrSwitch();
        }
    }

    async capturePiece(pieceValue) {
        const owner = pieceValue % 2 === 0 ? 1 : 0;
        const el = this.getPieceEl(pieceValue);
        
        el.style.opacity = 0.36;
        
        // 決定手牌 Y 座標 (避免重疊與指定規則)
        const ySlots = [-200, -120, -40, 40, 120, 200];
        // 簡單排序:保證小的數字在上方(負Y)
        this.hands[owner].push(pieceValue);
        this.hands[owner].sort((a,b) => a - b);
        const handIndex = this.hands[owner].indexOf(pieceValue);
        const finalY = ySlots[handIndex % ySlots.length];
        const finalX = owner === 1 ? 208 : -208; // 乙方 208, 甲方 -208

        await this.animateLinear(el, finalX, finalY);
        el.style.opacity = 1;
    }

    checkWinOrSwitch() {
        if (!this.checkWinConditions()) {
            this.switchTurn();
        }
    }

    checkWinConditions() {
        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) {
            this.endGame(4); // 乙方贏
            return true;
        }

        if (this.turn === 0 && oddsCorrect) {
            // 檢查乙方下一步是否能全部歸位
            // 若乙方所有剩下的雙數都在手上,且乙方接下來只要一直落子就能贏,但因為這裡只判定「緊接著下一步」
            // 根據題意簡化:判定乙方是否只差一步(只剩一個未歸位)且能落子
            let unplacedEvens = [2,4,6,8,10,12].filter(v => this.board[v-1] !== v);
            if (unplacedEvens.length === 0 || (unplacedEvens.length === 1 && this.hands[1].includes(unplacedEvens[0]))) {
                this.endGame(3); // 共贏
            } else {
                this.endGame(1); // 甲方贏 (乙方輸)
            }
            return true;
        }
        return false;
    }

    switchTurn() {
        this.turn = this.turn === 1 ? 0 : 1;
        this.hourRotation = this.turn === 1 ? 90 + Math.floor(this.hourRotation/360)*360 : 270 + Math.floor(this.hourRotation/360)*360;
        this.updatePointers();
        this.statusOutput.textContent = `輪到${this.turn === 1 ? '乙方' : '甲方'}`;
        
        // 結算上一回合的行動記錄
        if (this.currentTurnMoves.length > 0) {
            this.moveSequence.push(this.currentTurnMoves.join('-'));
            this.currentTurnMoves = [];
        }
    }

    recordMoveStr(move) {
        this.currentTurnMoves.push(move);
    }

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

        this.minuteRotation += 180;
        if (resultCode === 4) { // 乙方贏 (初始為乙)
            this.statusOutput.textContent = '遊戲結束:乙方獲勝';
            this.hourRotation = 90; 
        } else if (resultCode === 1) { // 甲方贏
            this.statusOutput.textContent = '遊戲結束:甲方獲勝';
            this.hourRotation = 270;
        } else if (resultCode === 3) { // 共贏
            this.statusOutput.textContent = '遊戲結束:甲乙共贏';
            this.hourRotation = 360;
        } else { // 同輸
            this.statusOutput.textContent = '遊戲結束:甲乙同輸';
            this.hourRotation = 180;
        }
        
        this.updatePointers();
        this.btnRestart.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);
    }

    async flashGleam() {
        const gleam = this.doc.querySelector('#ckgleam');
        if (!gleam) return;
        return new Promise(resolve => {
            gleam.style.transition = 'opacity 0.18s linear';
            gleam.style.opacity = 1;
            setTimeout(() => {
                gleam.style.opacity = 0;
                setTimeout(resolve, 180);
            }, 180);
        });
    }

    async animateArc(el, startIdx, endIdx) {
        return new Promise(resolve => {
            let startAngle = (startIdx + 1) * 30;
            let endAngle = (endIdx + 1) * 30;
            if (endAngle <= startAngle) endAngle += 360; // 順時針轉
            
            const duration = 1000;
            const startTime = performance.now();

            const step = (currentTime) => {
                const elapsed = currentTime - startTime;
                const progress = Math.min(elapsed / duration, 1);
                const currentAngle = startAngle + (endAngle - startAngle) * progress;
                const rad = currentAngle * Math.PI / 180;
                
                const x = 144 * Math.sin(rad);
                const y = -144 * Math.cos(rad);
                el.setAttribute('transform', `translate(${x}, ${y})`);

                if (progress < 1) {
                    requestAnimationFrame(step);
                } else {
                    resolve();
                }
            };
            requestAnimationFrame(step);
        });
    }

    async animateLinear(el, targetX, targetY) {
        return new Promise(resolve => {
            el.style.transition = 'transform 1s linear';
            el.setAttribute('transform', `translate(${targetX}, ${targetY})`);
            setTimeout(() => {
                el.style.transition = '';
                resolve();
            }, 1000);
        });
    }

    updatePointers(isInit = false) {
        const hourHand = this.doc.querySelector('#path900');
        const minHand = this.doc.querySelector('#path1029');
        if (!hourHand || !minHand) return;

        if (!isInit) {
            hourHand.style.transition = 'transform 1s linear';
            minHand.style.transition = 'transform 1s linear';
        }
        
        // 設定以 (0,0) 為旋轉中心 (需配合 SVG 結構)
        hourHand.style.transformOrigin = '0px 0px';
        minHand.style.transformOrigin = '0px 0px';
        
        hourHand.style.transform = `rotate(${this.hourRotation}deg)`;
        minHand.style.transform = `rotate(${this.minuteRotation}deg)`;
    }

    async generateRandomBoard() {
        this.isAnimating = true;
        
        // 1. 所有棋子消失
        for (let i = 1; i <= 12; i++) {
            const el = this.getPieceEl(i);
            if (el) {
                el.style.transition = 'opacity 0.8s linear';
                el.style.opacity = 0;
            }
        }
        await new Promise(r => setTimeout(r, 800));

        // 2. 分針旋轉 360 度 (1.2s),同時每 0.1s 顯示一個棋子
        this.minuteRotation += 360;
        const minHand = this.doc.querySelector('#path1029');
        minHand.style.transition = 'transform 1.2s linear';
        minHand.style.transform = `rotate(${this.minuteRotation}deg)`;

        const newBoard = this.getRandomDerangement();
        this.board = newBoard;
        this.initialBoardStr = newBoard.join(',');

        for (let i = 0; i < 12; i++) {
            setTimeout(() => {
                const pieceVal = newBoard[i];
                const el = this.getPieceEl(pieceVal);
                if (el) {
                    const coords = this.getCoordsForPosition(i);
                    el.style.transition = 'none'; // 重置位置不帶動畫
                    el.setAttribute('transform', `translate(${coords.x}, ${coords.y})`);
                    el.style.transition = 'opacity 0.1s linear';
                    el.style.opacity = 1;
                }
            }, i * 100);
        }

        await new Promise(r => setTimeout(r, 1200));
        this.isAnimating = false;
        this.bindEvents();
    }

    restartGame() {
        this.btnRestart.style.display = 'none';
        this.statusOutput.textContent = '等待開始';
        this.minuteRotation += 180; // 朝上
        this.hourRotation = -90;
        this.updatePointers();
        
        this.state = 'waiting';
        this.moveSequence = [];
        this.currentTurnMoves = [];
        this.consecutiveSkips = 0;
        this.lastMovedPiece = null;
        
        const ckvalue = this.doc.querySelector('svg').getAttribute('ckvalue');
        if (!ckvalue || ckvalue.trim() === "") {
            this.btnRandom.style.display = 'inline-block';
        }
        this.btnStart.style.display = 'inline-block';
    }
}

// 初始化頁面上所有的 ChaosClock
document.addEventListener('DOMContentLoaded', () => {
    const clockObjects = document.querySelectorAll('object.cclok_main');
    clockObjects.forEach(obj => {
        new ChaosClock(obj);
    });
});
```

### 部署與使用說明
1. 將上述程式碼儲存為 `.js` 檔案,或直接放入 HTML 的 `<script>` 標籤中(放在 `<body>` 的最底端)。
2. 確保 HTML 中的 `<object>` 標籤載入的是與本腳本同源(Same-Origin)的 SVG 檔案,否則 `contentDocument` 會因為跨域安全限制(CORS)而回傳 `null`。
3. 若您的 SVG 中的棋子圖案使用 `#tile0`~`#tile11`,這段程式碼會透過 CSS 選擇器 `.etdrop > use` 取出 href,精確對應棋子 1 到 12。
4. 遊戲的所有判定與結果會自動寫入陣列,並在 `endGame` 時以規定的「棋譜格式」列印到瀏覽器的開發者控制台(Console)中。
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
一枝独秀
一枝独秀
帖子: 5210
注册时间: 2022年 11月 18日 17:36
为圈友点赞: 175 次
被圈友点赞: 204 次
联系:

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

帖子 ejsoon »

改進:
1.在實現動畫時,元素的transform內是可能已包含了translate、rotate、scale的,應該只操作需要改變的。

2.當落子時也要實現移動動畫。

3.當棋子被吃掉時,所移去的地方不應是已有棋子的地方。

4.僅當ckvalue為空時才會出現「隨機局面」按鈕。

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

6.兩方接連都放棄行動應判同輸。

7.棋子移動、時針轉動、反光面的動畫都應該同時進行。

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


代码: 全选

這裡為您提供 **ChaosClock** 類別中所需要新增與修改的部分。請將這些方法替換或加入到原本的類別程式碼中:

### 1. 新增輔助方法 (控制動畫變形、尋找空位與提示)
請將以下方法加入到 `ChaosClock` 類別中:

```javascript
    // 取得 transform 中特定的變形屬性值
    getTransformValue(el, command) {
        const transform = el.getAttribute('transform') || '';
        const match = transform.match(new RegExp(`${command}\\(([^)]+)\\)`));
        return match ? match[1] : null;
    }

    // 僅更新 transform 中的 translate,保留其他 rotate 或 scale
    updateTransform(el, command, value) {
        let transform = el.getAttribute('transform') || '';
        const regex = new RegExp(`${command}\\([^)]*\\)`, 'g');
        if (regex.test(transform)) {
            transform = transform.replace(regex, `${command}(${value})`);
        } else {
            transform += ` ${command}(${value})`;
        }
        el.setAttribute('transform', transform.trim());
    }

    // 取得手牌區不與現有棋子重疊的 y 座標
    getAvailableHandY(owner) {
        const ySlots = [-200, -120, -40, 40, 120, 200];
        const occupiedYs = this.hands[owner].map(pieceVal => {
            const el = this.getPieceEl(pieceVal);
            const trans = this.getTransformValue(el, 'translate');
            return trans ? parseFloat(trans.split(',')[1]) : null;
        }).filter(y => y !== null);
        
        // 尋找尚未被佔據的 y 槽 (給定 10px 容錯)
        const available = ySlots.find(y => !occupiedYs.some(oy => Math.abs(oy - y) < 10));
        return available !== undefined ? available : ySlots[0]; 
    }

    // 顯示 3 秒鐘的臨時文字提示
    showTempMessage(msg) {
        if (this.statusTimer) clearTimeout(this.statusTimer);
        const originalText = this.statusOutput.dataset.original || this.statusOutput.textContent;
        this.statusOutput.dataset.original = originalText;
        this.statusOutput.textContent = msg;
        this.statusTimer = setTimeout(() => {
            this.statusOutput.textContent = this.statusOutput.dataset.original;
        }, 3000);
    }

    // 更新並記錄常駐狀態文字
    setNormalMessage(msg) {
        this.statusOutput.textContent = msg;
        this.statusOutput.dataset.original = msg;
    }
```

### 2. 修改渲染與初始設定方法

```javascript
    parseInitialState() {
        const svgEl = this.doc.querySelector('svg');
        const ckvalue = svgEl.getAttribute('ckvalue');
        this.isRandomMode = false;

        if (ckvalue && ckvalue.trim() !== "") {
            const vals = ckvalue.split(',').map(Number);
            this.setBoard(vals);
        } else {
            this.isRandomMode = true; // 記錄是否為空值
            this.btnRandom.style.display = 'inline-block';
            this.setBoard(this.getRandomDerangement());
        }
    }

    renderPiecePosition(pieceValue, boardIndex) {
        const el = this.getPieceEl(pieceValue);
        if (!el) return;
        const coords = this.getCoordsForPosition(boardIndex);
        this.updateTransform(el, 'translate', `${coords.x}, ${coords.y}`);
        el.style.opacity = 1;
    }
```

### 3. 修改點擊事件判斷與取消行動

```javascript
    async handlePieceClick(pieceValue) {
        if (this.state !== 'playing' || this.isAnimating) return;
        if (pieceValue === this.lastMovedPiece) return;

        const boardIndex = this.board.indexOf(pieceValue);
        const owner = pieceValue % 2 === 0 ? 1 : 0;

        if (boardIndex !== -1) {
            // 在鐘面上 -> 走子
            if (boardIndex === pieceValue - 1) {
                this.showTempMessage('處於正位的棋子不可移動');
                return;
            }
            await this.executeMove(pieceValue, boardIndex);
        } else {
            // 在手上 -> 落子
            if (owner !== this.turn) {
                this.showTempMessage(this.turn === 1 ? '雙數棋子只能由乙方落子' : '單數棋子只能由甲方落子');
                return;
            }
            await this.executePlace(pieceValue);
        }
    }

    async handleCancelClick() {
        if (this.state !== 'playing' || this.isAnimating) return;
        this.consecutiveSkips++;
        this.recordMoveStr('放棄');
        
        // 開始時針轉動與反光面動畫
        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;
        this.updatePointers();

        await this.flashGleam();
        
        // 連續兩次放棄判同輸
        if (this.consecutiveSkips >= 2) {
            this.endGame(2);
        } else {
            this.switchTurn(false); // 不需再轉動時針
        }
    }
```

### 4. 修改走子、落子與吃子邏輯 (同時進行動畫)

```javascript
    async executeMove(pieceValue, startIndex) {
        const steps = pieceValue;
        const targetIndex = (startIndex + steps) % 12;
        const targetOccupant = this.board[targetIndex];

        if (targetOccupant !== null && targetOccupant === targetIndex + 1) {
            this.showTempMessage('處於正位的棋子不可被吃');
            return;
        }

        this.isAnimating = true;
        this.consecutiveSkips = 0;
        this.lastMovedPiece = pieceValue;
        this.recordMoveStr(pieceValue.toString());
        
        // 提前轉動時針 (與其他動畫同時)
        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;
        this.updatePointers();

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

        // 動畫同時進行
        await Promise.all([
            this.flashGleam(),
            this.animateArc(el, startIndex, targetIndex)
        ]);

        this.board[startIndex] = null;

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

        this.board[targetIndex] = pieceValue;
        this.isAnimating = false;
        this.checkWinOrSwitch();
    }

    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;
        }
        this.updatePointers();

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

        const coords = this.getCoordsForPosition(targetIndex);
        
        await Promise.all([
            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();
        }
    }

    async capturePiece(pieceValue) {
        const owner = pieceValue % 2 === 0 ? 1 : 0;
        const el = this.getPieceEl(pieceValue);
        
        el.style.opacity = 0.36;
        
        const finalY = this.getAvailableHandY(owner);
        const finalX = owner === 1 ? 208 : -208;
        this.hands[owner].push(pieceValue);

        await this.animateLinear(el, finalX, finalY);
        el.style.opacity = 1;
    }
```

### 5. 修改動畫核心方法 (保留其他 transform)

```javascript
    async animateArc(el, startIdx, endIdx) {
        return new Promise(resolve => {
            let startAngle = (startIdx + 1) * 30;
            let endAngle = (endIdx + 1) * 30;
            if (endAngle <= startAngle) endAngle += 360;
            
            const duration = 1000;
            const startTime = performance.now();

            const step = (currentTime) => {
                const elapsed = currentTime - startTime;
                const progress = Math.min(elapsed / duration, 1);
                const currentAngle = startAngle + (endAngle - startAngle) * progress;
                const rad = currentAngle * Math.PI / 180;
                
                const x = 144 * Math.sin(rad);
                const y = -144 * Math.cos(rad);
                this.updateTransform(el, 'translate', `${x}, ${y}`);

                if (progress < 1) requestAnimationFrame(step);
                else resolve();
            };
            requestAnimationFrame(step);
        });
    }

    async animateLinear(el, targetX, targetY) {
        return new Promise(resolve => {
            const startTrans = this.getTransformValue(el, 'translate');
            let startX = 0, startY = 0;
            if (startTrans) {
                const parts = startTrans.split(',').map(v => parseFloat(v));
                startX = parts[0] || 0;
                startY = parts[1] || 0;
            }
            
            const duration = 1000;
            const startTime = performance.now();

            const step = (currentTime) => {
                const elapsed = currentTime - startTime;
                const progress = Math.min(elapsed / duration, 1);
                const x = startX + (targetX - startX) * progress;
                const y = startY + (targetY - startY) * progress;
                this.updateTransform(el, 'translate', `${x}, ${y}`);
                
                if (progress < 1) requestAnimationFrame(step);
                else resolve();
            };
            requestAnimationFrame(step);
        });
    }
```

### 6. 修改遊戲狀態與回合切換

```javascript
    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.updatePointers();
    }

    checkWinOrSwitch() {
        if (!this.checkWinConditions()) {
            this.switchTurn(false); // 動畫時已轉動過時針
        }
    }

    switchTurn(updatePointer = true) {
        this.turn = this.turn === 1 ? 0 : 1;
        if (updatePointer) {
            this.hourRotation = this.turn === 1 ? 90 + Math.floor(this.hourRotation/360)*360 : 270 + Math.floor(this.hourRotation/360)*360;
            this.updatePointers();
        }
        this.setNormalMessage(`輪到${this.turn === 1 ? '乙方' : '甲方'}`);
        
        if (this.currentTurnMoves.length > 0) {
            this.moveSequence.push(this.currentTurnMoves.join('-'));
            this.currentTurnMoves = [];
        }
    }

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

        this.minuteRotation += 180;
        if (resultCode === 4) {
            this.setNormalMessage('遊戲結束:乙方獲勝');
            this.hourRotation = 90; 
        } else if (resultCode === 1) { 
            this.setNormalMessage('遊戲結束:甲方獲勝');
            this.hourRotation = 270;
        } else if (resultCode === 3) {
            this.setNormalMessage('遊戲結束:甲乙共贏');
            this.hourRotation = 360;
        } else {
            this.setNormalMessage('遊戲結束:甲乙同輸');
            this.hourRotation = 180;
        }
        
        this.updatePointers();
        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);
    }
```

### 7. 修復重置時產生隨機局面的位移方式

```javascript
    // 在 generateRandomBoard 函數中的內部迴圈
    // 尋找 `el.setAttribute('transform', ...);` 並替換:
    for (let i = 0; i < 12; i++) {
        setTimeout(() => {
            const pieceVal = newBoard[i];
            const el = this.getPieceEl(pieceVal);
            if (el) {
                const coords = this.getCoordsForPosition(i);
                el.style.transition = 'none'; 
                this.updateTransform(el, 'translate', `${coords.x}, ${coords.y}`);
                el.style.transition = 'opacity 0.1s linear';
                el.style.opacity = 1;
            }
        }, i * 100);
    }
```
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
一枝独秀
一枝独秀
帖子: 5210
注册时间: 2022年 11月 18日 17:36
为圈友点赞: 175 次
被圈友点赞: 204 次
联系:

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

帖子 ejsoon »

在實現動畫時,全部用實時改變svg的transform屬性的方式,不要用改變style的方式。

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

代码: 全选

這裡針對您的需求,將動畫處理邏輯從 CSS `style` 變換改為純粹的 SVG `transform` 屬性即時更新(透過 `requestAnimationFrame` 驅動)。

主要修改集中在 `animateLinear`、`updatePointers` 以及 `flashGleam` 的實現方式:

### 1. 修改線性移動動畫 (animateLinear)
將原本依賴 CSS `transition` 的邏輯改為每幀計算並更新 `transform` 屬性。

```javascript
    async animateLinear(el, targetX, targetY) {
        return new Promise(resolve => {
            const startTrans = this.getTransformValue(el, 'translate');
            let startX = 0, startY = 0;
            if (startTrans) {
                const parts = startTrans.split(startTrans.includes(',') ? ',' : ' ').map(v => parseFloat(v));
                startX = parts[0] || 0;
                startY = parts[1] || 0;
            }
            
            const duration = 1000;
            const startTime = performance.now();

            const step = (currentTime) => {
                const elapsed = currentTime - startTime;
                const progress = Math.min(elapsed / duration, 1);
                
                // 使用線性插值 (Lerp)
                const x = startX + (targetX - startX) * progress;
                const y = startY + (targetY - startY) * progress;
                
                this.updateTransform(el, 'translate', `${x} ${y}`);
                
                if (progress < 1) {
                    requestAnimationFrame(step);
                } else {
                    resolve();
                }
            };
            requestAnimationFrame(step);
        });
    }
```

### 2. 修改指針轉動動畫 (updatePointers)
為了讓指針轉動與棋子同步且不依賴 CSS,我們需要一個專門處理屬性動畫的輔助方法,並更新 `updatePointers`。

```javascript
    // 新增:通用屬性動畫器
    async animateAttribute(el, attrName, startVal, endVal, duration = 1000, isRotation = false) {
        return new Promise(resolve => {
            const startTime = performance.now();
            const step = (currentTime) => {
                const elapsed = currentTime - startTime;
                const progress = Math.min(elapsed / duration, 1);
                const current = startVal + (endVal - startVal) * progress;
                
                if (isRotation) {
                    // 指針旋轉中心固定為 (0,0)
                    el.setAttribute(attrName, `rotate(${current} 0 0)`);
                } else {
                    el.setAttribute(attrName, current);
                }

                if (progress < 1) requestAnimationFrame(step);
                else resolve();
            };
            requestAnimationFrame(step);
        });
    }

    // 修改:指標更新不再設定 style.transition
    updatePointers(isInit = false) {
        const hourHand = this.doc.querySelector('#path900');
        const minHand = this.doc.querySelector('#path1029');
        if (!hourHand || !minHand) return;

        if (isInit) {
            hourHand.setAttribute('transform', `rotate(${this.hourRotation} 0 0)`);
            minHand.setAttribute('transform', `rotate(${this.minuteRotation} 0 0)`);
        } else {
            // 取得當前旋轉角度 (需解析屬性字串)
            const getRotate = (el) => {
                const t = el.getAttribute('transform') || '';
                const m = t.match(/rotate\(([^ ]+)/);
                return m ? parseFloat(m[1]) : 0;
            };
            
            this.animateAttribute(hourHand, 'transform', getRotate(hourHand), this.hourRotation, 1000, true);
            this.animateAttribute(minHand, 'transform', getRotate(minHand), this.minuteRotation, 1000, true);
        }
    }
```

### 3. 修改反光面動畫 (flashGleam)
同樣移除 `style.transition`,改用 `opacity` 屬性動畫。

```javascript
    async flashGleam() {
        const gleam = this.doc.querySelector('#ckgleam');
        if (!gleam) return;
        
        // 0 -> 1 用時 0.18s, 1 -> 0 用時 0.18s
        await this.animateAttribute(gleam, 'opacity', 0, 1, 180);
        await this.animateAttribute(gleam, 'opacity', 1, 0, 180);
    }
```

### 4. 輔助方法微調 (updateTransform)
確保在更新屬性時,格式符合 SVG 標準(座標建議用空格或逗號分隔,且不帶單位)。

```javascript
    updateTransform(el, command, value) {
        let transform = el.getAttribute('transform') || '';
        // 移除可能存在的舊命令
        const regex = new RegExp(`${command}\\([^)]*\\)`, 'g');
        transform = transform.replace(regex, '').trim();
        // 加入新命令
        transform += ` ${command}(${value})`;
        el.setAttribute('transform', transform.trim());
    }
```

### 5. 修改隨機局面重置邏輯
在 `restartGame` 或 `generateRandomBoard` 中,確保重置屬性時直接操作屬性而非 style。

```javascript
    // 在 generateRandomBoard 內
    for (let i = 0; i < 12; i++) {
        setTimeout(() => {
            const pieceVal = newBoard[i];
            const el = this.getPieceEl(pieceVal);
            if (el) {
                const coords = this.getCoordsForPosition(i);
                // 直接操作屬性
                this.updateTransform(el, 'translate', `${coords.x} ${coords.y}`);
                el.setAttribute('opacity', '1');
            }
        }, i * 100);
    }
```
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
一枝独秀
一枝独秀
帖子: 5210
注册时间: 2022年 11月 18日 17:36
为圈友点赞: 175 次
被圈友点赞: 204 次
联系:

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

帖子 ejsoon »

在實現動畫時,全部用實時改變元素屬性的方式,包括transform和opacity,不要用改變style的方式。

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


代码: 全选

這裡為您提供修改後的程式碼片段。我們將放棄 CSS `transition`,改為使用 `requestAnimationFrame` 配合 `setAttribute` 來實時計算並更新屬性值,以確保動畫的精確度與各元件(棋子、指針、反光面)的同步性。

### 1. 核心屬性更新方法
修改 `updateTransform` 並新增 `updateOpacity`,確保完全透過屬性(Attribute)操作:

```javascript
// 僅操作屬性,不觸碰 style
updateTransform(el, command, value) {
    let transform = el.getAttribute('transform') || '';
    const regex = new RegExp(`${command}\\([^)]*\\)`, 'g');
    if (regex.test(transform)) {
        transform = transform.replace(regex, `${command}(${value})`);
    } else {
        transform += ` ${command}(${value})`;
    }
    el.setAttribute('transform', transform.trim());
}

updateOpacity(el, value) {
    el.setAttribute('opacity', value);
}
```

---

### 2. 同步動畫引擎
將所有動畫(走子、平移、透明度、指針旋轉)統一為屬性驅動的 Promise 結構:

```javascript
// 通用的屬性動畫執行器
animateAttribute(duration, frameFunc) {
    return new Promise(resolve => {
        const startTime = performance.now();
        const step = (currentTime) => {
            const elapsed = currentTime - startTime;
            const progress = Math.min(elapsed / duration, 1);
            frameFunc(progress);
            if (progress < 1) requestAnimationFrame(step);
            else resolve();
        };
        requestAnimationFrame(step);
    });
}

// 實時改變時針與分針屬性
updatePointersRealtime(duration) {
    const hourHand = this.doc.querySelector('#path900');
    const minHand = this.doc.querySelector('#path1029');
    const startHour = this.currentHourRotation || -90;
    const startMin = this.currentMinRotation || 0;

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

// 實時改變反光面透明度
async flashGleam() {
    const gleam = this.doc.querySelector('#ckgleam');
    if (!gleam) return;
    // 0.36秒內從 0 -> 1 -> 0
    await this.animateAttribute(360, (p) => {
        const opacity = p <= 0.5 ? p * 2 : 2 - p * 2;
        this.updateOpacity(gleam, opacity);
    });
}
```

---

### 3. 修改走子與落子邏輯 (全屬性操作)
將 `animateArc` 與 `animateLinear` 改為不使用 CSS transition 的版本:

```javascript
async animateArc(el, startIdx, endIdx) {
    const startAngle = (startIdx + 1) * 30;
    let endAngle = (endIdx + 1) * 30;
    if (endAngle <= startAngle) endAngle += 360;

    return this.animateAttribute(1000, (p) => {
        const currentAngle = startAngle + (endAngle - startAngle) * p;
        const rad = currentAngle * Math.PI / 180;
        const x = 144 * Math.sin(rad);
        const y = -144 * Math.cos(rad);
        this.updateTransform(el, 'translate', `${x},${y}`);
    });
}

async animateLinear(el, targetX, targetY) {
    const startTrans = this.getTransformValue(el, 'translate');
    let sX = 0, sY = 0;
    if (startTrans) {
        [sX, sY] = startTrans.split(',').map(Number);
    }

    return this.animateAttribute(1000, (p) => {
        const x = sX + (targetX - sX) * p;
        const y = sY + (targetY - sY) * p;
        this.updateTransform(el, 'translate', `${x},${y}`);
    });
}
```

---

### 4. 修正吃子與棋子隱藏 (透明度實時改變)
```javascript
async capturePiece(pieceValue) {
    const owner = pieceValue % 2 === 0 ? 1 : 0;
    const el = this.getPieceEl(pieceValue);
    const finalY = this.getAvailableHandY(owner);
    const finalX = owner === 1 ? 208 : -208;
    this.hands[owner].push(pieceValue);

    // 同時執行:透明度變 0.36 與 移動到手上
    await Promise.all([
        this.animateAttribute(1000, (p) => {
            const currentOpacity = 1 - (1 - 0.36) * p;
            this.updateOpacity(el, currentOpacity);
        }),
        this.animateLinear(el, finalX, finalY)
    ]);
    this.updateOpacity(el, 1); // 到達後恢復
}

// 隨機局面按鈕:棋子消失動畫
async hideAllPieces() {
    const pieces = [];
    for(let i=1; i<=12; i++) pieces.push(this.getPieceEl(i));
    
    await this.animateAttribute(800, (p) => {
        pieces.forEach(el => this.updateOpacity(el, 1 - p));
    });
}
```

---

### 5. 同步執行呼叫範例
確保指針轉動、走子/落子、反光面同時啟動:

```javascript
// 在 executeMove 中
await Promise.all([
    this.updatePointersRealtime(1000), // 時針轉動 1s
    this.flashGleam(),                 // 反光面 0.36s
    this.animateArc(el, startIndex, targetIndex) // 走子 1s
]);
```
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
一枝独秀
一枝独秀
帖子: 5210
注册时间: 2022年 11月 18日 17:36
为圈友点赞: 175 次
被圈友点赞: 204 次
联系:

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

帖子 ejsoon »

去掉所有對cclok內部元素的style改動。如有必要,應直接更改svg元素的屬性。

代码: 全选

以下是為您移除所有針對 `cclok` 內部 SVG 元素 `style` 屬性操作,改為直接控制 DOM 屬性(Attributes)的修改片段:

### 1. 修改綁定事件中的滑鼠游標設定 (移至 `bindEvents`)
將原本的 `style.cursor = 'pointer'` 改為 `setAttribute('cursor', 'pointer')`:

```javascript
    bindEvents() {
        for (let i = 1; i <= 12; i++) {
            const el = this.getPieceEl(i);
            if (el) {
                el.setAttribute('cursor', 'pointer'); // 移除 style,改用 SVG 屬性
                el.onclick = () => this.handlePieceClick(i);
            }
        }
        const cancelArea = this.doc.querySelector('#canclearea');
        if (cancelArea) {
            cancelArea.setAttribute('cursor', 'pointer'); // 移除 style,改用 SVG 屬性
            cancelArea.onclick = () => this.handleCancelClick();
        }
    }
```

### 2. 修改初始渲染中的透明度設定 (移至 `renderPiecePosition`)
確保不使用 `el.style.opacity`:

```javascript
    renderPiecePosition(pieceValue, boardIndex) {
        const el = this.getPieceEl(pieceValue);
        if (!el) return;
        const coords = this.getCoordsForPosition(boardIndex);
        this.updateTransform(el, 'translate', `${coords.x}, ${coords.y}`);
        this.updateOpacity(el, 1); // 使用前面定義的 updateOpacity 屬性方法
    }
```

### 3. 修改靜態指針更新方法 (移至 `updatePointers`)
完全移除 `style.transition`、`style.transformOrigin` 與 `style.transform`,一律使用屬性操作。對於 SVG 的旋轉中心點,我們可以直接利用屬性 `transform-origin="0 0"` 或在 `rotate` 中指定中心點:

```javascript
    updatePointers(isInit = false) {
        const hourHand = this.doc.querySelector('#path900');
        const minHand = this.doc.querySelector('#path1029');
        if (!hourHand || !minHand) return;

        // 移除所有 style 操作,直接設置 SVG 原生屬性
        hourHand.setAttribute('transform-origin', '0 0');
        minHand.setAttribute('transform-origin', '0 0');
        
        this.updateTransform(hourHand, 'rotate', this.hourRotation);
        this.updateTransform(minHand, 'rotate', this.minuteRotation);
    }
```

### 4. 修改隨機局面中的展示迴圈 (移至 `generateRandomBoard` 的內部迴圈)
移除重置 `style.transition` 與改變 `style.opacity` 的寫法:

```javascript
        // ... (generateRandomBoard 內部) ...
        for (let i = 0; i < 12; i++) {
            setTimeout(() => {
                const pieceVal = newBoard[i];
                const el = this.getPieceEl(pieceVal);
                if (el) {
                    const coords = this.getCoordsForPosition(i);
                    // 移除 style.transition = 'none' 
                    this.updateTransform(el, 'translate', `${coords.x}, ${coords.y}`);
                    // 移除 style.opacity = 1,改為屬性操作
                    this.updateOpacity(el, 1);
                }
            }, i * 100);
        }
        // ...
```
https://ejsoon.vip/
弈趣極光:享受思維樂趣
回复

在线用户

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