將改進混亂時鐘的網頁版
Re: 將改進混亂時鐘的網頁版
代码: 全选
改進程式:
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/
弈趣極光:享受思維樂趣
弈趣極光:享受思維樂趣
Re: 將改進混亂時鐘的網頁版
代码: 全选
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/
弈趣極光:享受思維樂趣
弈趣極光:享受思維樂趣
Re: 將改進混亂時鐘的網頁版
代码: 全选
改進程式:
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秒保存不會。
只需給出所要修改的地方。所有的代碼和注釋都要使用英文。代码: 全选
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/
弈趣極光:享受思維樂趣
弈趣極光:享受思維樂趣
Re: 將改進混亂時鐘的網頁版
當點擊center時,如果svg中不存在「viewBox="0 0 480 480" preserveAspectRatio="xMidYMid meet"」屬性,則加上。當upload ejtile時,如果存在則要在上載的內容中去掉。
只需給出所要修改的地方,所有的代碼和注釋都要使用英文。
- 附件
-
ejtileanimate202511172231.js- (137.7 KiB) 已下载 8 次
https://ejsoon.vip/
弈趣極光:享受思維樂趣
弈趣極光:享受思維樂趣
Re: 將改進混亂時鐘的網頁版
代码: 全选
根據您的需求,以下是針對 `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();
});
}
```代码: 全选
// 修改 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/
弈趣極光:享受思維樂趣
弈趣極光:享受思維樂趣
Re: 將改進混亂時鐘的網頁版
代码: 全选
重寫該程式:
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/
弈趣極光:享受思維樂趣
弈趣極光:享受思維樂趣
Re: 將改進混亂時鐘的網頁版
對此有何解決辦法?
在博弈搜尋(尤其是蒙特卡洛樹搜尋,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/
弈趣極光:享受思維樂趣
弈趣極光:享受思維樂趣
Re: 將改進混亂時鐘的網頁版
代码: 全选
寫一個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/
弈趣極光:享受思維樂趣
弈趣極光:享受思維樂趣
Re: 將改進混亂時鐘的網頁版
代码: 全选
為混亂時鐘(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/
弈趣極光:享受思維樂趣
弈趣極光:享受思維樂趣
Re: 將改進混亂時鐘的網頁版
代码: 全选
這是一個非常有趣且邏輯嚴密的遊戲設計!為了解決這個需求,我們需要為每一個 `<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/
弈趣極光:享受思維樂趣
弈趣極光:享受思維樂趣
Re: 將改進混亂時鐘的網頁版
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/
弈趣極光:享受思維樂趣
弈趣極光:享受思維樂趣
Re: 將改進混亂時鐘的網頁版
只需給出所要修改的地方。
代码: 全选
這裡針對您的需求,將動畫處理邏輯從 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/
弈趣極光:享受思維樂趣
弈趣極光:享受思維樂趣
Re: 將改進混亂時鐘的網頁版
只需給出所要修改的地方。
代码: 全选
這裡為您提供修改後的程式碼片段。我們將放棄 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/
弈趣極光:享受思維樂趣
弈趣極光:享受思維樂趣
Re: 將改進混亂時鐘的網頁版
代码: 全选
以下是為您移除所有針對 `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] 和 0 访客
