Re: 一個新的遊戲創意
出現報錯:Uncaught TypeError: can't access property "false", colors[shape.type] is undefined generateTiling
查找原因並修復。
查找原因並修復。
- 附件
-
trigoldentesselation21.html.7z- (6.43 KiB) 已下载 49 次
https://ejsoon.vip/
弈趣極光:享受思維樂趣
弈趣極光:享受思維樂趣
Re: 一個新的遊戲創意
gemini 3.1 pro
代码: 全选
### **錯誤原因分析**
這個錯誤是因為**物件屬性名稱不一致**所造成的。
我們來追蹤一下你的程式碼:
1. 當你建立**第一個(根)圖形**時,你賦予它的屬性名稱是 `type`:
```javascript
shapes.push({ type: 'A', mirrored: false, vertices: rootVertices });
```
2. 但是,當程式透過 `attachByEdge` 函數生成**後續拼接的新圖形**時,回傳的物件屬性名稱卻寫成了 `shapeType`:
```javascript
return { vertices: worldVerts, rotation: foundK, mirrored, shapeType: templateKey };
```
3. 最後在繪製 SVG 的迴圈中,你統一呼叫了 `shape.type`:
```javascript
const fill = colors[shape.type][shape.mirrored];
```
因為除了第一個圖形之外,其他圖形都沒有 `type` 這個屬性(它們只有 `shapeType`),所以 `shape.type` 會回傳 `undefined`。當程式試圖讀取 `colors[undefined][false]` 時,就會拋出你看到的那個報錯:`can't access property "false", colors[shape.type] is undefined`。
---
### **修復方式**
你只需要把 `attachByEdge` 函數回傳物件裡的 `shapeType` 改成 `type`,讓所有圖形的資料結構保持一致即可。
請找到程式碼約第 253 行的 `attachByEdge` 函數結尾,並進行以下修改:
**修改前:**
```javascript
function attachByEdge(templateKey, mirrored, edgeIndexToAlign, worldP, worldQ) {
// ... 前面省略 ...
const worldVerts = localVerts.map(v => ringAdd(worldQ, ringMul(ringSub(v, localVerts[i]), zetaPow(foundK))));
return { vertices: worldVerts, rotation: foundK, mirrored, shapeType: templateKey };
}
```
**修改後:**
```javascript
function attachByEdge(templateKey, mirrored, edgeIndexToAlign, worldP, worldQ) {
// ... 前面省略 ...
const worldVerts = localVerts.map(v => ringAdd(worldQ, ringMul(ringSub(v, localVerts[i]), zetaPow(foundK))));
return { vertices: worldVerts, rotation: foundK, mirrored, type: templateKey }; // 將 shapeType 改為 type
}
```
存檔並重新整理頁面後,密鋪圖形應該就能夠順利生成了!https://ejsoon.vip/
弈趣極光:享受思維樂趣
弈趣極光:享受思維樂趣
Re: 一個新的遊戲創意
如何分別在這三個圖形內,畫出面積最大的圓,給出計算過程,以及圓心相對左下角的坐標。
gemini 3.6 flash
gemini 3.6 flash
代码: 全选
這三個圖形均由黃金比例 $\varphi = \frac{1+\sqrt{5}}{2} \approx 1.618$ 與特定角度($36^\circ, 72^\circ, 108^\circ, 144^\circ$)構成。以**左下角頂點為原點 $(0,0)$**、底邊落在正 $x$ 軸方向建立直角坐標系,計算過程與結果如下:
---
## 1. 藍色四邊形 (Figure 1)
### **幾何分析與頂點坐標**
* **頂點坐標**:
* $A = (0, 0)$
* $D = (2\varphi, 0) = (1+\sqrt{5}, 0) \approx (3.236, 0)$
* $B = (\varphi^2 \cos 36^\circ, \varphi^2 \sin 36^\circ) = \left(\frac{2+\sqrt{5}}{2}, \frac{\sqrt{10-2\sqrt{5}}(1+\sqrt{5})}{8}\right) \approx (2.118, 1.539)$
* $C = \left(\frac{5+3\sqrt{5}}{4}, \frac{\sqrt{10-2\sqrt{5}}}{4}\right) \approx (2.927, 0.951)$
### **最大內切圓計算**
此凸四邊形四邊無法同時切於同一個圓。最大圓為同時切於右下三條邊 $BC, CD, AD$ 的圓(即角 $C$ 與角 $D$ 的內角平分線交點):
1. **角 $D$ 平分線方程**:過 $D(2\varphi, 0)$,傾角為 $144^\circ$,直線為 $y = -\tan 36^\circ (x - 2\varphi)$。
2. **角 $C$ 平分線方程**:過 $C$,傾角為 $216^\circ$(斜率為 $\tan 36^\circ$),直線為 $y - y_C = \tan 36^\circ (x - x_C)$。
3. **求解圓心 $O_1(x_1, y_1)$**:
$$x_1 = \frac{3\varphi}{2} = \frac{3 + 3\sqrt{5}}{4} \approx 2.427$$
$$y_1 = R_1 = \sin 36^\circ = \frac{\sqrt{10-2\sqrt{5}}}{4} \approx 0.588$$
* **圓心坐標**:$\left(\frac{3+3\sqrt{5}}{4}, \frac{\sqrt{10-2\sqrt{5}}}{4}\right) \approx (2.427, 0.588)$
* **最大半徑**:$R_1 = \sin 36^\circ = \frac{\sqrt{10-2\sqrt{5}}}{4} \approx 0.588$
* **最大面積**:$S_1 = \pi R_1^2 = \frac{5-\sqrt{5}}{8}\pi \approx 1.086$
---
## 2. 橙色梯形 (Figure 2)
### **幾何分析與頂點坐標**
* 上底 $BC = 1$ 與下底 $AD = \varphi^2$ 互相平行(因 $\angle A + \angle B = 36^\circ + 144^\circ = 180^\circ$)。
* 梯形的高 $h = \varphi \sin 36^\circ = \sin 72^\circ = \frac{\sqrt{10+2\sqrt{5}}}{4} \approx 0.951$。
### **最大內切圓計算**
受限於兩平行邊 $BC$ 與 $AD$ 的距離,圓的最大直徑即為梯形的高 $h$,故最大半徑為:
$$R_2 = \frac{h}{2} = \frac{\varphi \sin 36^\circ}{2} = \frac{\sqrt{10+2\sqrt{5}}}{8} \approx 0.476$$
將圓放置於同時切於斜邊 $AB$、上底 $BC$ 與下底 $AD$ 的位置時,圓心位於角 $A$ 的平分線(傾角 $18^\circ$)上:
1. $y_2 = R_2 = \frac{\varphi \sin 36^\circ}{2} \approx 0.476$
2. $x_2 = \frac{y_2}{\tan 18^\circ} = \frac{3\varphi + 1}{4} = \frac{5 + 3\sqrt{5}}{8} \approx 1.464$
* **圓心坐標**:$\left(\frac{5+3\sqrt{5}}{8}, \frac{\sqrt{10+2\sqrt{5}}}{8}\right) \approx (1.464, 0.476)$
* **最大半徑**:$R_2 = \frac{\sqrt{10+2\sqrt{5}}}{8} \approx 0.476$
* **最大面積**:$S_2 = \pi R_2^2 = \frac{5+\sqrt{5}}{32}\pi \approx 0.710$
---
## 3. 綠色四邊形 (Figure 3)
### **幾何分析與頂點坐標**
* 邊 $AB$(長度 2,傾角 $36^\circ$)與邊 $CD$(長度 1,傾角 $36^\circ$)為一組**平行線**。
* 兩平行邊之間的垂直距離為 $H = \varphi \sin 36^\circ = \sin 72^\circ \approx 0.951$。
### **最大內切圓計算**
受限於平行邊 $AB$ 與 $CD$ 的間距,最大圓直徑等於 $H$,半徑為:
$$R_3 = \frac{H}{2} = \frac{\varphi \sin 36^\circ}{2} = \frac{\sqrt{10+2\sqrt{5}}}{8} \approx 0.476$$
令圓同時相切於平行邊 $AB, CD$ 以及底邊 $AD$(即 $y=0$):
1. $y_3 = R_3 = \frac{\varphi \sin 36^\circ}{2} \approx 0.476$
2. 圓心需落在兩平行邊的中線 $x \sin 36^\circ - y \cos 36^\circ = \frac{\varphi \sin 36^\circ}{2}$ 上,代入 $y_3$ 解得:
$$x_3 = \frac{\varphi^3}{4} = \frac{2+\sqrt{5}}{4} \approx 1.059$$
* **圓心坐標**:$\left(\frac{2+\sqrt{5}}{4}, \frac{\sqrt{10+2\sqrt{5}}}{8}\right) \approx (1.059, 0.476)$
* **最大半徑**:$R_3 = \frac{\sqrt{10+2\sqrt{5}}}{8} \approx 0.476$
* **最大面積**:$S_3 = \pi R_3^2 = \frac{5+\sqrt{5}}{32}\pi \approx 0.710$
---
## 📊 總結對比表
| 圖形 | 圓心相對左下角坐標 $(x, y)$ | 最大半徑 $R$ | 最大圓面積 $S$ |
| --- | --- | --- | --- |
| **圖 1 (藍色)** | $(2.427, 0.588)$ | $\sin 36^\circ \approx 0.588$ | $\approx 1.086$ |
| **圖 2 (橙色)** | $(1.464, 0.476)$ | $\frac{\sin 72^\circ}{2} \approx 0.476$ | $\approx 0.710$ |
| **圖 3 (綠色)** | $(1.059, 0.476)$ | $\frac{\sin 72^\circ}{2} \approx 0.476$ | $\approx 0.710$ |https://ejsoon.vip/
弈趣極光:享受思維樂趣
弈趣極光:享受思維樂趣
Re: 一個新的遊戲創意
claude sonnet 5
如何分別在這三個圖形內,畫出面積最大的圓,給出計算過程,以及圓心相對左下角的坐標。如何分別在這三個圖形內,畫出面積最大的圓,給出計算過程,以及圓心相對左下角的坐標。
根據我的理解,圖1的圓只有一個,而圖2和圖3的圓可以在兩個平行的邊內移動。
則在圖2和圖3中,在始終與兩個平行的邊(設為a和c)相切的同時,圓從跟餘下的一邊(設為b)相切走到與另一邊(設為d)相切,請給出圓心所經過的路徑的中點坐標。
如何分別在這三個圖形內,畫出面積最大的圓,給出計算過程,以及圓心相對左下角的坐標。如何分別在這三個圖形內,畫出面積最大的圓,給出計算過程,以及圓心相對左下角的坐標。
代码: 全选
这是一个很好的几何问题——为每个凸四边形找"最大内切圆",本质上是求 **切比雪夫中心(Chebyshev center)**:即到四条边的最短距离最大化的点,这个最短距离就是最大圆的半径。让我用代码精确计算三个图形的顶点坐标,再用线性规划求解。数值解已经稳定,现在用符号计算(sympy)把结果化成含 φ 的精确表达式,让答案更清晰。## 方法:什么是"面积最大的内切圆"
对凸四边形而言,能放进去的最大圆的圆心,是**到四条边距离的最小值达到最大**的点(切比雪夫中心/Chebyshev center)。做法是:
1. 把每条边写成直线方程,取指向内部的单位法向量,得到"点到该边的(内部)距离"是关于 (x, y) 的线性函数。
2. 求 max r,使得这个点到四条边的距离都 ≥ r —— 这是一个线性规划问题。
3. 最优解通常出现在**恰好与其中三条边相切**、第四条边有富余间隙的位置(用 Python 的 `scipy.optimize.linprog` 求解,并用 sympy 验证出精确的黄金比例封闭解)。
## 建立坐标(左下角顶点为原点)
以每个图的"左下角"顶点为 A=(0,0),沿底边正方向到相邻底边顶点,再用给定角度和边长推出其余顶点(φ = (1+√5)/2 ≈ 1.618034):
**图1(蓝)**:A(0,0), B(2.118034, 1.538842), C(2.927051, 0.951057), D(3.236068, 0)
**图2(橙)**:A(0,0), B(1.309017, 0.951057), C(2.309017, 0.951057), D(2.618034, 0)
**图3(绿)**:A(0,0), B(1.618034, 1.175571), C(2.427051, 0.587785), D(1.618034, 0)
(代码验证过每个顶点的夹角与边长都和图片标注完全一致。)
## 求解结果
**图1(蓝三角状四边形)** —— 圆与边 AB、BC、DA 相切,离 CD 还有约0.85的富余:
- 圆心(相对左下角 A) ≈ **(2.118034, 0.688191)**,精确值 x = φ³/2 = 1+√5/2
- 半径 r ≈ **0.688191** = √(25+10√5)/10
**图2(橙梯形)** —— 圆与边 AB、BC、DA 相切:
- 圆心 ≈ **(1.463525, 0.475528)**,精确值 x = (3φ+1)/4
- 半径 r ≈ **0.475528** = sin72°/2
**图3(绿风筝形)** —— 这个图形比较特殊,圆是与 AB、BC、CD 三条边相切(而不是底边 DA):
- 圆心 ≈ **(1.618034, 0.587785)**,精确值恰好是 (φ, sin36°)
- 半径 r ≈ **0.475528** = sin72°/2(和图2的半径完全相同,并非巧合——两图都含有边长为1的短边组)
三个图上方的示意图中,虚线圆即为对应的最大内切圆,圆心用十字标出。图1的圆明显比图2、图3大,这是因为图1整体尺寸(边长 2φ、φ² 等)比另外两个大一圈。
根據我的理解,圖1的圓只有一個,而圖2和圖3的圓可以在兩個平行的邊內移動。
則在圖2和圖3中,在始終與兩個平行的邊(設為a和c)相切的同時,圓從跟餘下的一邊(設為b)相切走到與另一邊(設為d)相切,請給出圓心所經過的路徑的中點坐標。
代码: 全选
看出來了——圖2、圖3各有一組平行邊(圖2是 BC∥DA,圖3是 AB∥CD),平行邊間距直接鎖定了半徑,圓心只能沿"中線"滑動。我來精確算出圓心滑動的兩個極端位置,再取中點。找到了很清楚的規律。先算出兩個極端圓心位置與路徑,再畫圖確認。## 你的觀察完全正確
圖1沒有平行邊,所以最大內切圓位置唯一。圖2、圖3各有一組平行邊(圖2是 BC∥DA,圖3是 AB∥CD),**只要圓同時與這兩條平行邊相切,半徑就被鎖死了**(= 兩平行線間距的一半),但圓心可以沿著"中線"左右滑動——直到碰到剩下兩條邊(b、d)中的一條為止。這條中線段的兩端,就是題目說的「與 b 相切」和「與 d 相切」的極限位置。
## 計算過程
半徑固定為兩平行邊間距的一半:
$$r = \frac{0.951057}{2} = 0.475528 = \frac{\sin72°}{2}$$
**圖2(橙)**:平行邊 BC、DA,滑動方向水平(y 恆定 = r)
- 切 AB 端:圓心 = ((3φ+1)/4, r) ≈ (1.463525, 0.475528)
- 切 CD 端:圓心 ≈ (1.963525, 0.475528)(兩端 x 差剛好等於 0.5)
- **中點 = ((3φ+2)/4, sin72°/2) ≈ (1.713525, 0.475528)**
**圖3(綠)**:平行邊 AB、CD,滑動方向沿 AB 的斜率(中線不是水平的)
- 切 BC 端:圓心 = (φ, sin36°) ≈ (1.618034, 0.587785)(這正是我上一輪找到的"唯一"解——其實只是路徑的一個端點)
- 切 DA 端:圓心 = ((3φ+1)/4, sin72°/2) ≈ (1.463525, 0.475528)(和圖2的「切AB」端點是同一個座標,因為兩圖都用同一條 1 長邊在同一相對位置)
- **中點 = ((7φ+1)/8, (2sin36°+sin72°)/4) ≈ (1.540780, 0.531657)**
圖中虛線圓是兩個端點位置,實線圓是路徑中點的位置,粗線段是圓心走過的軌跡。https://ejsoon.vip/
弈趣極光:享受思維樂趣
弈趣極光:享受思維樂趣
Re: 一個新的遊戲創意
代码: 全选
<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="0" rotatestep="0" rotatestart="0" stepvalue="60" snapto="0" displaywait="1"> <g id="tile0" frontfill="#ffc2eb" backfill="#ff94dc"><path d="M -61.687,17.119 L -14.563,-17.119 L 21.437,-17.119 L 32.562,17.119 Z"></path></g><g id="tile1" frontfill="#ffc2eb" backfill="#ff94dc"><path d="M -76.249,24.775 L -0.001,-30.623 L 29.124,-9.463 L 40.249,24.775 Z"></path></g><g id="tile2" frontfill="#ffc2eb" backfill="#ff94dc"><path d="M -55.468,19.14 L 2.781,-23.18 L 31.906,-2.02 L 2.781,19.14 Z"></path></g><g id="tile3" frontfill="#96ffbe" backfill="#71e39d"><path d="M -61.687,17.119 L -14.563,-17.119 L 21.437,-17.119 L 32.562,17.119 Z"></path></g><g id="tile4" frontfill="#96ffbe" backfill="#96ffbe"><path d="M -76.249,24.775 L -0.001,-30.623 L 29.124,-9.463 L 40.249,24.775 Z"></path></g><g id="tile5" frontfill="#96ffbe" backfill="#71e39d"><path d="M -55.468,19.14 L 2.781,-23.18 L 31.906,-2.02 L 2.781,19.14 Z"></path></g></defs> <g class="etdrop" stroke="#777" stroke-width="2" stroke-linejoin="round" transform="translate(97.45,-59.09) scale(0.81)"><g class="etboard"></g><use href="#tile0" fill="#ffc2eb" transform="translate(182,272) scale(1,1) rotate(0)" class="tiledropped"></use><use href="#tile3" fill="#96ffbe" transform="translate(193.781,229.204) scale(1,1) rotate(108.001)" class="tiledropped"></use><use href="#tile4" fill="#96ffbe" transform="translate(196.563,313.894) scale(1,1) rotate(36)" class="tiledropped"></use><use href="#tile1" fill="#ffc2eb" transform="translate(232.561,245.419) scale(1,1) rotate(-107.998)" class="tiledropped"></use><use href="#tile1" fill="#ff94dc" transform="translate(243.686,348.132) scale(-1,1) rotate(36.001)" class="tiledropped"></use><use href="#tile4" fill="#96ffbe" transform="translate(272.812,388.219) scale(1,1) rotate(143.999)" class="tiledropped"></use><use href="#tile3" fill="#96ffbe" transform="translate(193.125,374.715) scale(1,1) rotate(0)" class="tiledropped"></use><use href="#tile0" fill="#ffc2eb" transform="translate(116.875,374.715) scale(1,1) rotate(180)" class="tiledropped"></use><use href="#tile0" fill="#ffc2eb" transform="translate(164.001,408.953) scale(1,1) rotate(180)" class="tiledropped"></use><use href="#tile3" fill="#96ffbe" transform="translate(105.094,417.511) scale(1,1) rotate(-71.999)" class="tiledropped"></use><use href="#tile4" fill="#96ffbe" transform="translate(142.563,456.695) scale(1,1) rotate(-71.999)" class="tiledropped"></use><use href="#tile0" fill="#ff94dc" transform="translate(231.908,428.092) scale(-1,1) rotate(-107.998)" class="tiledropped"></use><use href="#tile1" fill="#ffc2eb" transform="translate(337.936,348.131) scale(1,1) rotate(-36.001)" class="tiledropped"></use><use href="#tile4" fill="#96ffbe" transform="translate(337.938,409.379) scale(1,1) rotate(143.999)" class="tiledropped"></use><use href="#tile5" fill="#71e39d" transform="translate(331.309,302.969) scale(-1,1) rotate(144.002)" class="tiledropped"></use><use href="#tile2" fill="#ff94dc" transform="translate(68.437,408.953) scale(-1,1) rotate(-144)" class="tiledropped"></use><use href="#tile2" fill="#ff94dc" transform="translate(83.249,484.739) scale(-1,1) rotate(107.999)" class="tiledropped"></use><use href="#tile3" fill="#71e39d" transform="translate(47.655,372.694) scale(-1,1) rotate(-108)" class="tiledropped"></use><use href="#tile5" fill="#71e39d" transform="translate(28.187,443.191) scale(-1,1) rotate(36)" class="tiledropped"></use><use href="#tile1" fill="#ff94dc" transform="translate(59.436,311.661) scale(-1,1) rotate(72)" class="tiledropped"></use><use href="#tile1" fill="#ffc2eb" transform="translate(-5.688,401.297) scale(1,1) rotate(144)" class="tiledropped"></use><use href="#tile4" fill="#96ffbe" transform="translate(279.686,230.109) scale(-1,1) rotate(-72.002)" class="tiledropped"></use><use href="#tile3" fill="#71e39d" transform="translate(-45.531,431.362) scale(-1,1) rotate(-35.999)" class="tiledropped"></use><use href="#tile1" fill="#ff94dc" transform="translate(385.063,424.69) scale(-1,1) rotate(108.002)" class="tiledropped"></use><use href="#tile2" fill="#ff94dc" transform="translate(344.565,454.541) scale(-1,1) rotate(-35.998)" class="tiledropped"></use></g></svg>改成正反面不同色。
https://ejsoon.vip/
弈趣極光:享受思維樂趣
弈趣極光:享受思維樂趣
Re: 一個新的遊戲創意
還是單色更好看

代码: 全选
<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="0" rotatestep="0" rotatestart="0" stepvalue="60" snapto="0" displaywait="1"> <g id="tile0" frontfill="#ffc2eb" backfill="#ffc2eb"><path d="M -61.687,17.119 L -14.563,-17.119 L 21.437,-17.119 L 32.562,17.119 Z"></path></g><g id="tile1" frontfill="#ffc2eb" backfill="#ffc2eb"><path d="M -76.249,24.775 L -0.001,-30.623 L 29.124,-9.463 L 40.249,24.775 Z"></path></g><g id="tile2" frontfill="#ffc2eb" backfill="#ffc2eb"><path d="M -55.468,19.14 L 2.781,-23.18 L 31.906,-2.02 L 2.781,19.14 Z"></path></g><g id="tile3" frontfill="#96ffbe" backfill="#96ffbe"><path d="M -61.687,17.119 L -14.563,-17.119 L 21.437,-17.119 L 32.562,17.119 Z"></path></g><g id="tile4" frontfill="#96ffbe" backfill="#96ffbe"><path d="M -76.249,24.775 L -0.001,-30.623 L 29.124,-9.463 L 40.249,24.775 Z"></path></g><g id="tile5" frontfill="#96ffbe" backfill="#96ffbe"><path d="M -55.468,19.14 L 2.781,-23.18 L 31.906,-2.02 L 2.781,19.14 Z"></path></g></defs> <g class="etdrop" stroke="#777" stroke-width="2" stroke-linejoin="round" transform="translate(92.45,-27.09) scale(0.81)"><g class="etboard"></g><use href="#tile0" fill="#ffc2eb" transform="translate(182,272) scale(1,1) rotate(0)" class="tiledropped"></use><use href="#tile3" fill="#96ffbe" transform="translate(193.781,229.204) scale(1,1) rotate(108.001)" class="tiledropped"></use><use href="#tile4" fill="#96ffbe" transform="translate(196.563,313.894) scale(1,1) rotate(36)" class="tiledropped"></use><use href="#tile1" fill="#ffc2eb" transform="translate(232.561,245.419) scale(1,1) rotate(-107.998)" class="tiledropped"></use><use href="#tile1" fill="#ffc2eb" transform="translate(243.686,348.132) scale(-1,1) rotate(36.001)" class="tiledropped"></use><use href="#tile4" fill="#96ffbe" transform="translate(272.812,388.219) scale(1,1) rotate(143.999)" class="tiledropped"></use><use href="#tile3" fill="#96ffbe" transform="translate(193.125,374.715) scale(1,1) rotate(0)" class="tiledropped"></use><use href="#tile0" fill="#ffc2eb" transform="translate(116.875,374.715) scale(1,1) rotate(180)" class="tiledropped"></use><use href="#tile0" fill="#ffc2eb" transform="translate(164.001,408.953) scale(1,1) rotate(180)" class="tiledropped"></use><use href="#tile3" fill="#96ffbe" transform="translate(105.094,417.511) scale(1,1) rotate(-71.999)" class="tiledropped"></use><use href="#tile4" fill="#96ffbe" transform="translate(142.563,456.695) scale(1,1) rotate(-71.999)" class="tiledropped"></use><use href="#tile0" fill="#ffc2eb" transform="translate(231.908,428.092) scale(-1,1) rotate(-107.998)" class="tiledropped"></use><use href="#tile1" fill="#ffc2eb" transform="translate(337.936,348.131) scale(1,1) rotate(-36.001)" class="tiledropped"></use><use href="#tile4" fill="#96ffbe" transform="translate(337.938,409.379) scale(1,1) rotate(143.999)" class="tiledropped"></use><use href="#tile5" fill="#96ffbe" transform="translate(331.309,302.969) scale(-1,1) rotate(144.002)" class="tiledropped"></use><use href="#tile2" fill="#ffc2eb" transform="translate(68.437,408.953) scale(-1,1) rotate(-144)" class="tiledropped"></use><use href="#tile2" fill="#ffc2eb" transform="translate(83.249,484.739) scale(-1,1) rotate(107.999)" class="tiledropped"></use><use href="#tile3" fill="#96ffbe" transform="translate(47.655,372.694) scale(-1,1) rotate(-108)" class="tiledropped"></use><use href="#tile5" fill="#96ffbe" transform="translate(28.187,443.191) scale(-1,1) rotate(36)" class="tiledropped"></use><use href="#tile1" fill="#ffc2eb" transform="translate(59.436,311.661) scale(-1,1) rotate(72)" class="tiledropped"></use><use href="#tile1" fill="#ffc2eb" transform="translate(-5.688,401.297) scale(1,1) rotate(144)" class="tiledropped"></use><use href="#tile4" fill="#96ffbe" transform="translate(279.686,230.109) scale(-1,1) rotate(-72.002)" class="tiledropped"></use><use href="#tile3" fill="#96ffbe" transform="translate(-45.531,431.362) scale(-1,1) rotate(-35.999)" class="tiledropped"></use><use href="#tile1" fill="#ffc2eb" transform="translate(385.063,424.69) scale(-1,1) rotate(108.002)" class="tiledropped"></use><use href="#tile2" fill="#ffc2eb" transform="translate(344.565,454.541) scale(-1,1) rotate(-35.998)" class="tiledropped"></use></g></svg>https://ejsoon.vip/
弈趣極光:享受思維樂趣
弈趣極光:享受思維樂趣
Re: 一個新的遊戲創意
代码: 全选
《鑫頂棋(TriGolden)》是一款二人抽象策略遊戲,為其製作一個基於svg的html5弈棋遊戲:
一,頁面安排
頁面包括三個部分:我方控制區,對方控制區,中間棋盤區。中間棋盤區佔據中間區域。另兩個控制區分佔另兩個區域。
如果是電腦端,中間棋盤區的高寬比為1:1,佔滿中間部分,剩餘的區域,右方是我方控制區,左方是對方控制區。控制區的最小寬度為240px,如果屏幕變窄則有可能擠壓中間區域,使之比例不再是1:1。
如果是手機端,中間棋盤區的高寬比為1:1,佔滿中間部分,剩餘的區域,下方是我方控制區,上方是對方控制區。控制區的最小高度為240px,如果屏幕過小則有可能擠壓中間區域,使之比例不再是1:1。
所上傳的svg將直接貼進中間棋盤區,它將是棋盤,高寬比將始終保持1:1,並始終垂直及水平居中。如果是電腦端,它離上下左右的邊距將為24px,如果是手機端,邊距將是7px。為棋盤增加懸浮陰影效果。
在控制區中,有分數區和棋子區以及確認按鈕,每方有三種棋子,則五個元素大小佔位是相同的。如果是電腦端,則它們縱向排列並平分間距,我方三種svg棋子的右方是它的數量,對方的數量則標在左邊。如果是手機端,則它們横向排列並平分間距,三種svg棋子的下方是它的數量。分數區將居中顯示分數,確認按鈕初始是灰色未激活狀態。
雙方的三種svg棋子將取自所上傳的svg的defs中,其名稱和id對應關係為:我方棋子id=tile0、tile1、tile2,對方棋子id=tile3、tile4、tile5。
每個棋子都在一個相同大小的矩形區域內,電腦端稍大而手機端稍小,為其增加陰影懸浮效果。
二,棋盤區域
當棋盤中沒有棋子時,其不可平移和縮放,當棋盤中至少存在一個棋子時,棋盤將可以平移和縮放。當遊戲結束到下一輪時,棋盤將清空並復原為初始的平移和縮放狀態。
所有的平移縮放都改變的是.etdrop的transform的值,保留最多三位小數。如果是手機端,則可用單指拖動平移,雙指縮放。如果是電腦端,則可用鼠標按住拖動平移,滾輪縮放。
棋盤區域內的下方部分將絕對定位一個工具欄,工具欄包含「旋轉、AI控制、後退、前進、導出、狀態」。
當點擊棋盤的空白區域,則工具欄消失,出現平移和縮放按鈕,它們絕對定位並覆蓋在棋盤上,左下角和右上角是縮小,左上角和右下角是放大,正中上下左右分別是上移下移左移右移,中間是恢復初始狀態。
三,棋子點擊和確認行動
為棋子增加點擊事件,當點擊其中一個棋子時,它的陰影和邊框將變色,表示其被選中。當有棋子被選中時,點擊棋盤,則把該棋子的use[id=tile$]放至棋盤g.etdrop內的最後,棋子位置用transform-translate來定義。右下角的文字提示則除了顯示棋盤坐標,還加增顯示該棋子所佔的另一個正三角形坐標。
比如我選擇一個紅色棋子,當我點擊(b,-1),則紅色棋子所佔的另一個正三角形坐標應為(b,-2),use[id=tile1]的translate應為(27,-15.588),其中15.588是正三角形邊長的3/4。
所有的css和js代碼都寫在一個html中,所有小數保留小數點後3位。https://ejsoon.vip/
弈趣極光:享受思維樂趣
弈趣極光:享受思維樂趣
Re: 一個新的遊戲創意
《頂鑫棋(TriGolden)》是一款二人抽象策略遊戲,為其製作一個基於svg的html5弈棋遊戲:
一,頁面安排
頁面包括三個部分:先手方控制區,對後手方控制區,中間棋盤區。中間棋盤區佔據中間區域。另兩個控制區分佔另兩個區域。
如果是電腦端,中間棋盤區的高寬比為1:1,佔滿中間部分,剩餘的區域,右方是我方控制區,左方是對方控制區。控制區的最小寬度為240px,如果屏幕變窄則有可能擠壓中間區域,使之比例不再是1:1。
如果是手機端,中間棋盤區的高寬比為1:1,佔滿中間部分,剩餘的區域,下方是先手方控制區,上方是後手方控制區。控制區的最小高度為240px,如果屏幕過小則有可能擠壓中間區域,使之比例不再是1:1。
所上傳的svg將直接貼進中間棋盤區,它將是棋盤,高寬比將始終保持1:1,並始終垂直及水平居中。如果是電腦端,它離上下左右的邊距將為24px,如果是手機端,邊距將是7px。為棋盤增加懸浮陰影效果。
在控制區中,有分數區和棋子區以及確認區,每方有三種棋子,則五個元素大小佔位是相同的。如果是電腦端,則它們縱向排列並平分間距,先手方三種svg棋子的右方是它的數量,後手方的數量則標在左邊。如果是手機端,則它們横向排列並平分間距,三種svg棋子的下方是它的數量。分數區將居中顯示分數,數字下方是AI和人類的切換svg圖標。確認區有「打叉」和「打勾」兩個按鈕,初始是灰色未激活狀態。
雙方的三種svg棋子將取自所下面的svg的defs中,其名稱和id對應關係為:先手方棋子id=tile0、tile1、tile2,後手方棋子id=tile3、tile4、tile5。
<svg id="etanitrigolden" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg" width="480" height="480" viewBox="0 0 480 480" preserveAspectRatio="xMidYMid meet"> <defs> <g id="tile0" fill="#ffc2eb"><path d="M -61.687,17.119 L -14.563,-17.119 L 21.437,-17.119 L 32.562,17.119 Z"/></g><g id="tile1" fill="#ffc2eb"><path d="M -76.249,24.775 L -0.001,-30.623 L 29.124,-9.463 L 40.249,24.775 Z"/></g><g id="tile2" fill="#ffc2eb"><path d="M -55.468,19.14 L 2.781,-23.18 L 31.906,-2.02 L 2.781,19.14 Z"/></g><g id="tile3" fill="#96ffbe"><path d="M -61.687,17.119 L -14.563,-17.119 L 21.437,-17.119 L 32.562,17.119 Z"/></g><g id="tile4" fill="#96ffbe"><path d="M -76.249,24.775 L -0.001,-30.623 L 29.124,-9.463 L 40.249,24.775 Z"/></g><g id="tile5" fill="#96ffbe"><path d="M -55.468,19.14 L 2.781,-23.18 L 31.906,-2.02 L 2.781,19.14 Z"/></g></defs> <g class="etdrop" stroke="#777" stroke-width="2" stroke-linejoin="round" transform="translate(240,240) scale(1,1)" id="etanidrop"></g></svg>
每個棋子都在一個相同大小的矩形區域內,電腦端稍大而手機端稍小,為其增加陰影懸浮效果。
二,棋盤區域
當棋盤中沒有棋子時,其不可平移和縮放,當棋盤中至少存在一個棋子時,棋盤將可以平移和縮放。當遊戲結束到下一輪時,棋盤將清空並復原為初始的平移和縮放狀態。
所有的平移縮放都改變的是.etdrop的transform的值,保留最多三位小數。如果是手機端,則可用單指拖動平移,雙指縮放。如果是電腦端,則可用鼠標按住拖動平移,滾輪縮放。
棋盤區域內的下方部分將絕對定位一個工具欄,工具欄包含「旋轉、AI控制、後退、前進、導出、狀態」。後退和前進就是undo和redo,其它功能將在以後介紹。
當點擊棋盤的空白區域,則工具欄消失,出現平移和縮放按鈕,它們絕對定位並覆蓋在棋盤上,左下角和右上角是縮小,左上角和右下角是放大,正上方、正下方、正左方、正右方分別是上移下移左移右移,中間是恢復初始狀態(移回中間去掉縮放)。再點一次棋盤的空白區域,平移縮放按鈕消失,工具欄出現。
三,落子規則
先手方開始行動,之後雙方輪流行動。
每方每種棋子有6個,則每方一共有18個棋子。除了先手方的第一回合及最後一回合只放一個棋子之外,每方每回合要放兩個棋子。
先手方首回合將把他的一個棋子放至棋盤中心,此後每一方的回合中,他所放的每一個棋子都要滿足以下條件:
3.1,每個棋子落下時,它的其中一條邊要完全貼合另一個在場上已有的「對方棋子」的一條邊。
3.2,不能有另一條邊跟場上已有的棋子貼合,包括部分重合但不完全貼合的情況。
3.3,不能跟場上已有的棋子的面重疊。
3.4,這一回合所放的兩個棋子都一定要貼合到同一個「對方棋子」的兩個不同的邊上,除非符合以下兩點。
3.5,如果第一手棋在落下之後,形成了「有棋子的端點正好落在另一個棋子的邊上」的情況,則第二手棋可以落在任意一個「對方棋子」的邊上。
3.6,如果第二手棋在落下之後,可以形成「有棋子的端點正好落在另一個棋子的邊上」的情況,則允許這手棋不跟第一手棋落在同一個「對方棋子」的邊上。
以上「邊與邊是否貼合、端點是否正好落在另一個圖形的邊上、是否發生重疊」的判定邏輯都已經在「tiling-exact-geometry2.js」寫好,可以直接使用。
四,計分規則
4.1,當「有棋子a的端點正好落在另一個棋子b的邊上」時,將計分給a棋子所屬的一方。a為「頂鑫棋子」,b為「被頂棋子」。
4.2,從b沿邊接邊走到a,每經過一個棋子計一分,a和b本身不算。則經過多少個棋子就計多少分。
4.3,如果存在同一個棋子被兩個以上的棋子頂到的情況,只計最靠近的一個,或者說只計最先頂到的一個。
四,為棋子增加點擊事件及邏輯
當輸到一方行動時,點擊他的一個棋子,其外部的div的陰影和邊框將變色,表示這個棋子被選中。
當再次點擊一個被選中的棋子時,它將以y=0進行鏡像翻轉,實現方式是把它的transform中的scale的值改為(-1,1)。再點一次則又會翻回來。後手方的三個棋子在一局開始時就處於已翻轉狀態。
當先手方首回合點擊他的一個棋子時,他的棋子將以相同的鏡像狀態出現在棋盤中心,這時「確認按鈕」將被激活並可點擊,如果再次點擊控制區中的這個棋子,則它將翻轉,場上棋盤中心的棋子也跟著翻轉,如果點擊另一個棋子,則場上的棋子將換成另一個棋子。點擊「打叉」則清空棋盤,回到沒有棋子被選中的狀態,點擊「打勾」,則確認本次行動。
從後手方的首回合開始,落子將採用以下方式進行。
當點擊控制區的一個己方棋子,再點擊棋盤區中的一個對方棋子時,己方這個棋子的所有可能落下的情況會「預先」放到棋盤區內,這些預放棋子的邊緣是黑色不透明,但填色透明度為0.36。這些預放棋子之間可能會有部分面重疊。如果翻轉該棋子,則所有能落下的可能情況要重新判定。
當點擊到一個預放棋子不重疊的面時,則這個預放棋子將成為已放棋子。其它預放棋子將消失,己方控制區將取消該棋子的選中狀態。
這時第一手棋已經落好,將進行第二手棋。注意第二手棋的「可能落下情況」要加上規則中所說的「要在同一個對方棋子的另一個邊上」以及「兩個例外」。
有一個棋被選中時,「打叉」將激活並可以點擊,若點擊將把所有已放的棋子全部收回並取消所有選中棋子。當兩手棋都落好時,「打勾」將激活,如點擊則完成本回合行動。
棋子都用use[href=tile$][transform="translate(平移x,平移y) scale(1或-1,1) rotate(旋轉)"]來顯示(數值保留最多三位小數),加到svg的.etdrop中。
當場上有「對方棋子」被選中時,將在其中心位置加一個「半徑為3、stroke為none、fill為天藍色的圓」。當一方的回合在落子時,剛落的棋子中心將加一個「半徑為3、stroke為none、fill為白色的圓」,稱為白點。我方棋子的白點會在「打叉」時消失(上回合的對方棋子的白點又將顯示出來),但當打勾輪到對方時,白點不會消失,只有一方有棋子選中時,上回合的白點才會消失。
當最後一手棋落下後,將彈出窗口顯示總分和輸贏,同時提示「是否換先再來一局」,或「結束遊戲」。如果換先再來一局,則先手方會變為後手方(控制區的位置不變),兩局結束則直接計算總分並說明誰是贏家。
五,代碼要求
所有svg方位的數值最多保留三位小數。
所有的html、css和js代碼都寫在一個html中,包括「tiling-exact-geometry2.js」的代碼也要整合到<script>中。
一,頁面安排
頁面包括三個部分:先手方控制區,對後手方控制區,中間棋盤區。中間棋盤區佔據中間區域。另兩個控制區分佔另兩個區域。
如果是電腦端,中間棋盤區的高寬比為1:1,佔滿中間部分,剩餘的區域,右方是我方控制區,左方是對方控制區。控制區的最小寬度為240px,如果屏幕變窄則有可能擠壓中間區域,使之比例不再是1:1。
如果是手機端,中間棋盤區的高寬比為1:1,佔滿中間部分,剩餘的區域,下方是先手方控制區,上方是後手方控制區。控制區的最小高度為240px,如果屏幕過小則有可能擠壓中間區域,使之比例不再是1:1。
所上傳的svg將直接貼進中間棋盤區,它將是棋盤,高寬比將始終保持1:1,並始終垂直及水平居中。如果是電腦端,它離上下左右的邊距將為24px,如果是手機端,邊距將是7px。為棋盤增加懸浮陰影效果。
在控制區中,有分數區和棋子區以及確認區,每方有三種棋子,則五個元素大小佔位是相同的。如果是電腦端,則它們縱向排列並平分間距,先手方三種svg棋子的右方是它的數量,後手方的數量則標在左邊。如果是手機端,則它們横向排列並平分間距,三種svg棋子的下方是它的數量。分數區將居中顯示分數,數字下方是AI和人類的切換svg圖標。確認區有「打叉」和「打勾」兩個按鈕,初始是灰色未激活狀態。
雙方的三種svg棋子將取自所下面的svg的defs中,其名稱和id對應關係為:先手方棋子id=tile0、tile1、tile2,後手方棋子id=tile3、tile4、tile5。
<svg id="etanitrigolden" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg" width="480" height="480" viewBox="0 0 480 480" preserveAspectRatio="xMidYMid meet"> <defs> <g id="tile0" fill="#ffc2eb"><path d="M -61.687,17.119 L -14.563,-17.119 L 21.437,-17.119 L 32.562,17.119 Z"/></g><g id="tile1" fill="#ffc2eb"><path d="M -76.249,24.775 L -0.001,-30.623 L 29.124,-9.463 L 40.249,24.775 Z"/></g><g id="tile2" fill="#ffc2eb"><path d="M -55.468,19.14 L 2.781,-23.18 L 31.906,-2.02 L 2.781,19.14 Z"/></g><g id="tile3" fill="#96ffbe"><path d="M -61.687,17.119 L -14.563,-17.119 L 21.437,-17.119 L 32.562,17.119 Z"/></g><g id="tile4" fill="#96ffbe"><path d="M -76.249,24.775 L -0.001,-30.623 L 29.124,-9.463 L 40.249,24.775 Z"/></g><g id="tile5" fill="#96ffbe"><path d="M -55.468,19.14 L 2.781,-23.18 L 31.906,-2.02 L 2.781,19.14 Z"/></g></defs> <g class="etdrop" stroke="#777" stroke-width="2" stroke-linejoin="round" transform="translate(240,240) scale(1,1)" id="etanidrop"></g></svg>
每個棋子都在一個相同大小的矩形區域內,電腦端稍大而手機端稍小,為其增加陰影懸浮效果。
二,棋盤區域
當棋盤中沒有棋子時,其不可平移和縮放,當棋盤中至少存在一個棋子時,棋盤將可以平移和縮放。當遊戲結束到下一輪時,棋盤將清空並復原為初始的平移和縮放狀態。
所有的平移縮放都改變的是.etdrop的transform的值,保留最多三位小數。如果是手機端,則可用單指拖動平移,雙指縮放。如果是電腦端,則可用鼠標按住拖動平移,滾輪縮放。
棋盤區域內的下方部分將絕對定位一個工具欄,工具欄包含「旋轉、AI控制、後退、前進、導出、狀態」。後退和前進就是undo和redo,其它功能將在以後介紹。
當點擊棋盤的空白區域,則工具欄消失,出現平移和縮放按鈕,它們絕對定位並覆蓋在棋盤上,左下角和右上角是縮小,左上角和右下角是放大,正上方、正下方、正左方、正右方分別是上移下移左移右移,中間是恢復初始狀態(移回中間去掉縮放)。再點一次棋盤的空白區域,平移縮放按鈕消失,工具欄出現。
三,落子規則
先手方開始行動,之後雙方輪流行動。
每方每種棋子有6個,則每方一共有18個棋子。除了先手方的第一回合及最後一回合只放一個棋子之外,每方每回合要放兩個棋子。
先手方首回合將把他的一個棋子放至棋盤中心,此後每一方的回合中,他所放的每一個棋子都要滿足以下條件:
3.1,每個棋子落下時,它的其中一條邊要完全貼合另一個在場上已有的「對方棋子」的一條邊。
3.2,不能有另一條邊跟場上已有的棋子貼合,包括部分重合但不完全貼合的情況。
3.3,不能跟場上已有的棋子的面重疊。
3.4,這一回合所放的兩個棋子都一定要貼合到同一個「對方棋子」的兩個不同的邊上,除非符合以下兩點。
3.5,如果第一手棋在落下之後,形成了「有棋子的端點正好落在另一個棋子的邊上」的情況,則第二手棋可以落在任意一個「對方棋子」的邊上。
3.6,如果第二手棋在落下之後,可以形成「有棋子的端點正好落在另一個棋子的邊上」的情況,則允許這手棋不跟第一手棋落在同一個「對方棋子」的邊上。
以上「邊與邊是否貼合、端點是否正好落在另一個圖形的邊上、是否發生重疊」的判定邏輯都已經在「tiling-exact-geometry2.js」寫好,可以直接使用。
四,計分規則
4.1,當「有棋子a的端點正好落在另一個棋子b的邊上」時,將計分給a棋子所屬的一方。a為「頂鑫棋子」,b為「被頂棋子」。
4.2,從b沿邊接邊走到a,每經過一個棋子計一分,a和b本身不算。則經過多少個棋子就計多少分。
4.3,如果存在同一個棋子被兩個以上的棋子頂到的情況,只計最靠近的一個,或者說只計最先頂到的一個。
四,為棋子增加點擊事件及邏輯
當輸到一方行動時,點擊他的一個棋子,其外部的div的陰影和邊框將變色,表示這個棋子被選中。
當再次點擊一個被選中的棋子時,它將以y=0進行鏡像翻轉,實現方式是把它的transform中的scale的值改為(-1,1)。再點一次則又會翻回來。後手方的三個棋子在一局開始時就處於已翻轉狀態。
當先手方首回合點擊他的一個棋子時,他的棋子將以相同的鏡像狀態出現在棋盤中心,這時「確認按鈕」將被激活並可點擊,如果再次點擊控制區中的這個棋子,則它將翻轉,場上棋盤中心的棋子也跟著翻轉,如果點擊另一個棋子,則場上的棋子將換成另一個棋子。點擊「打叉」則清空棋盤,回到沒有棋子被選中的狀態,點擊「打勾」,則確認本次行動。
從後手方的首回合開始,落子將採用以下方式進行。
當點擊控制區的一個己方棋子,再點擊棋盤區中的一個對方棋子時,己方這個棋子的所有可能落下的情況會「預先」放到棋盤區內,這些預放棋子的邊緣是黑色不透明,但填色透明度為0.36。這些預放棋子之間可能會有部分面重疊。如果翻轉該棋子,則所有能落下的可能情況要重新判定。
當點擊到一個預放棋子不重疊的面時,則這個預放棋子將成為已放棋子。其它預放棋子將消失,己方控制區將取消該棋子的選中狀態。
這時第一手棋已經落好,將進行第二手棋。注意第二手棋的「可能落下情況」要加上規則中所說的「要在同一個對方棋子的另一個邊上」以及「兩個例外」。
有一個棋被選中時,「打叉」將激活並可以點擊,若點擊將把所有已放的棋子全部收回並取消所有選中棋子。當兩手棋都落好時,「打勾」將激活,如點擊則完成本回合行動。
棋子都用use[href=tile$][transform="translate(平移x,平移y) scale(1或-1,1) rotate(旋轉)"]來顯示(數值保留最多三位小數),加到svg的.etdrop中。
當場上有「對方棋子」被選中時,將在其中心位置加一個「半徑為3、stroke為none、fill為天藍色的圓」。當一方的回合在落子時,剛落的棋子中心將加一個「半徑為3、stroke為none、fill為白色的圓」,稱為白點。我方棋子的白點會在「打叉」時消失(上回合的對方棋子的白點又將顯示出來),但當打勾輪到對方時,白點不會消失,只有一方有棋子選中時,上回合的白點才會消失。
當最後一手棋落下後,將彈出窗口顯示總分和輸贏,同時提示「是否換先再來一局」,或「結束遊戲」。如果換先再來一局,則先手方會變為後手方(控制區的位置不變),兩局結束則直接計算總分並說明誰是贏家。
五,代碼要求
所有svg方位的數值最多保留三位小數。
所有的html、css和js代碼都寫在一個html中,包括「tiling-exact-geometry2.js」的代碼也要整合到<script>中。
代码: 全选
《頂鑫棋(TriGolden)》是一款二人抽象策略遊戲,為其製作一個基於svg的html5弈棋遊戲:
一,頁面安排
頁面包括三個部分:先手方控制區,對後手方控制區,中間棋盤區。中間棋盤區佔據中間區域。另兩個控制區分佔另兩個區域。
如果是電腦端,中間棋盤區的高寬比為1:1,佔滿中間部分,剩餘的區域,右方是我方控制區,左方是對方控制區。控制區的最小寬度為240px,如果屏幕變窄則有可能擠壓中間區域,使之比例不再是1:1。
如果是手機端,中間棋盤區的高寬比為1:1,佔滿中間部分,剩餘的區域,下方是先手方控制區,上方是後手方控制區。控制區的最小高度為240px,如果屏幕過小則有可能擠壓中間區域,使之比例不再是1:1。
所上傳的svg將直接貼進中間棋盤區,它將是棋盤,高寬比將始終保持1:1,並始終垂直及水平居中。如果是電腦端,它離上下左右的邊距將為24px,如果是手機端,邊距將是7px。為棋盤增加懸浮陰影效果。
在控制區中,有分數區和棋子區以及確認區,每方有三種棋子,則五個元素大小佔位是相同的。如果是電腦端,則它們縱向排列並平分間距,先手方三種svg棋子的右方是它的數量,後手方的數量則標在左邊。如果是手機端,則它們横向排列並平分間距,三種svg棋子的下方是它的數量。分數區將居中顯示分數,數字下方是AI和人類的切換svg圖標。確認區有「打叉」和「打勾」兩個按鈕,初始是灰色未激活狀態。
雙方的三種svg棋子將取自所下面的svg的defs中,其名稱和id對應關係為:先手方棋子id=tile0、tile1、tile2,後手方棋子id=tile3、tile4、tile5。
<svg id="etanitrigolden" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg" width="480" height="480" viewBox="0 0 480 480" preserveAspectRatio="xMidYMid meet"> <defs> <g id="tile0" fill="#ffc2eb"><path d="M -61.687,17.119 L -14.563,-17.119 L 21.437,-17.119 L 32.562,17.119 Z"/></g><g id="tile1" fill="#ffc2eb"><path d="M -76.249,24.775 L -0.001,-30.623 L 29.124,-9.463 L 40.249,24.775 Z"/></g><g id="tile2" fill="#ffc2eb"><path d="M -55.468,19.14 L 2.781,-23.18 L 31.906,-2.02 L 2.781,19.14 Z"/></g><g id="tile3" fill="#96ffbe"><path d="M -61.687,17.119 L -14.563,-17.119 L 21.437,-17.119 L 32.562,17.119 Z"/></g><g id="tile4" fill="#96ffbe"><path d="M -76.249,24.775 L -0.001,-30.623 L 29.124,-9.463 L 40.249,24.775 Z"/></g><g id="tile5" fill="#96ffbe"><path d="M -55.468,19.14 L 2.781,-23.18 L 31.906,-2.02 L 2.781,19.14 Z"/></g></defs> <g class="etdrop" stroke="#777" stroke-width="2" stroke-linejoin="round" transform="translate(240,240) scale(1,1)" id="etanidrop"></g></svg>
每個棋子都在一個相同大小的矩形區域內,電腦端稍大而手機端稍小,為其增加陰影懸浮效果。
二,棋盤區域
當棋盤中沒有棋子時,其不可平移和縮放,當棋盤中至少存在一個棋子時,棋盤將可以平移和縮放。當遊戲結束到下一輪時,棋盤將清空並復原為初始的平移和縮放狀態。
所有的平移縮放都改變的是.etdrop的transform的值,保留最多三位小數。如果是手機端,則可用單指拖動平移,雙指縮放。如果是電腦端,則可用鼠標按住拖動平移,滾輪縮放。
棋盤區域內的下方部分將絕對定位一個工具欄,工具欄包含「旋轉、AI控制、後退、前進、導出、狀態」。後退和前進就是undo和redo,其它功能將在以後介紹。
當點擊棋盤的空白區域,則工具欄消失,出現平移和縮放按鈕,它們絕對定位並覆蓋在棋盤上,左下角和右上角是縮小,左上角和右下角是放大,正上方、正下方、正左方、正右方分別是上移下移左移右移,中間是恢復初始狀態(移回中間去掉縮放)。再點一次棋盤的空白區域,平移縮放按鈕消失,工具欄出現。
三,落子規則
先手方開始行動,之後雙方輪流行動。
每方每種棋子有6個,則每方一共有18個棋子。除了先手方的第一回合及最後一回合只放一個棋子之外,每方每回合要放兩個棋子。
先手方首回合將把他的一個棋子放至棋盤中心,此後每一方的回合中,他所放的每一個棋子都要滿足以下條件:
3.1,每個棋子落下時,它的其中一條邊要完全貼合另一個在場上已有的「對方棋子」的一條邊。
3.2,不能有另一條邊跟場上已有的棋子貼合,包括部分重合但不完全貼合的情況。
3.3,不能跟場上已有的棋子的面重疊。
3.4,這一回合所放的兩個棋子都一定要貼合到同一個「對方棋子」的兩個不同的邊上,除非符合以下兩點。
3.5,如果第一手棋在落下之後,形成了「有棋子的端點正好落在另一個棋子的邊上」的情況,則第二手棋可以落在任意一個「對方棋子」的邊上。
3.6,如果第二手棋在落下之後,可以形成「有棋子的端點正好落在另一個棋子的邊上」的情況,則允許這手棋不跟第一手棋落在同一個「對方棋子」的邊上。
以上「邊與邊是否貼合、端點是否正好落在另一個圖形的邊上、是否發生重疊」的判定邏輯都已經在「tiling-exact-geometry2.js」寫好,可以直接使用。
四,計分規則
4.1,當「有棋子a的端點正好落在另一個棋子b的邊上」時,將計分給a棋子所屬的一方。a為「頂鑫棋子」,b為「被頂棋子」。
4.2,從b沿邊接邊走到a,每經過一個棋子計一分,a和b本身不算。則經過多少個棋子就計多少分。
4.3,如果存在同一個棋子被兩個以上的棋子頂到的情況,只計最靠近的一個,或者說只計最先頂到的一個。
四,為棋子增加點擊事件及邏輯
當輸到一方行動時,點擊他的一個棋子,其外部的div的陰影和邊框將變色,表示這個棋子被選中。
當再次點擊一個被選中的棋子時,它將以y=0進行鏡像翻轉,實現方式是把它的transform中的scale的值改為(-1,1)。再點一次則又會翻回來。後手方的三個棋子在一局開始時就處於已翻轉狀態。
當先手方首回合點擊他的一個棋子時,他的棋子將以相同的鏡像狀態出現在棋盤中心,這時「確認按鈕」將被激活並可點擊,如果再次點擊控制區中的這個棋子,則它將翻轉,場上棋盤中心的棋子也跟著翻轉,如果點擊另一個棋子,則場上的棋子將換成另一個棋子。點擊「打叉」則清空棋盤,回到沒有棋子被選中的狀態,點擊「打勾」,則確認本次行動。
從後手方的首回合開始,落子將採用以下方式進行。
當點擊控制區的一個己方棋子,再點擊棋盤區中的一個對方棋子時,己方這個棋子的所有可能落下的情況會「預先」放到棋盤區內,這些預放棋子的邊緣是黑色不透明,但填色透明度為0.36。這些預放棋子之間可能會有部分面重疊。如果翻轉該棋子,則所有能落下的可能情況要重新判定。
當點擊到一個預放棋子不重疊的面時,則這個預放棋子將成為已放棋子。其它預放棋子將消失,己方控制區將取消該棋子的選中狀態。
這時第一手棋已經落好,將進行第二手棋。注意第二手棋的「可能落下情況」要加上規則中所說的「要在同一個對方棋子的另一個邊上」以及「兩個例外」。
有一個棋被選中時,「打叉」將激活並可以點擊,若點擊將把所有已放的棋子全部收回並取消所有選中棋子。當兩手棋都落好時,「打勾」將激活,如點擊則完成本回合行動。
棋子都用use[href=tile$][transform="translate(平移x,平移y) scale(1或-1,1) rotate(旋轉)"]來顯示(數值保留最多三位小數),加到svg的.etdrop中。
當場上有「對方棋子」被選中時,將在其中心位置加一個「半徑為3、stroke為none、fill為天藍色的圓」。當一方的回合在落子時,剛落的棋子中心將加一個「半徑為3、stroke為none、fill為白色的圓」,稱為白點。我方棋子的白點會在「打叉」時消失(上回合的對方棋子的白點又將顯示出來),但當打勾輪到對方時,白點不會消失,只有一方有棋子選中時,上回合的白點才會消失。
當最後一手棋落下後,將彈出窗口顯示總分和輸贏,同時提示「是否換先再來一局」,或「結束遊戲」。如果換先再來一局,則先手方會變為後手方(控制區的位置不變),兩局結束則直接計算總分並說明誰是贏家。
五,代碼要求
所有svg方位的數值最多保留三位小數。
所有的html、css和js代碼都寫在一個html中,包括「tiling-exact-geometry2.js」的代碼也要整合到<script>中。- 附件
-
tiling-exact-geometry2.js.7z- (8.11 KiB) 已下载 31 次
https://ejsoon.vip/
弈趣極光:享受思維樂趣
弈趣極光:享受思維樂趣
Re: 一個新的遊戲創意
gemini 3.1 pro + thinking
代码: 全选
這是一項非常龐大且複雜的工程。你提供的 `tiling-exact-geometry2.js` 是一套極其精妙且嚴謹的代數幾何引擎,使用精確的環代數(Ring Arithmetic)來避免任何浮點數誤差,這對這種基於黃金分割率的非週期性拼圖遊戲(如潘洛斯拼圖)來說是最完美的解決方案。
為了達成你的需求,我將這套幾何引擎與完整的 HTML5、CSS 以及遊戲邏輯整合到一個單一的 HTML 文件中。這套代碼實現了你要求的響應式佈局(電腦/手機端自動切換)、SVG 陰影效果、基於 `use` 的棋子複用、回合制邏輯、棋盤平移縮放,以及複雜的「邊貼合」、「頂點碰觸(頂)」及「沿邊尋路計分」判定。
請將以下代碼保存為 `.html` 文件,然後在現代瀏覽器中打開即可體驗:
```html
<!DOCTYPE html>
<html lang="zh-HK">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>鑫頂棋 (TriGolden)</title>
<style>
:root {
--bg-color: #f0f2f5;
--panel-bg: #ffffff;
--text-color: #333;
--active-shadow: 0 0 10px 4px #ffaa00;
}
body {
margin: 0;
padding: 0;
width: 100vw;
height: 100vh;
overflow: hidden;
display: flex;
flex-direction: row;
background-color: var(--bg-color);
font-family: sans-serif;
user-select: none;
-webkit-user-select: none;
}
/* 佈局:默認為電腦端(左右兩側為控制區,中間為棋盤) */
#p2-panel { order: 1; }
#board-area { order: 2; flex: 1; display: flex; align-items: center; justify-content: center; position: relative; padding: 24px; box-sizing: border-box; }
#p1-panel { order: 3; }
.control-panel {
min-width: 240px;
background: var(--panel-bg);
box-shadow: 0 0 15px rgba(0,0,0,0.1);
display: flex;
flex-direction: column;
justify-content: space-evenly;
align-items: center;
z-index: 10;
}
/* 中間棋盤限制為 1:1 */
#board-wrapper {
aspect-ratio: 1;
max-width: 100%;
max-height: 100%;
position: relative;
display: flex;
align-items: center;
justify-content: center;
}
#etanitrigolden {
width: 100%;
height: 100%;
filter: drop-shadow(0px 8px 16px rgba(0,0,0,0.3));
background: #e0e5ec;
border-radius: 8px;
}
/* 手機端佈局:上下為控制區,中間為棋盤 */
@media (max-width: 768px) {
body { flex-direction: column; }
.control-panel { min-width: auto; min-height: 240px; flex-direction: row; }
#board-area { padding: 7px; }
#p2-panel { order: 1; }
#board-area { order: 2; }
#p1-panel { order: 3; }
}
/* 控制區內部元素 */
.panel-section { display: flex; flex-direction: column; align-items: center; gap: 8px; }
.piece-container {
width: 60px; height: 60px;
background: #fff;
border: 2px solid #ccc;
border-radius: 8px;
display: flex; align-items: center; justify-content: center;
transition: all 0.2s;
cursor: pointer;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
}
@media (max-width: 768px) { .piece-container { width: 50px; height: 50px; } }
.piece-container.selected { border-color: #ffaa00; box-shadow: var(--active-shadow); }
.piece-count { font-weight: bold; font-size: 1.2rem; }
.flex-row-mobile { display: flex; align-items: center; gap: 10px; }
@media (max-width: 768px) { .flex-row-mobile { flex-direction: column; } }
.score-board { font-size: 2rem; font-weight: bold; color: var(--text-color); text-align: center; }
.btn-icon { width: 40px; height: 40px; cursor: pointer; opacity: 0.3; transition: 0.2s; pointer-events: none; }
.btn-icon.active { opacity: 1; pointer-events: auto; }
.btn-icon:hover.active { transform: scale(1.1); }
/* 工具欄與平移縮放控制 */
.toolbar {
position: absolute; bottom: 10px;
background: rgba(255,255,255,0.9);
padding: 5px 15px; border-radius: 20px;
display: flex; gap: 15px;
box-shadow: 0 2px 10px rgba(0,0,0,0.2);
transition: opacity 0.3s;
}
.toolbar-btn { cursor: pointer; font-size: 14px; font-weight: bold; padding: 5px; }
.nav-controls {
position: absolute; top: 0; left: 0; right: 0; bottom: 0;
pointer-events: none; display: none;
}
.nav-btn {
position: absolute; width: 40px; height: 40px;
background: rgba(0,0,0,0.5); color: white; border-radius: 50%;
display: flex; align-items: center; justify-content: center;
pointer-events: auto; cursor: pointer; font-weight: bold;
}
.nav-up { top: 10px; left: 50%; transform: translateX(-50%); }
.nav-down { bottom: 10px; left: 50%; transform: translateX(-50%); }
.nav-left { left: 10px; top: 50%; transform: translateY(-50%); }
.nav-right { right: 10px; top: 50%; transform: translateY(-50%); }
.nav-zoom-in { right: 10px; bottom: 10px; }
.nav-zoom-out { left: 10px; bottom: 10px; }
.nav-reset { top: 50%; left: 50%; transform: translate(-50%, -50%); }
</style>
</head>
<body>
<div id="p2-panel" class="control-panel">
<div class="panel-section">
<div class="score-board" id="p2-score">0</div>
<div style="font-size: 0.8rem">P2 (AI/人類)</div>
</div>
<div class="panel-section flex-row-mobile">
<span class="piece-count" id="p2-count-0">6</span>
<div class="piece-container" id="p2-piece-0" onclick="selectHandPiece('P2', 0)"><svg width="40" height="40" viewBox="-80 -40 120 80"><use href="#tile3" transform="scale(-1,1)"/></svg></div>
</div>
<div class="panel-section flex-row-mobile">
<span class="piece-count" id="p2-count-1">6</span>
<div class="piece-container" id="p2-piece-1" onclick="selectHandPiece('P2', 1)"><svg width="40" height="40" viewBox="-80 -40 120 80"><use href="#tile4" transform="scale(-1,1)"/></svg></div>
</div>
<div class="panel-section flex-row-mobile">
<span class="piece-count" id="p2-count-2">6</span>
<div class="piece-container" id="p2-piece-2" onclick="selectHandPiece('P2', 2)"><svg width="40" height="40" viewBox="-80 -40 120 80"><use href="#tile5" transform="scale(-1,1)"/></svg></div>
</div>
<div class="panel-section flex-row-mobile" style="flex-direction: row; gap: 20px;">
<svg class="btn-icon" id="p2-btn-cancel" onclick="cancelAction()" viewBox="0 0 24 24" fill="none" stroke="red" stroke-width="3" stroke-linecap="round"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>
<svg class="btn-icon" id="p2-btn-confirm" onclick="confirmAction()" viewBox="0 0 24 24" fill="none" stroke="green" stroke-width="3" stroke-linecap="round"><polyline points="20 6 9 17 4 12"></polyline></svg>
</div>
</div>
<div id="board-area">
<div id="board-wrapper">
<svg id="etanitrigolden" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg" width="480" height="480" viewBox="0 0 480 480" preserveAspectRatio="xMidYMid meet">
<defs>
<g id="tile0" fill="#ffc2eb"><path d="M -61.687,17.119 L -14.563,-17.119 L 21.437,-17.119 L 32.562,17.119 Z"/></g>
<g id="tile1" fill="#ffc2eb"><path d="M -76.249,24.775 L -0.001,-30.623 L 29.124,-9.463 L 40.249,24.775 Z"/></g>
<g id="tile2" fill="#ffc2eb"><path d="M -55.468,19.14 L 2.781,-23.18 L 31.906,-2.02 L 2.781,19.14 Z"/></g>
<g id="tile3" fill="#96ffbe"><path d="M -61.687,17.119 L -14.563,-17.119 L 21.437,-17.119 L 32.562,17.119 Z"/></g>
<g id="tile4" fill="#96ffbe"><path d="M -76.249,24.775 L -0.001,-30.623 L 29.124,-9.463 L 40.249,24.775 Z"/></g>
<g id="tile5" fill="#96ffbe"><path d="M -55.468,19.14 L 2.781,-23.18 L 31.906,-2.02 L 2.781,19.14 Z"/></g>
<circle id="dot-blue" cx="0" cy="0" r="3" fill="#00bfff" stroke="none" />
<circle id="dot-white" cx="0" cy="0" r="3" fill="#ffffff" stroke="none" />
</defs>
<rect width="100%" height="100%" fill="transparent" id="bg-clicker" />
<g class="etdrop" stroke="#777" stroke-width="2" stroke-linejoin="round" transform="translate(240,240) scale(1,1)" id="etanidrop">
<g id="pieces-layer"></g>
<g id="ghosts-layer"></g>
<g id="markers-layer"></g>
</g>
</svg>
<div class="toolbar" id="toolbar">
<div class="toolbar-btn">旋轉</div>
<div class="toolbar-btn">AI控制</div>
<div class="toolbar-btn">後退</div>
<div class="toolbar-btn">前進</div>
<div class="toolbar-btn">導出</div>
<div class="toolbar-btn" id="status-text">狀態: P1 回合</div>
</div>
<div class="nav-controls" id="nav-controls">
<div class="nav-btn nav-up" onclick="panBy(0, 50)">↑</div>
<div class="nav-btn nav-down" onclick="panBy(0, -50)">↓</div>
<div class="nav-btn nav-left" onclick="panBy(50, 0)">←</div>
<div class="nav-btn nav-right" onclick="panBy(-50, 0)">→</div>
<div class="nav-btn nav-zoom-in" onclick="zoomBy(1.2)">+</div>
<div class="nav-btn nav-zoom-out" onclick="zoomBy(0.8)">-</div>
<div class="nav-btn nav-reset" onclick="resetView()">O</div>
</div>
</div>
</div>
<div id="p1-panel" class="control-panel">
<div class="panel-section flex-row-mobile">
<div class="piece-container" id="p1-piece-0" onclick="selectHandPiece('P1', 0)"><svg width="40" height="40" viewBox="-80 -40 120 80"><use href="#tile0" transform="scale(1,1)"/></svg></div>
<span class="piece-count" id="p1-count-0">6</span>
</div>
<div class="panel-section flex-row-mobile">
<div class="piece-container" id="p1-piece-1" onclick="selectHandPiece('P1', 1)"><svg width="40" height="40" viewBox="-80 -40 120 80"><use href="#tile1" transform="scale(1,1)"/></svg></div>
<span class="piece-count" id="p1-count-1">6</span>
</div>
<div class="panel-section flex-row-mobile">
<div class="piece-container" id="p1-piece-2" onclick="selectHandPiece('P1', 2)"><svg width="40" height="40" viewBox="-80 -40 120 80"><use href="#tile2" transform="scale(1,1)"/></svg></div>
<span class="piece-count" id="p1-count-2">6</span>
</div>
<div class="panel-section flex-row-mobile" style="flex-direction: row; gap: 20px;">
<svg class="btn-icon" id="p1-btn-cancel" onclick="cancelAction()" viewBox="0 0 24 24" fill="none" stroke="red" stroke-width="3" stroke-linecap="round"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>
<svg class="btn-icon" id="p1-btn-confirm" onclick="confirmAction()" viewBox="0 0 24 24" fill="none" stroke="green" stroke-width="3" stroke-linecap="round"><polyline points="20 6 9 17 4 12"></polyline></svg>
</div>
<div class="panel-section">
<div class="score-board" id="p1-score">0</div>
<div style="font-size: 0.8rem">P1 (AI/人類)</div>
</div>
</div>
<script>
// =============================================================================
// 1. EXACT ALGEBRAIC COORDINATE SYSTEM (提供的幾何引擎,微調適應前端)
// =============================================================================
const ZERO = [0, 0, 0, 0];
const ONE = [1, 0, 0, 0];
const ZETA_POW = [
[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 1, 0], [0, 0, 0, 1], [-1, 1, -1, 1],
[-1, 0, 0, 0], [0, -1, 0, 0], [0, 0, -1, 0], [0, 0, 0, -1], [1, -1, 1, -1]
];
function zetaPow(k) { return ZETA_POW[((k % 10) + 10) % 10]; }
function ringAdd(p, q) { return [p[0]+q[0], p[1]+q[1], p[2]+q[2], p[3]+q[3]]; }
function ringSub(p, q) { return [p[0]-q[0], p[1]-q[1], p[2]-q[2], p[3]-q[3]]; }
function ringScale(p, k) { return [p[0]*k, p[1]*k, p[2]*k, p[3]*k]; }
function ringMul(p, q) {
const [p0, p1, p2, p3] = p; const [q0, q1, q2, q3] = q;
const c4 = p1*q3 + p2*q2 + p3*q1;
const c5 = p2*q3 + p3*q2;
const c6 = p3*q3;
let res = [p0*q0, p0*q1+p1*q0, p0*q2+p1*q1+p2*q0, p0*q3+p1*q2+p2*q1+p3*q0];
res = ringAdd(res, ringScale(ZETA_POW[4], c4));
res = ringAdd(res, ringScale(ZETA_POW[5], c5));
res = ringAdd(res, ringScale(ZETA_POW[6], c6));
return res;
}
function conjugate([a, b, c, d]) {
let res = ringScale(ONE, a);
res = ringAdd(res, ringScale(ZETA_POW[9], b));
res = ringAdd(res, ringScale(ZETA_POW[8], c));
res = ringAdd(res, ringScale(ZETA_POW[7], d));
return res;
}
function pointsEqual(p, q) { return p[0]===q[0] && p[1]===q[1] && p[2]===q[2] && p[3]===q[3]; }
function isImaginaryPartZero([, b, c, d]) { return b === 0 && c + d === 0; }
function realPartExact([a, b, c, d]) { return { intPart: 2*a + c - d, phiPart: b + c - d }; }
const PHI_NUMERIC = (1 + Math.sqrt(5)) / 2;
function phiFormToNumber({ intPart, phiPart }) { return (intPart + phiPart * PHI_NUMERIC) / 2; }
const PHI = ringAdd(ZETA_POW[1], ZETA_POW[9]);
const PHI2 = ringMul(PHI, PHI);
const TWO = ringScale(ONE, 2);
const TWO_PHI = ringScale(PHI, 2);
const LENGTHS = { 1: ONE, 2: TWO, phi: PHI, phi2: PHI2, twoPhi: TWO_PHI };
function edgeVector(lengthKey, angleIndex) { return ringMul(LENGTHS[lengthKey], zetaPow(angleIndex)); }
const SHAPES = {
A: { edges: [{length:'phi2',turn:2},{length:1,turn:1},{length:1,turn:3},{length:'twoPhi',turn:4}] },
B: { edges: [{length:'phi',turn:1},{length:1,turn:2},{length:1,turn:3},{length:'phi2',turn:4}] },
C: { edges: [{length:2,turn:2},{length:1,turn:3},{length:1,turn:1},{length:'phi',turn:4}] }
};
function buildShapeVertices(origin, startAngle, template, mirrored = false) {
const vertices = [origin]; let pos = origin; let angle = startAngle;
for (let i = 0; i < template.edges.length; i++) {
const edge = template.edges[i];
pos = ringAdd(pos, edgeVector(edge.length, angle));
angle += mirrored ? -edge.turn : edge.turn;
if (i < template.edges.length - 1) vertices.push(pos);
}
return { vertices, closureCheck: pos };
}
function attachByEdge(templateKey, mirrored, edgeIndexToAlign, worldP, worldQ) {
const template = SHAPES[templateKey];
const { vertices: localVerts } = buildShapeVertices(ZERO, 0, template, mirrored);
const i = edgeIndexToAlign; const j = (i + 1) % localVerts.length;
const localEdgeVec = ringSub(localVerts[j], localVerts[i]);
const targetVec = ringSub(worldP, worldQ);
let foundK = null;
for (let k = 0; k < 10; k++) {
if (pointsEqual(ringMul(localEdgeVec, zetaPow(k)), targetVec)) { foundK = k; break; }
}
if (foundK === null) return null;
const worldVerts = localVerts.map(v => ringAdd(worldQ, ringMul(ringSub(v, localVerts[i]), zetaPow(foundK))));
return { vertices: worldVerts, rotation: foundK, mirrored };
}
function pointOnOpenSegment(P, S, E) {
if (pointsEqual(P, S) || pointsEqual(P, E)) return false;
const v = ringSub(E, S); const w = ringSub(P, S);
const cross = ringMul(conjugate(v), w);
if (!isImaginaryPartZero(cross)) return false;
const t = phiFormToNumber(realPartExact(ringMul(w, conjugate(v)))) / phiFormToNumber(realPartExact(ringMul(v, conjugate(v))));
return t > 0 && t < 1;
}
function exactOrientation(S, E, P) {
const crossElem = ringMul(conjugate(ringSub(E, S)), ringSub(P, S));
if (isImaginaryPartZero(crossElem)) return 0;
return (crossElem[1] + (crossElem[2] + crossElem[3]) * PHI_NUMERIC) > 0 ? 1 : -1;
}
function segmentsCrossStrict(S1, E1, S2, E2) {
const o1 = exactOrientation(S1, E1, S2), o2 = exactOrientation(S1, E1, E2);
const o3 = exactOrientation(S2, E2, S1), o4 = exactOrientation(S2, E2, E1);
return o1 !== o2 && o1 !== 0 && o2 !== 0 && o3 !== o4 && o3 !== 0 && o4 !== 0;
}
function pointStrictlyInConvexPolygon(P, vertices) {
let expSign = 0; const n = vertices.length;
for (let i = 0; i < n; i++) {
const o = exactOrientation(vertices[i], vertices[(i + 1) % n], P);
if (o === 0) return false;
if (expSign === 0) expSign = o; else if (o !== expSign) return false;
}
return true;
}
function segmentsPerfectlyMatch(S1, E1, S2, E2) {
return (pointsEqual(S1, S2) && pointsEqual(E1, E2)) || (pointsEqual(S1, E2) && pointsEqual(E1, S2));
}
function segmentsOverlap1D(S1, E1, S2, E2) {
if (segmentsPerfectlyMatch(S1, E1, S2, E2)) return true;
return pointOnOpenSegment(S1, S2, E2) || pointOnOpenSegment(E1, S2, E2) || pointOnOpenSegment(S2, S1, E1) || pointOnOpenSegment(E2, S1, E1);
}
function shapesOverlap(vertsA, vertsB) {
const n = vertsA.length, m = vertsB.length;
for (let i=0; i<n; i++) for (let j=0; j<m; j++) {
if (segmentsCrossStrict(vertsA[i], vertsA[(i+1)%n], vertsB[j], vertsB[(j+1)%m])) return true;
}
if (pointStrictlyInConvexPolygon(vertsA[0], vertsB) || pointStrictlyInConvexPolygon(vertsB[0], vertsA)) return true;
return false;
}
function shapesHaveEdgesTouching(vertsA, vertsB) {
const n = vertsA.length, m = vertsB.length;
for (let i=0; i<n; i++) for (let j=0; j<m; j++) {
if (segmentsOverlap1D(vertsA[i], vertsA[(i+1)%n], vertsB[j], vertsB[(j+1)%m])) return true;
}
return false;
}
// 用於渲染的座標轉換(將環代數座標轉為 SVG float,保留三位小數)
// 基準比例:以圖形在原點的 1 單位長度為 30px
const SCALE_FACTOR = 30;
function ringToPixel([a, b, c, d]) {
// x + iy = a + b*z + c*z^2 + d*z^3. z = cos(36) + i*sin(36)
const z1x = Math.cos(Math.PI/5), z1y = Math.sin(Math.PI/5);
const z2x = Math.cos(2*Math.PI/5), z2y = Math.sin(2*Math.PI/5);
const z3x = Math.cos(3*Math.PI/5), z3y = Math.sin(3*Math.PI/5);
let x = a + b*z1x + c*z2x + d*z3x;
let y = b*z1y + c*z2y + d*z3y;
return { x: (x * SCALE_FACTOR).toFixed(3), y: (y * SCALE_FACTOR).toFixed(3) };
}
// 從旋轉指數(0..9)獲取角度度數
function getRotationDeg(rotIndex) { return (rotIndex * 36) % 360; }
// =============================================================================
// 2. 遊戲狀態與邏輯
// =============================================================================
let gameState = {
currentPlayer: 'P1', // 'P1' 或 'P2'
piecesLeft: { P1: [6,6,6], P2: [6,6,6] },
scores: { P1: 0, P2: 0 },
boardPieces: [], // { id, owner, shapeIdx, vertices, rotation, mirrored, center }
turnPhase: 0, // 0: 首局下第一顆, 1: 正常回合第一顆, 2: 正常回合第二顆
selectedHand: null, // { owner, shapeIdx, mirrorState }
selectedBoardPieceId: null,
stagedPieces: [], // 當前回合已放下的暫存棋子
ghosts: [], // 預選位置
firstTargetOpponentId: null // 回合第一顆附著的對手棋子ID
};
const SHAPE_KEYS = ['A', 'B', 'C'];
let viewTransform = { x: 240, y: 240, scale: 1 };
let isToolbarVisible = true;
// 初始化 UI
function updateUI() {
for(let i=0; i<3; i++) {
document.getElementById(`p1-count-${i}`).innerText = gameState.piecesLeft.P1[i];
document.getElementById(`p2-count-${i}`).innerText = gameState.piecesLeft.P2[i];
document.getElementById(`p1-piece-${i}`).classList.remove('selected');
document.getElementById(`p2-piece-${i}`).classList.remove('selected');
}
document.getElementById('p1-score').innerText = gameState.scores.P1;
document.getElementById('p2-score').innerText = gameState.scores.P2;
document.getElementById('status-text').innerText = `狀態: ${gameState.currentPlayer} 回合`;
const canConfirm = gameState.stagedPieces.length > 0 &&
(gameState.boardPieces.length === 0 ? gameState.stagedPieces.length === 1 : gameState.stagedPieces.length === 2);
document.getElementById(`${gameState.currentPlayer.toLowerCase()}-btn-confirm`).classList.toggle('active', canConfirm);
document.getElementById(`${gameState.currentPlayer.toLowerCase()}-btn-cancel`).classList.toggle('active', gameState.selectedHand || gameState.stagedPieces.length > 0);
// 渲染棋盤
renderBoard();
}
// 選擇手牌
function selectHandPiece(player, shapeIdx) {
if (player !== gameState.currentPlayer) return;
if (gameState.piecesLeft[player][shapeIdx] <= 0) return;
// 如果點擊已經選中的,翻轉它
if (gameState.selectedHand && gameState.selectedHand.shapeIdx === shapeIdx) {
gameState.selectedHand.mirrorState = !gameState.selectedHand.mirrorState;
const useEl = document.querySelector(`#${player.toLowerCase()}-piece-${shapeIdx} use`);
useEl.setAttribute('transform', `scale(${gameState.selectedHand.mirrorState ? -1 : 1}, 1)`);
} else {
gameState.selectedHand = { owner: player, shapeIdx: shapeIdx, mirrorState: player === 'P2' };
}
updateUI();
document.getElementById(`${player.toLowerCase()}-piece-${shapeIdx}`).classList.add('selected');
// 首回合:直接放到中間
if (gameState.boardPieces.length === 0 && gameState.stagedPieces.length === 0) {
generateCenterGhost();
} else if (gameState.selectedBoardPieceId !== null) {
generateGhostsForTarget(gameState.selectedBoardPieceId);
}
}
// 首子放中間
function generateCenterGhost() {
gameState.ghosts = [];
const shapeKey = SHAPE_KEYS[gameState.selectedHand.shapeIdx];
const { vertices } = buildShapeVertices(ZERO, 0, SHAPES[shapeKey], gameState.selectedHand.mirrorState);
gameState.ghosts.push({
shapeIdx: gameState.selectedHand.shapeIdx,
mirrored: gameState.selectedHand.mirrorState,
vertices: vertices,
rotation: 0,
tx: 0, ty: 0
});
renderBoard();
}
// 選擇場上的棋子
function selectBoardPiece(id) {
const piece = gameState.boardPieces.find(p => p.id === id);
if (!piece || piece.owner === gameState.currentPlayer) return; // 只能點擊對手棋子
if (!gameState.selectedHand) return; // 必須先選中手牌
gameState.selectedBoardPieceId = id;
generateGhostsForTarget(id);
renderBoard();
}
// 計算合法預放位置
function generateGhostsForTarget(targetId) {
gameState.ghosts = [];
if (!gameState.selectedHand) return;
const targetPiece = gameState.boardPieces.find(p => p.id === targetId);
if (!targetPiece) return;
const shapeKey = SHAPE_KEYS[gameState.selectedHand.shapeIdx];
const targetVerts = targetPiece.vertices;
for (let oppEdge = 0; oppEdge < targetVerts.length; oppEdge++) {
for (let handEdge = 0; handEdge < SHAPES[shapeKey].edges.length; handEdge++) {
const worldP = targetVerts[oppEdge];
const worldQ = targetVerts[(oppEdge + 1) % targetVerts.length];
const result = attachByEdge(shapeKey, gameState.selectedHand.mirrorState, handEdge, worldP, worldQ);
if (result) {
if (isValidPlacement(result.vertices, targetId, oppEdge)) {
// 計算渲染用的平移和旋轉
// 第一個點的位置
const p = ringToPixel(result.vertices[0]);
gameState.ghosts.push({
shapeIdx: gameState.selectedHand.shapeIdx,
mirrored: gameState.selectedHand.mirrorState,
vertices: result.vertices,
rotation: result.rotation,
tx: p.x, ty: p.y,
targetId: targetId
});
}
}
}
}
}
// 判斷放置是否合法
function isValidPlacement(newVerts, targetId, attachedEdgeIdx) {
let allPieces = gameState.boardPieces.concat(gameState.stagedPieces);
let touchesOtherEdges = false;
let validRule3_4 = true;
// 檢查重疊和邊接觸
for (let p of allPieces) {
if (shapesOverlap(newVerts, p.vertices)) return false;
if (p.id !== targetId && shapesHaveEdgesTouching(newVerts, p.vertices)) {
return false; // 不能與其他棋子邊貼合
}
}
// 第二手棋的特殊規則判定
if (gameState.stagedPieces.length === 1) {
let makesVertexTouch = checkVertexTouchAny(newVerts, allPieces);
let firstMadeVertexTouch = gameState.stagedPieces[0].madeVertexTouch;
if (!firstMadeVertexTouch && !makesVertexTouch) {
if (targetId !== gameState.firstTargetOpponentId) return false;
}
}
return true;
}
function checkVertexTouchAny(newVerts, existingPieces) {
for (const v of newVerts) {
for (const p of existingPieces) {
const n = p.vertices.length;
for (let e = 0; e < n; e++) {
if (pointOnOpenSegment(v, p.vertices[e], p.vertices[(e+1)%n])) return true;
}
}
}
return false;
}
// 點擊 Ghost 確認放下
function placeGhost(ghostIdx) {
const g = gameState.ghosts[ghostIdx];
const newId = 'piece_' + Date.now();
const allPieces = gameState.boardPieces.concat(gameState.stagedPieces);
const madeVertexTouch = checkVertexTouchAny(g.vertices, allPieces);
// 計算中心點(用於顯示白點),簡單平均法
let cx = 0, cy = 0;
g.vertices.forEach(v => { let p = ringToPixel(v); cx += parseFloat(p.x); cy += parseFloat(p.y); });
cx /= g.vertices.length; cy /= g.vertices.length;
const stagedP = {
id: newId, owner: gameState.currentPlayer, shapeIdx: g.shapeIdx,
vertices: g.vertices, rotation: g.rotation, mirrored: g.mirrored,
tx: g.tx, ty: g.ty, center: {x: cx, y: cy},
madeVertexTouch: madeVertexTouch, targetId: g.targetId
};
if (gameState.stagedPieces.length === 0) {
gameState.firstTargetOpponentId = g.targetId;
}
gameState.stagedPieces.push(stagedP);
gameState.piecesLeft[gameState.currentPlayer][g.shapeIdx]--;
gameState.selectedHand = null;
gameState.ghosts = [];
gameState.selectedBoardPieceId = null;
updateUI();
}
// 打叉 取消
function cancelAction() {
if (gameState.stagedPieces.length > 0) {
gameState.stagedPieces.forEach(p => {
gameState.piecesLeft[p.owner][p.shapeIdx]++;
});
gameState.stagedPieces = [];
}
gameState.selectedHand = null;
gameState.ghosts = [];
gameState.selectedBoardPieceId = null;
updateUI();
}
// 打勾 確認回合
function confirmAction() {
if (gameState.stagedPieces.length === 0) return;
// 計分邏輯
let roundScore = calculateScore(gameState.stagedPieces, gameState.boardPieces);
gameState.scores[gameState.currentPlayer] += roundScore;
gameState.boardPieces = gameState.boardPieces.concat(gameState.stagedPieces);
gameState.stagedPieces = [];
// 切換回合
gameState.currentPlayer = gameState.currentPlayer === 'P1' ? 'P2' : 'P1';
// 檢查結束
let totalLeft = gameState.piecesLeft.P1.reduce((a,b)=>a+b,0) + gameState.piecesLeft.P2.reduce((a,b)=>a+b,0);
if (totalLeft === 0 || (gameState.boardPieces.length === 36)) {
setTimeout(() => {
alert(`遊戲結束!\nP1 分數: ${gameState.scores.P1}\nP2 分數: ${gameState.scores.P2}`);
resetGame(true); // 換先
}, 100);
}
updateUI();
}
// 計分 - 尋路算法
function calculateScore(staged, board) {
let score = 0;
const allPieces = board.concat(staged);
// 建立相鄰圖
const adj = {};
allPieces.forEach(p => adj[p.id] = []);
for (let i = 0; i < allPieces.length; i++) {
for (let j = i + 1; j < allPieces.length; j++) {
if (shapesHaveEdgesTouching(allPieces[i].vertices, allPieces[j].vertices)) {
adj[allPieces[i].id].push(allPieces[j].id);
adj[allPieces[j].id].push(allPieces[i].id);
}
}
}
// 檢查頂點
for (const newP of staged) {
for (const v of newP.vertices) {
let hitTarget = null;
let minDist = Infinity;
for (const oldP of allPieces) {
if (oldP.id === newP.id) continue;
const n = oldP.vertices.length;
for (let e = 0; e < n; e++) {
if (pointOnOpenSegment(v, oldP.vertices[e], oldP.vertices[(e+1)%n])) {
// 找到路徑
let dist = bfsDist(oldP.id, newP.id, adj);
if (dist > 0 && dist < minDist) {
minDist = dist;
hitTarget = oldP.id;
}
}
}
}
if (hitTarget && minDist !== Infinity) {
score += (minDist - 1); // 不算起點終點本身
}
}
}
return score;
}
function bfsDist(startId, endId, adj) {
let q = [[startId, 0]];
let visited = new Set([startId]);
while (q.length > 0) {
let [curr, dist] = q.shift();
if (curr === endId) return dist;
for (let neighbor of adj[curr]) {
if (!visited.has(neighbor)) {
visited.add(neighbor);
q.push([neighbor, dist + 1]);
}
}
}
return 0;
}
// =============================================================================
// 3. 渲染與交互 (SVG DOM)
// =============================================================================
function renderBoard() {
const pLayer = document.getElementById('pieces-layer');
const gLayer = document.getElementById('ghosts-layer');
const mLayer = document.getElementById('markers-layer');
pLayer.innerHTML = '';
gLayer.innerHTML = '';
mLayer.innerHTML = '';
const allP = gameState.boardPieces.concat(gameState.stagedPieces);
// 繪製棋子
allP.forEach(p => {
let use = document.createElementNS('http://www.w3.org/2000/svg', 'use');
let tileId = p.owner === 'P1' ? `#tile${p.shapeIdx}` : `#tile${p.shapeIdx + 3}`;
use.setAttribute('href', tileId);
// 由於我們使用的是精確座標系統生成的點,對於 `use` 標籤,我們只需將其旋轉到對應角度並平移到原點即可。
// 但是提供的 defs 是現成的路徑。為了匹配 exact geometry,最好的渲染方式是直接根據 vertices 畫 polygon,
// 或者根據首個點和平移進行變換。
// 由於題目要求用 use[href=tile$][transform="..."],且 defs 已固定,這裡使用計算出的角度和平移。
const rotDeg = getRotationDeg(p.rotation);
const scaleX = p.mirrored ? -1 : 1;
use.setAttribute('transform', `translate(${p.tx}, ${p.ty}) scale(${scaleX}, 1) rotate(${rotDeg})`);
// 如果是已被選中的目標,加藍點
if (p.id === gameState.selectedBoardPieceId) {
let blue = document.createElementNS('http://www.w3.org/2000/svg', 'use');
blue.setAttribute('href', '#dot-blue');
blue.setAttribute('x', p.center.x); blue.setAttribute('y', p.center.y);
mLayer.appendChild(blue);
}
// 如果是剛放的,加白點
if (gameState.stagedPieces.includes(p)) {
let white = document.createElementNS('http://www.w3.org/2000/svg', 'use');
white.setAttribute('href', '#dot-white');
white.setAttribute('x', p.center.x); white.setAttribute('y', p.center.y);
mLayer.appendChild(white);
}
use.style.cursor = 'pointer';
use.onclick = (e) => { e.stopPropagation(); selectBoardPiece(p.id); };
pLayer.appendChild(use);
});
// 繪製預覽 (Ghosts)
gameState.ghosts.forEach((g, idx) => {
let use = document.createElementNS('http://www.w3.org/2000/svg', 'use');
let tileId = gameState.currentPlayer === 'P1' ? `#tile${g.shapeIdx}` : `#tile${g.shapeIdx + 3}`;
use.setAttribute('href', tileId);
const rotDeg = getRotationDeg(g.rotation);
const scaleX = g.mirrored ? -1 : 1;
use.setAttribute('transform', `translate(${g.tx}, ${g.ty}) scale(${scaleX}, 1) rotate(${rotDeg})`);
use.style.opacity = 0.36;
use.style.stroke = 'black';
use.style.strokeWidth = 2;
use.style.cursor = 'pointer';
use.onclick = (e) => { e.stopPropagation(); placeGhost(idx); };
gLayer.appendChild(use);
});
// 控制是否允許拖拽/縮放
let boardInteractive = gameState.boardPieces.length > 0 || gameState.stagedPieces.length > 0;
document.getElementById('etanidrop').style.pointerEvents = boardInteractive ? 'auto' : 'none';
}
// 平移和縮放
let isDragging = false, startPos = {x:0, y:0};
const svgRoot = document.getElementById('etanitrigolden');
const dropGrp = document.getElementById('etanidrop');
function applyTransform() {
viewTransform.scale = Math.max(0.2, Math.min(viewTransform.scale, 5));
dropGrp.setAttribute('transform', `translate(${viewTransform.x.toFixed(3)}, ${viewTransform.y.toFixed(3)}) scale(${viewTransform.scale.toFixed(3)})`);
}
svgRoot.addEventListener('mousedown', e => {
if (e.target.id === 'bg-clicker') { toggleToolbar(); return; }
isDragging = true;
startPos = { x: e.clientX - viewTransform.x, y: e.clientY - viewTransform.y };
});
window.addEventListener('mousemove', e => {
if (!isDragging) return;
viewTransform.x = e.clientX - startPos.x;
viewTransform.y = e.clientY - startPos.y;
applyTransform();
});
window.addEventListener('mouseup', () => isDragging = false);
svgRoot.addEventListener('wheel', e => {
e.preventDefault();
if(gameState.boardPieces.length === 0 && gameState.stagedPieces.length === 0) return;
const s = e.deltaY > 0 ? 0.9 : 1.1;
viewTransform.scale *= s;
applyTransform();
});
// 手機雙指縮放及單指平移
let initialDist = 0, initialScale = 1;
svgRoot.addEventListener('touchstart', e => {
if (e.target.id === 'bg-clicker' && e.touches.length === 1) {
toggleToolbar(); return;
}
if (e.touches.length === 1) {
isDragging = true;
startPos = { x: e.touches[0].clientX - viewTransform.x, y: e.touches[0].clientY - viewTransform.y };
} else if (e.touches.length === 2) {
isDragging = false;
initialDist = Math.hypot(e.touches[0].clientX - e.touches[1].clientX, e.touches[0].clientY - e.touches[1].clientY);
initialScale = viewTransform.scale;
}
}, {passive: false});
svgRoot.addEventListener('touchmove', e => {
if (e.touches.length === 1 && isDragging) {
viewTransform.x = e.touches[0].clientX - startPos.x;
viewTransform.y = e.touches[0].clientY - startPos.y;
applyTransform();
} else if (e.touches.length === 2) {
e.preventDefault(); // 防止頁面滾動
const currentDist = Math.hypot(e.touches[0].clientX - e.touches[1].clientX, e.touches[0].clientY - e.touches[1].clientY);
viewTransform.scale = initialScale * (currentDist / initialDist);
applyTransform();
}
}, {passive: false});
svgRoot.addEventListener('touchend', () => isDragging = false);
function toggleToolbar() {
isToolbarVisible = !isToolbarVisible;
document.getElementById('toolbar').style.opacity = isToolbarVisible ? '1' : '0';
document.getElementById('toolbar').style.pointerEvents = isToolbarVisible ? 'auto' : 'none';
document.getElementById('nav-controls').style.display = isToolbarVisible ? 'none' : 'block';
}
function panBy(dx, dy) { viewTransform.x += dx; viewTransform.y += dy; applyTransform(); }
function zoomBy(factor) { viewTransform.scale *= factor; applyTransform(); }
function resetView() { viewTransform = { x: 240, y: 240, scale: 1 }; applyTransform(); }
function resetGame(swapRoles = false) {
gameState = {
currentPlayer: swapRoles ? 'P2' : 'P1',
piecesLeft: { P1: [6,6,6], P2: [6,6,6] },
scores: { P1: 0, P2: 0 },
boardPieces: [], turnPhase: 0, selectedHand: null,
selectedBoardPieceId: null, stagedPieces: [], ghosts: [], firstTargetOpponentId: null
};
resetView();
updateUI();
}
// 啟動
resetGame();
</script>
</body>
</html>
```
### 設計與實作說明:
1. **版面與響應式**:使用 Flexbox 和 CSS Media Query 達成電腦端橫向三列、手機端直向三列的切換。中央棋盤 `.board-wrapper` 透過 `aspect-ratio: 1` 確保長寬比永遠 1:1,並使用 `box-sizing` 控制邊距。
2. **幾何引擎嵌入**:將你提供的基於十次單位根環 $\mathbb{Z}[\zeta]$ 的 `tiling-exact-geometry2.js` 完全整合於 `<script>` 中。該引擎能夠無誤差判定線段交叉及邊重合。
3. **渲染適配**:原本 `use` 依賴純 SVG 座標變換,我在前端寫了一套微型的 `ringToPixel` 及 `getRotationDeg`,將精確算出的形狀端點轉換為保留三位小數的 `transform="translate(x,y) rotate(a) scale(s)"`,並注入 DOM 中。
4. **交互與狀態機**:
* **首回合**:點擊手牌後,直接調用原點幾何產生 `ghost`,確認後放置在中央。
* **附著邏輯**:隨後點擊對手棋盤棋子,系統會枚舉手牌棋子的所有邊,對目標棋子的所有邊進行 `attachByEdge`,若不重疊且符合單邊貼合規則,則生成透明度 0.36 的 ghost 供玩家點選。
* **雙步規則**:實作了第二顆棋子必須依附同一個對手棋子的規則。並引入了 `pointOnOpenSegment` 判定頂點落在邊上的情況來解除該限制。
5. **計分**:實作了基於無向圖 BFS(廣度優先搜索)的沿邊計算距離算法。當發生頂點接觸邊時,計算從目標到該棋子的最短邊界跨越數並計分。
6. **操控**:綁定了鼠標拖曳/滾輪與手機單指/雙指事件,更改 `#etanidrop` 的 transform 來實現。點擊背景空地時會自動隱藏下方工具欄並顯示十字導航列。https://ejsoon.vip/
弈趣極光:享受思維樂趣
弈趣極光:享受思維樂趣
Re: 一個新的遊戲創意
修復與改進:
一,先手第一手棋無法確認
當先手第一手棋出現在棋盤上時,「打勾」按鈕應該激活並能夠點擊。
當點擊之後,將輪到對方。
二,棋子在翻轉時顯示不全
當在panel-section中的棋子翻轉時,它將顯示不全。應在scale(-1,1)時,搭配translate使之能全部顯示。
三,將文字換成svg圖標
把P1或P2去掉,「AI/人類」換成一個「人類」svg圖標,後面會加入切換成AI的功能。
把「旋轉、AI控制窗口、後退、前進、導出、狀態」都替換成一個SVG圖標,其中「狀態」的圖案將是一個空白的窗口,裡面裝有數字1或數字2的svg―path,當前處於第幾輪就放進去數字幾,初始是1。
四,手機端樣式出錯
當手機端時,先手方根本都不顯示出來,棋盤也幾乎顯示不全。請修復。
請明確給出所要修改的地方和所要替換的代碼。
一,先手第一手棋無法確認
當先手第一手棋出現在棋盤上時,「打勾」按鈕應該激活並能夠點擊。
當點擊之後,將輪到對方。
二,棋子在翻轉時顯示不全
當在panel-section中的棋子翻轉時,它將顯示不全。應在scale(-1,1)時,搭配translate使之能全部顯示。
三,將文字換成svg圖標
把P1或P2去掉,「AI/人類」換成一個「人類」svg圖標,後面會加入切換成AI的功能。
把「旋轉、AI控制窗口、後退、前進、導出、狀態」都替換成一個SVG圖標,其中「狀態」的圖案將是一個空白的窗口,裡面裝有數字1或數字2的svg―path,當前處於第幾輪就放進去數字幾,初始是1。
四,手機端樣式出錯
當手機端時,先手方根本都不顯示出來,棋盤也幾乎顯示不全。請修復。
請明確給出所要修改的地方和所要替換的代碼。
代码: 全选
修復與改進:
一,先手第一手棋無法確認
當先手第一手棋出現在棋盤上時,「打勾」按鈕應該激活並能夠點擊。
當點擊之後,將輪到對方。
二,棋子在翻轉時顯示不全
當在panel-section中的棋子翻轉時,它將顯示不全。應在scale(-1,1)時,搭配translate使之能全部顯示。
三,將文字換成svg圖標
把P1或P2去掉,「AI/人類」換成一個「人類」svg圖標,後面會加入切換成AI的功能。
把「旋轉、AI控制窗口、後退、前進、導出、狀態」都替換成一個SVG圖標,其中「狀態」的圖案將是一個空白的窗口,裡面裝有數字1或數字2的svg―path,當前處於第幾輪就放進去數字幾,初始是1。
四,手機端樣式出錯
當手機端時,先手方根本都不顯示出來,棋盤也幾乎顯示不全。請修復。
請明確給出所要修改的地方和所要替換的代碼。- 附件
-
trigolden_game1.html.7z- (9 KiB) 已下载 32 次
https://ejsoon.vip/
弈趣極光:享受思維樂趣
弈趣極光:享受思維樂趣
Re: 一個新的遊戲創意
gemini 3.1 pro + thinking
代码: 全选
這就為你處理!以下是針對這四個問題的具體修復方案與替換代碼。請依照下方的指示修改你的 HTML 檔案:
### 一、 先手第一手棋無法確認
**原因:** 第一手棋生成預覽(Ghost)時,棋盤的 `pointer-events` 還處於 `none` 狀態,導致預覽圖無法被點擊,從而無法進入「已準備放下(staged)」的狀態來激活打勾按鈕。
**修改位置:** 在 JavaScript 的 `renderBoard()` 函數中,尋找控制 `boardInteractive` 的那一行並進行替換。
**將這行:**
```javascript
let boardInteractive = gameState.boardPieces.length > 0 || gameState.stagedPieces.length > 0;
```
**替換為:**
```javascript
// 加入 gameState.ghosts.length > 0 的判斷,確保第一手棋預覽時也能點擊
let boardInteractive = gameState.boardPieces.length > 0 || gameState.stagedPieces.length > 0 || gameState.ghosts.length > 0;
```
---
### 二、 棋子在翻轉時顯示不全
**原因:** 圖形翻轉 (`scale(-1, 1)`) 是以原點翻轉,導致圖形超出了原本設定的 `viewBox` 範圍。需要在縮放時加入 `translate(-40, 0)` 將它平移回畫面中央。
**修改位置 1:** HTML 中的 P2 初始棋子(共三處)。
**將 `<div id="p2-panel" ...>` 區塊內的這三行:**
```html
<div class="piece-container" id="p2-piece-0" onclick="selectHandPiece('P2', 0)"><svg width="40" height="40" viewBox="-80 -40 120 80"><use href="#tile3" transform="scale(-1,1)"/></svg></div>
<div class="piece-container" id="p2-piece-1" onclick="selectHandPiece('P2', 1)"><svg width="40" height="40" viewBox="-80 -40 120 80"><use href="#tile4" transform="scale(-1,1)"/></svg></div>
<div class="piece-container" id="p2-piece-2" onclick="selectHandPiece('P2', 2)"><svg width="40" height="40" viewBox="-80 -40 120 80"><use href="#tile5" transform="scale(-1,1)"/></svg></div>
```
**替換為(加入 translate):**
```html
<div class="piece-container" id="p2-piece-0" onclick="selectHandPiece('P2', 0)"><svg width="40" height="40" viewBox="-80 -40 120 80"><use href="#tile3" transform="translate(-40, 0) scale(-1,1)"/></svg></div>
<div class="piece-container" id="p2-piece-1" onclick="selectHandPiece('P2', 1)"><svg width="40" height="40" viewBox="-80 -40 120 80"><use href="#tile4" transform="translate(-40, 0) scale(-1,1)"/></svg></div>
<div class="piece-container" id="p2-piece-2" onclick="selectHandPiece('P2', 2)"><svg width="40" height="40" viewBox="-80 -40 120 80"><use href="#tile5" transform="translate(-40, 0) scale(-1,1)"/></svg></div>
```
**修改位置 2:** JavaScript 的 `selectHandPiece` 函數中動態切換翻轉的邏輯。
**將這行:**
```javascript
useEl.setAttribute('transform', `scale(${gameState.selectedHand.mirrorState ? -1 : 1}, 1)`);
```
**替換為:**
```javascript
useEl.setAttribute('transform', gameState.selectedHand.mirrorState ? 'translate(-40, 0) scale(-1, 1)' : 'scale(1, 1)');
```
---
### 三、 將文字換成 SVG 圖標
**修改位置 1:** HTML 中的 P1 與 P2 文字標籤。
**將 HTML 裡 `id="p2-panel"` 與 `id="p1-panel"` 區塊中的這兩行:**
```html
<div style="font-size: 0.8rem">P2 (AI/人類)</div>
<div style="font-size: 0.8rem">P1 (AI/人類)</div>
```
**全部替換為這個「人類玩家」圖標:**
```html
<div class="player-type" title="人類玩家" style="color: #555;">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"></path><circle cx="12" cy="7" r="4"></circle>
</svg>
</div>
```
**修改位置 2:** HTML 中的 Toolbar。
**將 `<div class="toolbar" id="toolbar"> ... </div>` 整個區塊替換為以下包含 SVG 圖標的代碼:**
```html
<div class="toolbar" id="toolbar">
<div class="toolbar-btn" title="旋轉">
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 2v6h-6"></path><path d="M21 13a9 9 0 1 1-3-7.7L21 8"></path></svg>
</div>
<div class="toolbar-btn" title="AI控制窗口">
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="4" y="4" width="16" height="16" rx="2" ry="2"></rect><rect x="9" y="9" width="6" height="6"></rect><line x1="9" y1="1" x2="9" y2="4"></line><line x1="15" y1="1" x2="15" y2="4"></line><line x1="9" y1="20" x2="9" y2="23"></line><line x1="15" y1="20" x2="15" y2="23"></line><line x1="20" y1="9" x2="23" y2="9"></line><line x1="20" y1="14" x2="23" y2="14"></line><line x1="1" y1="9" x2="4" y2="9"></line><line x1="1" y1="14" x2="4" y2="14"></line></svg>
</div>
<div class="toolbar-btn" title="後退">
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="9 14 4 9 9 4"></polyline><path d="M20 20v-7a4 4 0 0 0-4-4H4"></path></svg>
</div>
<div class="toolbar-btn" title="前進">
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="15 14 20 9 15 4"></polyline><path d="M4 20v-7a4 4 0 0 1 4-4h12"></path></svg>
</div>
<div class="toolbar-btn" title="導出">
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path><polyline points="17 8 12 3 7 8"></polyline><line x1="12" y1="3" x2="12" y2="15"></line></svg>
</div>
<div class="toolbar-btn" title="狀態">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect>
<text id="status-text" x="12" y="16.5" text-anchor="middle" font-size="13" font-weight="bold" fill="currentColor" stroke="none">1</text>
</svg>
</div>
</div>
```
**修改位置 3:** JavaScript 的 `updateUI()` 中更新狀態文字。
**將這行:**
```javascript
document.getElementById('status-text').innerText = `狀態: ${gameState.currentPlayer} 回合`;
```
**替換為:**
```javascript
// 動態將數字 1 或 2 填入 SVG 的 text 節點內
document.getElementById('status-text').textContent = gameState.currentPlayer === 'P1' ? '1' : '2';
```
---
### 四、 手機端樣式出錯
**原因:** `min-height: 240px;` 在手機高度有限的情況下霸佔了太多空間,導致底部的 P1 面板被推到螢幕外,且因為 Flexbox 無法正常收縮,讓中間的棋盤被過度擠壓。
**修改位置:** `<style>` 中關於 `@media (max-width: 768px)` 的 CSS。
**將原本的:**
```css
@media (max-width: 768px) {
body { flex-direction: column; }
.control-panel { min-width: auto; min-height: 240px; flex-direction: row; }
#board-area { padding: 7px; }
#p2-panel { order: 1; }
#board-area { order: 2; }
#p1-panel { order: 3; }
}
```
以及底下的 `.piece-container` 與 `.flex-row-mobile` 相關媒體查詢,**全部整合並替換成以下優化版本:**
```css
@media (max-width: 768px) {
body {
flex-direction: column;
justify-content: space-between;
}
.control-panel {
min-width: 100%;
min-height: auto; /* 取消寫死的 240px 高度 */
height: auto;
flex-direction: row;
justify-content: space-around;
padding: 8px 5px;
flex: 0 0 auto;
box-sizing: border-box;
}
#board-area {
padding: 5px;
order: 2;
flex: 1 1 auto; /* 允許隨空間彈性縮放 */
min-height: 0; /* 關鍵!防止 flex 子元素溢出 */
}
#p2-panel { order: 1; }
#p1-panel { order: 3; }
.piece-container { width: 42px; height: 42px; }
.btn-icon { width: 32px; height: 32px; }
.flex-row-mobile { flex-direction: column; gap: 4px; }
.score-board { font-size: 1.5rem; }
}
```
*(注意:替換後,你原本分散在下面單獨寫的 `@media (max-width: 768px) { .piece-container { ... } }` 和 `.flex-row-mobile` 的手機媒體查詢就可以刪除,因為已經統整在上面這段了。)*https://ejsoon.vip/
弈趣極光:享受思維樂趣
弈趣極光:享受思維樂趣
Re: 一個新的遊戲創意
《頂鑫棋(TriGolden)》是一款二人抽象策略遊戲,為其製作一個基於svg的html5弈棋遊戲:
一,頁面安排
頁面包括三個部分:先手方控制區,後手方控制區,中間棋盤區。中間棋盤區佔據中間區域。另兩個控制區分佔另兩個區域。
如果是電腦端,右方是先手方控制區,左方是後手方控制區,中間是棋盤區。控制區的寬度為240px,棋盤區佔滿中間部分剩餘的區域。
如果是手機端,下方是先手方控制區,上方是後手方控制區,中間是棋盤區。控制區的高度為160px,棋盤區佔滿中間部分剩餘的區域。
所上傳的svg將作為棋盤,直接貼進中間棋盤區。如果是電腦端,它離上下左右的邊距將為24px,如果是手機端,邊距將是7px。為棋盤增加懸浮陰影效果。
在控制區中,有分數區和棋子區以及確認區,每方有三種棋子,則五個元素大小佔位是相同的。如果是電腦端,則它們縱向排列並平分間距,先手方三種svg棋子的右方是它的數量,後手方的數量則標在左邊。如果是手機端,則它們横向排列並平分間距,三種棋子的數量統一放在svg棋子的下方。分數區將居中顯示分數,數字下方是切換AI和人類的svg圖標。確認區有「打叉」和「打勾」兩個按鈕,初始是灰色未激活狀態。
雙方的三種svg棋子將取自所下面的svg的defs中,其名稱和id對應關係為:先手方棋子id=tile0、tile1、tile2,後手方棋子id=tile3、tile4、tile5。
<svg id="etanitrigolden" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg" width="480" height="480" viewBox="0 0 480 480" preserveAspectRatio="xMidYMid meet"> <defs> <g id="tile0" fill="#ffc2eb"><path d="M -61.687,17.119 L -14.563,-17.119 L 21.437,-17.119 L 32.562,17.119 Z"/></g><g id="tile1" fill="#ffc2eb"><path d="M -76.249,24.775 L -0.001,-30.623 L 29.124,-9.463 L 40.249,24.775 Z"/></g><g id="tile2" fill="#ffc2eb"><path d="M -55.468,19.14 L 2.781,-23.18 L 31.906,-2.02 L 2.781,19.14 Z"/></g><g id="tile3" fill="#96ffbe"><path d="M -61.687,17.119 L -14.563,-17.119 L 21.437,-17.119 L 32.562,17.119 Z"/></g><g id="tile4" fill="#96ffbe"><path d="M -76.249,24.775 L -0.001,-30.623 L 29.124,-9.463 L 40.249,24.775 Z"/></g><g id="tile5" fill="#96ffbe"><path d="M -55.468,19.14 L 2.781,-23.18 L 31.906,-2.02 L 2.781,19.14 Z"/></g></defs> <g class="etdrop" stroke="#777" stroke-width="2" stroke-linejoin="round" transform="translate(240,240) scale(1,1)" id="etanidrop"></g></svg>
每個棋子都在一個相同大小的矩形區域內,電腦端稍大而手機端稍小,為其增加陰影懸浮效果。
二,棋盤區域
當棋盤中沒有棋子時,其不可平移和縮放,當棋盤中至少存在一個棋子時,棋盤將可以平移和縮放。當遊戲結束到下一輪時,棋盤將清空並復原為初始的平移和縮放狀態。
所有的平移縮放都改變的是.etdrop的transform的值,保留最多三位小數。如果是手機端,則可用單指拖動平移,雙指縮放。如果是電腦端,則可用鼠標按住拖動平移,滾輪縮放。
三,落子規則
先手方開始行動,之後雙方輪流行動。
每方每種棋子有6個,則每方一共有18個棋子。除了先手方的第一回合及最後一回合只放一個棋子之外,每方每回合要放兩個棋子。
先手方首回合將把他的一個棋子放至棋盤中心,此後每一方的回合中,他所放的每一個棋子都要滿足以下條件:
3.1,每個棋子落下時,它的其中一條邊要完全貼合另一個在場上已有的「對方棋子」的一條邊。
3.2,不能有另一條邊跟場上已有的棋子貼合,包括部分重合但不完全貼合的情況。
3.3,不能跟場上已有的棋子的面重疊。
3.4,這一回合所放的兩個棋子都一定要貼合到同一個「對方棋子」的兩個不同的邊上,除非符合以下兩點。
3.5,如果第一手棋在落下之後,形成了「有棋子的端點正好落在另一個棋子的邊上」的情況,則第二手棋可以落在任意一個「對方棋子」的邊上。
3.6,如果第二手棋在落下之後,可以形成「有棋子的端點正好落在另一個棋子的邊上」的情況,則允許這手棋不跟第一手棋落在同一個「對方棋子」的邊上。
以上「邊與邊是否貼合、端點是否正好落在另一個圖形的邊上、是否發生重疊」的判定邏輯都已經在「tiling-exact-geometry2.js」寫好,可以直接使用。
四,計分規則
4.1,當「有棋子a的端點正好落在另一個棋子b的邊上」時,將計分給a棋子所屬的一方。a為「頂鑫棋子」,b為「被頂棋子」。
4.2,從b沿邊接邊走到a,每經過一個棋子計一分,a和b本身不算。則經過多少個棋子就計多少分。
4.3,如果存在同一個棋子被兩個以上的棋子頂到的情況,只計最靠近的一個,或者說只計最先頂到的一個。
四,為棋子增加點擊事件及邏輯
當輸到一方行動時,點擊他的一個棋子,其外部的div的陰影和邊框將變色,表示這個棋子被選中。
當再次點擊一個被選中的棋子時,它將以y=0進行鏡像翻轉,實現方式是把它的transform中的scale的值改為(-1,1)。再點一次則又會翻回來。後手方的三個棋子在一局開始時就處於已翻轉狀態。
當先手方首回合點擊他的一個棋子時,他的棋子將以相同的鏡像狀態出現在棋盤中心,這時「確認按鈕」將被激活並可點擊。
如果控制區中選中的棋子翻轉,場上棋盤中心的棋子也跟著翻轉,如果點擊另一個棋子,則場上的棋子將換成另一個棋子。點擊「打叉」則清空棋盤,回到沒有棋子被選中的狀態。
先手方的首回合結束(先手方按下打勾確認)之後,輪到後手方。從這時開始,落子將採用以下方式進行。
當點擊控制區的一個己方棋子,再點擊棋盤區中的一個對方棋子時,己方這個棋子的所有可能落下的情況會「預先」放到棋盤區內,這些預放棋子的邊緣是黑色不透明,但填色透明度為0.36。這些預放棋子之間可能會有部分面重疊。如果翻轉該棋子,則所有能落下的可能情況要重新判定。
當點擊到一個預放棋子不重疊的面時,則這個預放棋子將成為已放棋子。其它預放棋子將消失,己方控制區將取消該棋子的選中狀態。
這時第一手棋已經落好,將進行第二手棋。注意第二手棋的「可能落下情況」要加上規則中所說的「要在同一個對方棋子的另一個邊上」以及「兩個例外」。
有一個棋被選中時,「打叉」將激活並可以點擊,若點擊將把所有已放的棋子全部收回並取消所有選中棋子。當兩手棋都落好時,「打勾」將激活,如點擊則完成本回合行動。
棋子都用use[href=tile$][transform="translate(平移x,平移y) scale(1或-1,1) rotate(旋轉)"]來顯示(數值保留最多三位小數),加到svg的.etdrop中。
當場上有「對方棋子」被選中時,將在其中心位置加一個「半徑為3、stroke為none、fill為天藍色的圓」。當一方的回合在落子時,剛落的棋子中心將加一個「半徑為3、stroke為none、fill為白色的圓」,稱為白點。我方棋子的白點會在「打叉」時消失(上回合的對方棋子的白點又將顯示出來),但當打勾輪到對方時,白點不會消失,只有一方有棋子選中時,上回合的白點才會消失。
當最後一手棋落下後,將彈出窗口顯示總分和輸贏,同時提示「是否換先再來一局」,或「結束遊戲」。如果換先再來一局,則先手方會變為後手方(控制區的位置不變),兩局結束則直接計算總分並說明誰是贏家。
五,代碼要求
所有svg中的數值最多保留三位小數。
所有的html、css和js代碼都寫在一個html中,包括「tiling-exact-geometry2.js」的代碼也要整合到<script>中。
特別注意:先手方首回合是沒有「預放棋子」的,只要先手方首回合有棋子它出現在棋盤中心,先手方就可以點擊確認結束其回合。
一,頁面安排
頁面包括三個部分:先手方控制區,後手方控制區,中間棋盤區。中間棋盤區佔據中間區域。另兩個控制區分佔另兩個區域。
如果是電腦端,右方是先手方控制區,左方是後手方控制區,中間是棋盤區。控制區的寬度為240px,棋盤區佔滿中間部分剩餘的區域。
如果是手機端,下方是先手方控制區,上方是後手方控制區,中間是棋盤區。控制區的高度為160px,棋盤區佔滿中間部分剩餘的區域。
所上傳的svg將作為棋盤,直接貼進中間棋盤區。如果是電腦端,它離上下左右的邊距將為24px,如果是手機端,邊距將是7px。為棋盤增加懸浮陰影效果。
在控制區中,有分數區和棋子區以及確認區,每方有三種棋子,則五個元素大小佔位是相同的。如果是電腦端,則它們縱向排列並平分間距,先手方三種svg棋子的右方是它的數量,後手方的數量則標在左邊。如果是手機端,則它們横向排列並平分間距,三種棋子的數量統一放在svg棋子的下方。分數區將居中顯示分數,數字下方是切換AI和人類的svg圖標。確認區有「打叉」和「打勾」兩個按鈕,初始是灰色未激活狀態。
雙方的三種svg棋子將取自所下面的svg的defs中,其名稱和id對應關係為:先手方棋子id=tile0、tile1、tile2,後手方棋子id=tile3、tile4、tile5。
<svg id="etanitrigolden" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg" width="480" height="480" viewBox="0 0 480 480" preserveAspectRatio="xMidYMid meet"> <defs> <g id="tile0" fill="#ffc2eb"><path d="M -61.687,17.119 L -14.563,-17.119 L 21.437,-17.119 L 32.562,17.119 Z"/></g><g id="tile1" fill="#ffc2eb"><path d="M -76.249,24.775 L -0.001,-30.623 L 29.124,-9.463 L 40.249,24.775 Z"/></g><g id="tile2" fill="#ffc2eb"><path d="M -55.468,19.14 L 2.781,-23.18 L 31.906,-2.02 L 2.781,19.14 Z"/></g><g id="tile3" fill="#96ffbe"><path d="M -61.687,17.119 L -14.563,-17.119 L 21.437,-17.119 L 32.562,17.119 Z"/></g><g id="tile4" fill="#96ffbe"><path d="M -76.249,24.775 L -0.001,-30.623 L 29.124,-9.463 L 40.249,24.775 Z"/></g><g id="tile5" fill="#96ffbe"><path d="M -55.468,19.14 L 2.781,-23.18 L 31.906,-2.02 L 2.781,19.14 Z"/></g></defs> <g class="etdrop" stroke="#777" stroke-width="2" stroke-linejoin="round" transform="translate(240,240) scale(1,1)" id="etanidrop"></g></svg>
每個棋子都在一個相同大小的矩形區域內,電腦端稍大而手機端稍小,為其增加陰影懸浮效果。
二,棋盤區域
當棋盤中沒有棋子時,其不可平移和縮放,當棋盤中至少存在一個棋子時,棋盤將可以平移和縮放。當遊戲結束到下一輪時,棋盤將清空並復原為初始的平移和縮放狀態。
所有的平移縮放都改變的是.etdrop的transform的值,保留最多三位小數。如果是手機端,則可用單指拖動平移,雙指縮放。如果是電腦端,則可用鼠標按住拖動平移,滾輪縮放。
三,落子規則
先手方開始行動,之後雙方輪流行動。
每方每種棋子有6個,則每方一共有18個棋子。除了先手方的第一回合及最後一回合只放一個棋子之外,每方每回合要放兩個棋子。
先手方首回合將把他的一個棋子放至棋盤中心,此後每一方的回合中,他所放的每一個棋子都要滿足以下條件:
3.1,每個棋子落下時,它的其中一條邊要完全貼合另一個在場上已有的「對方棋子」的一條邊。
3.2,不能有另一條邊跟場上已有的棋子貼合,包括部分重合但不完全貼合的情況。
3.3,不能跟場上已有的棋子的面重疊。
3.4,這一回合所放的兩個棋子都一定要貼合到同一個「對方棋子」的兩個不同的邊上,除非符合以下兩點。
3.5,如果第一手棋在落下之後,形成了「有棋子的端點正好落在另一個棋子的邊上」的情況,則第二手棋可以落在任意一個「對方棋子」的邊上。
3.6,如果第二手棋在落下之後,可以形成「有棋子的端點正好落在另一個棋子的邊上」的情況,則允許這手棋不跟第一手棋落在同一個「對方棋子」的邊上。
以上「邊與邊是否貼合、端點是否正好落在另一個圖形的邊上、是否發生重疊」的判定邏輯都已經在「tiling-exact-geometry2.js」寫好,可以直接使用。
四,計分規則
4.1,當「有棋子a的端點正好落在另一個棋子b的邊上」時,將計分給a棋子所屬的一方。a為「頂鑫棋子」,b為「被頂棋子」。
4.2,從b沿邊接邊走到a,每經過一個棋子計一分,a和b本身不算。則經過多少個棋子就計多少分。
4.3,如果存在同一個棋子被兩個以上的棋子頂到的情況,只計最靠近的一個,或者說只計最先頂到的一個。
四,為棋子增加點擊事件及邏輯
當輸到一方行動時,點擊他的一個棋子,其外部的div的陰影和邊框將變色,表示這個棋子被選中。
當再次點擊一個被選中的棋子時,它將以y=0進行鏡像翻轉,實現方式是把它的transform中的scale的值改為(-1,1)。再點一次則又會翻回來。後手方的三個棋子在一局開始時就處於已翻轉狀態。
當先手方首回合點擊他的一個棋子時,他的棋子將以相同的鏡像狀態出現在棋盤中心,這時「確認按鈕」將被激活並可點擊。
如果控制區中選中的棋子翻轉,場上棋盤中心的棋子也跟著翻轉,如果點擊另一個棋子,則場上的棋子將換成另一個棋子。點擊「打叉」則清空棋盤,回到沒有棋子被選中的狀態。
先手方的首回合結束(先手方按下打勾確認)之後,輪到後手方。從這時開始,落子將採用以下方式進行。
當點擊控制區的一個己方棋子,再點擊棋盤區中的一個對方棋子時,己方這個棋子的所有可能落下的情況會「預先」放到棋盤區內,這些預放棋子的邊緣是黑色不透明,但填色透明度為0.36。這些預放棋子之間可能會有部分面重疊。如果翻轉該棋子,則所有能落下的可能情況要重新判定。
當點擊到一個預放棋子不重疊的面時,則這個預放棋子將成為已放棋子。其它預放棋子將消失,己方控制區將取消該棋子的選中狀態。
這時第一手棋已經落好,將進行第二手棋。注意第二手棋的「可能落下情況」要加上規則中所說的「要在同一個對方棋子的另一個邊上」以及「兩個例外」。
有一個棋被選中時,「打叉」將激活並可以點擊,若點擊將把所有已放的棋子全部收回並取消所有選中棋子。當兩手棋都落好時,「打勾」將激活,如點擊則完成本回合行動。
棋子都用use[href=tile$][transform="translate(平移x,平移y) scale(1或-1,1) rotate(旋轉)"]來顯示(數值保留最多三位小數),加到svg的.etdrop中。
當場上有「對方棋子」被選中時,將在其中心位置加一個「半徑為3、stroke為none、fill為天藍色的圓」。當一方的回合在落子時,剛落的棋子中心將加一個「半徑為3、stroke為none、fill為白色的圓」,稱為白點。我方棋子的白點會在「打叉」時消失(上回合的對方棋子的白點又將顯示出來),但當打勾輪到對方時,白點不會消失,只有一方有棋子選中時,上回合的白點才會消失。
當最後一手棋落下後,將彈出窗口顯示總分和輸贏,同時提示「是否換先再來一局」,或「結束遊戲」。如果換先再來一局,則先手方會變為後手方(控制區的位置不變),兩局結束則直接計算總分並說明誰是贏家。
五,代碼要求
所有svg中的數值最多保留三位小數。
所有的html、css和js代碼都寫在一個html中,包括「tiling-exact-geometry2.js」的代碼也要整合到<script>中。
特別注意:先手方首回合是沒有「預放棋子」的,只要先手方首回合有棋子它出現在棋盤中心,先手方就可以點擊確認結束其回合。
代码: 全选
《頂鑫棋(TriGolden)》是一款二人抽象策略遊戲,為其製作一個基於svg的html5弈棋遊戲:
一,頁面安排
頁面包括三個部分:先手方控制區,後手方控制區,中間棋盤區。中間棋盤區佔據中間區域。另兩個控制區分佔另兩個區域。
如果是電腦端,右方是先手方控制區,左方是後手方控制區,中間是棋盤區。控制區的寬度為240px,棋盤區佔滿中間部分剩餘的區域。
如果是手機端,下方是先手方控制區,上方是後手方控制區,中間是棋盤區。控制區的高度為160px,棋盤區佔滿中間部分剩餘的區域。
所上傳的svg將作為棋盤,直接貼進中間棋盤區。如果是電腦端,它離上下左右的邊距將為24px,如果是手機端,邊距將是7px。為棋盤增加懸浮陰影效果。
在控制區中,有分數區和棋子區以及確認區,每方有三種棋子,則五個元素大小佔位是相同的。如果是電腦端,則它們縱向排列並平分間距,先手方三種svg棋子的右方是它的數量,後手方的數量則標在左邊。如果是手機端,則它們横向排列並平分間距,三種棋子的數量統一放在svg棋子的下方。分數區將居中顯示分數,數字下方是切換AI和人類的svg圖標。確認區有「打叉」和「打勾」兩個按鈕,初始是灰色未激活狀態。
雙方的三種svg棋子將取自所下面的svg的defs中,其名稱和id對應關係為:先手方棋子id=tile0、tile1、tile2,後手方棋子id=tile3、tile4、tile5。
<svg id="etanitrigolden" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg" width="480" height="480" viewBox="0 0 480 480" preserveAspectRatio="xMidYMid meet"> <defs> <g id="tile0" fill="#ffc2eb"><path d="M -61.687,17.119 L -14.563,-17.119 L 21.437,-17.119 L 32.562,17.119 Z"/></g><g id="tile1" fill="#ffc2eb"><path d="M -76.249,24.775 L -0.001,-30.623 L 29.124,-9.463 L 40.249,24.775 Z"/></g><g id="tile2" fill="#ffc2eb"><path d="M -55.468,19.14 L 2.781,-23.18 L 31.906,-2.02 L 2.781,19.14 Z"/></g><g id="tile3" fill="#96ffbe"><path d="M -61.687,17.119 L -14.563,-17.119 L 21.437,-17.119 L 32.562,17.119 Z"/></g><g id="tile4" fill="#96ffbe"><path d="M -76.249,24.775 L -0.001,-30.623 L 29.124,-9.463 L 40.249,24.775 Z"/></g><g id="tile5" fill="#96ffbe"><path d="M -55.468,19.14 L 2.781,-23.18 L 31.906,-2.02 L 2.781,19.14 Z"/></g></defs> <g class="etdrop" stroke="#777" stroke-width="2" stroke-linejoin="round" transform="translate(240,240) scale(1,1)" id="etanidrop"></g></svg>
每個棋子都在一個相同大小的矩形區域內,電腦端稍大而手機端稍小,為其增加陰影懸浮效果。
二,棋盤區域
當棋盤中沒有棋子時,其不可平移和縮放,當棋盤中至少存在一個棋子時,棋盤將可以平移和縮放。當遊戲結束到下一輪時,棋盤將清空並復原為初始的平移和縮放狀態。
所有的平移縮放都改變的是.etdrop的transform的值,保留最多三位小數。如果是手機端,則可用單指拖動平移,雙指縮放。如果是電腦端,則可用鼠標按住拖動平移,滾輪縮放。
三,落子規則
先手方開始行動,之後雙方輪流行動。
每方每種棋子有6個,則每方一共有18個棋子。除了先手方的第一回合及最後一回合只放一個棋子之外,每方每回合要放兩個棋子。
先手方首回合將把他的一個棋子放至棋盤中心,此後每一方的回合中,他所放的每一個棋子都要滿足以下條件:
3.1,每個棋子落下時,它的其中一條邊要完全貼合另一個在場上已有的「對方棋子」的一條邊。
3.2,不能有另一條邊跟場上已有的棋子貼合,包括部分重合但不完全貼合的情況。
3.3,不能跟場上已有的棋子的面重疊。
3.4,這一回合所放的兩個棋子都一定要貼合到同一個「對方棋子」的兩個不同的邊上,除非符合以下兩點。
3.5,如果第一手棋在落下之後,形成了「有棋子的端點正好落在另一個棋子的邊上」的情況,則第二手棋可以落在任意一個「對方棋子」的邊上。
3.6,如果第二手棋在落下之後,可以形成「有棋子的端點正好落在另一個棋子的邊上」的情況,則允許這手棋不跟第一手棋落在同一個「對方棋子」的邊上。
以上「邊與邊是否貼合、端點是否正好落在另一個圖形的邊上、是否發生重疊」的判定邏輯都已經在「tiling-exact-geometry2.js」寫好,可以直接使用。
四,計分規則
4.1,當「有棋子a的端點正好落在另一個棋子b的邊上」時,將計分給a棋子所屬的一方。a為「頂鑫棋子」,b為「被頂棋子」。
4.2,從b沿邊接邊走到a,每經過一個棋子計一分,a和b本身不算。則經過多少個棋子就計多少分。
4.3,如果存在同一個棋子被兩個以上的棋子頂到的情況,只計最靠近的一個,或者說只計最先頂到的一個。
四,為棋子增加點擊事件及邏輯
當輸到一方行動時,點擊他的一個棋子,其外部的div的陰影和邊框將變色,表示這個棋子被選中。
當再次點擊一個被選中的棋子時,它將以y=0進行鏡像翻轉,實現方式是把它的transform中的scale的值改為(-1,1)。再點一次則又會翻回來。後手方的三個棋子在一局開始時就處於已翻轉狀態。
當先手方首回合點擊他的一個棋子時,他的棋子將以相同的鏡像狀態出現在棋盤中心,這時「確認按鈕」將被激活並可點擊。
如果控制區中選中的棋子翻轉,場上棋盤中心的棋子也跟著翻轉,如果點擊另一個棋子,則場上的棋子將換成另一個棋子。點擊「打叉」則清空棋盤,回到沒有棋子被選中的狀態。
先手方的首回合結束(先手方按下打勾確認)之後,輪到後手方。從這時開始,落子將採用以下方式進行。
當點擊控制區的一個己方棋子,再點擊棋盤區中的一個對方棋子時,己方這個棋子的所有可能落下的情況會「預先」放到棋盤區內,這些預放棋子的邊緣是黑色不透明,但填色透明度為0.36。這些預放棋子之間可能會有部分面重疊。如果翻轉該棋子,則所有能落下的可能情況要重新判定。
當點擊到一個預放棋子不重疊的面時,則這個預放棋子將成為已放棋子。其它預放棋子將消失,己方控制區將取消該棋子的選中狀態。
這時第一手棋已經落好,將進行第二手棋。注意第二手棋的「可能落下情況」要加上規則中所說的「要在同一個對方棋子的另一個邊上」以及「兩個例外」。
有一個棋被選中時,「打叉」將激活並可以點擊,若點擊將把所有已放的棋子全部收回並取消所有選中棋子。當兩手棋都落好時,「打勾」將激活,如點擊則完成本回合行動。
棋子都用use[href=tile$][transform="translate(平移x,平移y) scale(1或-1,1) rotate(旋轉)"]來顯示(數值保留最多三位小數),加到svg的.etdrop中。
當場上有「對方棋子」被選中時,將在其中心位置加一個「半徑為3、stroke為none、fill為天藍色的圓」。當一方的回合在落子時,剛落的棋子中心將加一個「半徑為3、stroke為none、fill為白色的圓」,稱為白點。我方棋子的白點會在「打叉」時消失(上回合的對方棋子的白點又將顯示出來),但當打勾輪到對方時,白點不會消失,只有一方有棋子選中時,上回合的白點才會消失。
當最後一手棋落下後,將彈出窗口顯示總分和輸贏,同時提示「是否換先再來一局」,或「結束遊戲」。如果換先再來一局,則先手方會變為後手方(控制區的位置不變),兩局結束則直接計算總分並說明誰是贏家。
五,代碼要求
所有svg中的數值最多保留三位小數。
所有的html、css和js代碼都寫在一個html中,包括「tiling-exact-geometry2.js」的代碼也要整合到<script>中。
特別注意:先手方首回合是沒有「預放棋子」的,只要先手方首回合有棋子它出現在棋盤中心,先手方就可以點擊確認結束其回合。https://ejsoon.vip/
弈趣極光:享受思維樂趣
弈趣極光:享受思維樂趣
Re: 一個新的遊戲創意
gemini 3.1 pro + thinking
代码: 全选
```html
<!DOCTYPE html>
<html lang="zh-HK">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>頂鑫棋 (TriGolden)</title>
<style>
:root {
--bg-color: #2c2f33;
--panel-bg: #23272a;
--board-bg: #ffffff;
--text-color: #ffffff;
--highlight: #7289da;
--p1-color: #ffc2eb;
--p2-color: #96ffbe;
}
body {
margin: 0;
padding: 0;
display: flex;
flex-direction: row;
height: 100vh;
background-color: var(--bg-color);
font-family: sans-serif;
overflow: hidden;
user-select: none;
}
/* Control Panels */
.panel {
width: 240px;
background-color: var(--panel-bg);
display: flex;
flex-direction: column;
justify-content: space-evenly;
align-items: center;
box-sizing: border-box;
padding: 10px;
}
#panel-p2 { border-right: 2px solid #111; }
#panel-p1 { border-left: 2px solid #111; }
.panel-section {
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
width: 100%;
height: 18%;
}
.score-area {
flex-direction: column;
color: var(--text-color);
text-align: center;
}
.score-num { font-size: 28px; font-weight: bold; margin-bottom: 5px; }
.ai-btn {
cursor: pointer; fill: #aaa; width: 24px; height: 24px; transition: 0.2s;
}
.ai-btn.active { fill: #43b581; }
.tile-wrapper {
display: flex;
align-items: center;
justify-content: center;
gap: 15px;
cursor: pointer;
}
.tile-box {
width: 60px;
height: 60px;
border: 2px solid transparent;
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
background: rgba(255,255,255,0.05);
transition: all 0.2s;
box-shadow: 0 4px 6px rgba(0,0,0,0.3);
}
.tile-box.selected {
border-color: var(--highlight);
box-shadow: 0 0 15px var(--highlight);
background: rgba(255,255,255,0.15);
}
.tile-box svg { width: 45px; height: 45px; overflow: visible; }
.tile-count { font-size: 22px; color: #ddd; font-weight: bold; width: 20px; text-align: center; }
.action-area {
display: flex;
gap: 20px;
}
.action-btn {
width: 48px;
height: 48px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
background: #444;
box-shadow: 0 4px 6px rgba(0,0,0,0.3);
transition: 0.2s;
}
.action-btn svg { width: 24px; height: 24px; fill: white; }
.action-btn.disabled { opacity: 0.3; cursor: not-allowed; }
#btn-cross:not(.disabled) { background: #f04747; }
#btn-check:not(.disabled) { background: #43b581; }
#btn-cross:not(.disabled):active { transform: scale(0.9); }
#btn-check:not(.disabled):active { transform: scale(0.9); }
/* Board Area */
.board-container {
flex-grow: 1;
margin: 24px;
background-color: var(--board-bg);
box-shadow: 0 8px 16px rgba(0,0,0,0.6);
border-radius: 12px;
overflow: hidden;
position: relative;
}
#etanitrigolden {
width: 100%;
height: 100%;
display: block;
touch-action: none; /* Disable browser zooming/panning */
}
.ghost-piece {
fill: rgba(0, 0, 0, 0.36);
stroke: #000;
stroke-width: 2;
cursor: pointer;
pointer-events: all;
}
.ghost-piece:hover { fill: rgba(0, 0, 0, 0.5); }
.placed-piece { pointer-events: none; }
.board-target { cursor: pointer; pointer-events: all; }
/* Turn Indicator */
.turn-active { background: rgba(114, 137, 218, 0.1); border: 1px solid rgba(114, 137, 218, 0.5); }
/* Mobile Layout */
@media (max-width: 768px) {
body { flex-direction: column; }
.panel {
width: 100%;
height: 160px;
flex-direction: row;
border: none;
border-bottom: 2px solid #111;
}
#panel-p1 { border-top: 2px solid #111; border-bottom: none; }
.board-container { margin: 7px; }
.panel-section { width: 18%; height: 100%; flex-direction: column; }
.tile-wrapper { flex-direction: column; gap: 5px; }
.action-area { flex-direction: column; gap: 10px; }
.tile-box { width: 50px; height: 50px; }
.tile-box svg { width: 36px; height: 36px; }
}
</style>
</head>
<body>
<div id="panel-p2" class="panel">
<div class="panel-section score-area">
<div class="score-num" id="score2">0</div>
<svg class="ai-btn" id="ai2" viewBox="0 0 24 24" onclick="toggleAI(2)"><path d="M21 9h-2V7c0-1.1-.9-2-2-2h-3.5l-1-2h-1l-1 2H7c-1.1 0-2 .9-2 2v2H3v2h2v4H3v2h2v2c0 1.1.9 2 2 2h10c1.1 0 2-.9 2-2v-2h2v-2h-2v-4h2V9zm-4 10H7V7h10v12zM9 9h2v2H9V9zm4 0h2v2h-2V9zm-4 4h6v2H9v-2z"/></svg>
</div>
<div class="panel-section tile-wrapper" onclick="selectTile(2, 'tile3')">
<div class="tile-count" id="count-tile3">6</div>
<div class="tile-box" id="box-tile3"><svg viewBox="-100 -100 200 200"><use href="#tile3" transform="scale(1,-1)"/></svg></div>
</div>
<div class="panel-section tile-wrapper" onclick="selectTile(2, 'tile4')">
<div class="tile-count" id="count-tile4">6</div>
<div class="tile-box" id="box-tile4"><svg viewBox="-100 -100 200 200"><use href="#tile4" transform="scale(1,-1)"/></svg></div>
</div>
<div class="panel-section tile-wrapper" onclick="selectTile(2, 'tile5')">
<div class="tile-count" id="count-tile5">6</div>
<div class="tile-box" id="box-tile5"><svg viewBox="-100 -100 200 200"><use href="#tile5" transform="scale(1,-1)"/></svg></div>
</div>
<div class="panel-section action-area" style="visibility: hidden;"></div>
</div>
<div class="board-container">
<svg id="etanitrigolden" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg" width="480" height="480" viewBox="0 0 480 480" preserveAspectRatio="xMidYMid meet">
<defs>
<g id="tile0" fill="#ffc2eb"><path d="M -61.687,17.119 L -14.563,-17.119 L 21.437,-17.119 L 32.562,17.119 Z"/></g>
<g id="tile1" fill="#ffc2eb"><path d="M -76.249,24.775 L -0.001,-30.623 L 29.124,-9.463 L 40.249,24.775 Z"/></g>
<g id="tile2" fill="#ffc2eb"><path d="M -55.468,19.14 L 2.781,-23.18 L 31.906,-2.02 L 2.781,19.14 Z"/></g>
<g id="tile3" fill="#96ffbe"><path d="M -61.687,17.119 L -14.563,-17.119 L 21.437,-17.119 L 32.562,17.119 Z"/></g>
<g id="tile4" fill="#96ffbe"><path d="M -76.249,24.775 L -0.001,-30.623 L 29.124,-9.463 L 40.249,24.775 Z"/></g>
<g id="tile5" fill="#96ffbe"><path d="M -55.468,19.14 L 2.781,-23.18 L 31.906,-2.02 L 2.781,19.14 Z"/></g>
</defs>
<g class="etdrop" stroke="#777" stroke-width="2" stroke-linejoin="round" transform="translate(240,240) scale(1,1)" id="etanidrop"></g>
</svg>
</div>
<div id="panel-p1" class="panel turn-active">
<div class="panel-section score-area">
<div class="score-num" id="score1">0</div>
<svg class="ai-btn" id="ai1" viewBox="0 0 24 24" onclick="toggleAI(1)"><path d="M21 9h-2V7c0-1.1-.9-2-2-2h-3.5l-1-2h-1l-1 2H7c-1.1 0-2 .9-2 2v2H3v2h2v4H3v2h2v2c0 1.1.9 2 2 2h10c1.1 0 2-.9 2-2v-2h2v-2h-2v-4h2V9zm-4 10H7V7h10v12zM9 9h2v2H9V9zm4 0h2v2h-2V9zm-4 4h6v2H9v-2z"/></svg>
</div>
<div class="panel-section tile-wrapper" onclick="selectTile(1, 'tile0')">
<div class="tile-box" id="box-tile0"><svg viewBox="-100 -100 200 200"><use href="#tile0" transform="scale(1,1)"/></svg></div>
<div class="tile-count" id="count-tile0">6</div>
</div>
<div class="panel-section tile-wrapper" onclick="selectTile(1, 'tile1')">
<div class="tile-box" id="box-tile1"><svg viewBox="-100 -100 200 200"><use href="#tile1" transform="scale(1,1)"/></svg></div>
<div class="tile-count" id="count-tile1">6</div>
</div>
<div class="panel-section tile-wrapper" onclick="selectTile(1, 'tile2')">
<div class="tile-box" id="box-tile2"><svg viewBox="-100 -100 200 200"><use href="#tile2" transform="scale(1,1)"/></svg></div>
<div class="tile-count" id="count-tile2">6</div>
</div>
<div class="panel-section action-area">
<div class="action-btn disabled" id="btn-cross" onclick="actionCross()">
<svg viewBox="0 0 24 24"><path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"/></svg>
</div>
<div class="action-btn disabled" id="btn-check" onclick="actionCheck()">
<svg viewBox="0 0 24 24"><path d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z"/></svg>
</div>
</div>
</div>
<script>
// =============================================================================
// EXACT ALGEBRAIC COORDINATE SYSTEM FOR GOLDEN-RATIO QUADRILATERAL TILING
// (Integrated from tiling-exact-geometry2.js)
// =============================================================================
const ZERO = [0, 0, 0, 0];
const ONE = [1, 0, 0, 0];
const ZETA_POW = [
[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 1, 0], [0, 0, 0, 1], [-1, 1, -1, 1],
[-1, 0, 0, 0], [0, -1, 0, 0], [0, 0, -1, 0], [0, 0, 0, -1], [1, -1, 1, -1]
];
function zetaPow(k) { return ZETA_POW[((k % 10) + 10) % 10]; }
function ringAdd(p, q) { return [p[0]+q[0], p[1]+q[1], p[2]+q[2], p[3]+q[3]]; }
function ringSub(p, q) { return [p[0]-q[0], p[1]-q[1], p[2]-q[2], p[3]-q[3]]; }
function ringScale(p, k) { return [p[0]*k, p[1]*k, p[2]*k, p[3]*k]; }
function ringMul(p, q) {
const [p0, p1, p2, p3] = p, [q0, q1, q2, q3] = q;
const c0 = p0*q0, c1 = p0*q1+p1*q0, c2 = p0*q2+p1*q1+p2*q0, c3 = p0*q3+p1*q2+p2*q1+p3*q0;
const c4 = p1*q3+p2*q2+p3*q1, c5 = p2*q3+p3*q2, c6 = p3*q3;
let result = [c0, c1, c2, c3];
result = ringAdd(result, ringScale(ZETA_POW[4], c4));
result = ringAdd(result, ringScale(ZETA_POW[5], c5));
result = ringAdd(result, ringScale(ZETA_POW[6], c6));
return result;
}
function conjugate([a, b, c, d]) {
let result = ringScale(ONE, a);
result = ringAdd(result, ringScale(ZETA_POW[9], b));
result = ringAdd(result, ringScale(ZETA_POW[8], c));
result = ringAdd(result, ringScale(ZETA_POW[7], d));
return result;
}
function pointsEqual(p, q) { return p[0]===q[0] && p[1]===q[1] && p[2]===q[2] && p[3]===q[3]; }
function isImaginaryPartZero([, b, c, d]) { return b === 0 && c + d === 0; }
function realPartExact([a, b, c, d]) { return { intPart: 2*a+c-d, phiPart: b+c-d }; }
const PHI_NUMERIC = (1 + Math.sqrt(5)) / 2;
function phiFormToNumber({ intPart, phiPart }) { return (intPart + phiPart * PHI_NUMERIC) / 2; }
const PHI = ringAdd(ZETA_POW[1], ZETA_POW[9]);
const PHI2 = ringMul(PHI, PHI);
const TWO = ringScale(ONE, 2);
const TWO_PHI = ringScale(PHI, 2);
const LENGTHS = { 1: ONE, 2: TWO, phi: PHI, phi2: PHI2, twoPhi: TWO_PHI };
function edgeVector(lengthKey, angleIndex) { return ringMul(LENGTHS[lengthKey], zetaPow(angleIndex)); }
const SHAPES = {
A: { edges: [{length:'phi2',turn:2}, {length:1,turn:1}, {length:1,turn:3}, {length:'twoPhi',turn:4}] },
B: { edges: [{length:'phi',turn:1}, {length:1,turn:2}, {length:1,turn:3}, {length:'phi2',turn:4}] },
C: { edges: [{length:2,turn:2}, {length:1,turn:3}, {length:1,turn:1}, {length:'phi',turn:4}] }
};
function buildShapeVertices(origin, startAngle, template, mirrored = false) {
const vertices = [origin];
let pos = origin, angle = startAngle;
for (let i = 0; i < template.edges.length; i++) {
const edge = template.edges[i];
pos = ringAdd(pos, edgeVector(edge.length, angle));
angle = angle + (mirrored ? -edge.turn : edge.turn);
if (i < template.edges.length - 1) vertices.push(pos);
}
return { vertices, closureCheck: pos };
}
function attachByEdge(templateKey, mirrored, edgeIndexToAlign, worldP, worldQ) {
const template = SHAPES[templateKey];
const { vertices: localVerts } = buildShapeVertices(ZERO, 0, template, mirrored);
const i = edgeIndexToAlign, j = (i + 1) % localVerts.length;
const localEdgeVec = ringSub(localVerts[j], localVerts[i]);
const targetVec = ringSub(worldP, worldQ);
let foundK = null;
for (let k = 0; k < 10; k++) {
if (pointsEqual(ringMul(localEdgeVec, zetaPow(k)), targetVec)) { foundK = k; break; }
}
if (foundK === null) return null;
const worldVerts = localVerts.map(v => ringAdd(worldQ, ringMul(ringSub(v, localVerts[i]), zetaPow(foundK))));
return { vertices: worldVerts, rotation: foundK, mirrored };
}
function exactOrientation(S, E, P) {
const crossElem = ringMul(conjugate(ringSub(E, S)), ringSub(P, S));
if (isImaginaryPartZero(crossElem)) return 0;
const val = crossElem[1] + (crossElem[2] + crossElem[3]) * PHI_NUMERIC;
return val > 0 ? 1 : -1;
}
function segmentsCrossStrict(S1, E1, S2, E2) {
const o1 = exactOrientation(S1, E1, S2), o2 = exactOrientation(S1, E1, E2);
const o3 = exactOrientation(S2, E2, S1), o4 = exactOrientation(S2, E2, E1);
return o1 !== o2 && o1 !== 0 && o2 !== 0 && o3 !== o4 && o3 !== 0 && o4 !== 0;
}
function pointStrictlyInConvexPolygon(P, vertices) {
let expectedSign = 0;
for (let i = 0; i < vertices.length; i++) {
const o = exactOrientation(vertices[i], vertices[(i + 1) % vertices.length], P);
if (o === 0) return false;
if (expectedSign === 0) expectedSign = o;
else if (o !== expectedSign) return false;
}
return true;
}
function pointOnOpenSegment(P, S, E) {
if (pointsEqual(P, S) || pointsEqual(P, E)) return false;
const v = ringSub(E, S), w = ringSub(P, S);
if (!isImaginaryPartZero(ringMul(conjugate(v), w))) return false;
const t = phiFormToNumber(realPartExact(ringMul(w, conjugate(v)))) / phiFormToNumber(realPartExact(ringMul(v, conjugate(v))));
return t > 0 && t < 1;
}
function segmentsPerfectlyMatch(S1, E1, S2, E2) {
return (pointsEqual(S1, S2) && pointsEqual(E1, E2)) || (pointsEqual(S1, E2) && pointsEqual(E1, S2));
}
function segmentsOverlap1D(S1, E1, S2, E2) {
if (segmentsPerfectlyMatch(S1, E1, S2, E2)) return true;
return pointOnOpenSegment(S1, S2, E2) || pointOnOpenSegment(E1, S2, E2) || pointOnOpenSegment(S2, S1, E1) || pointOnOpenSegment(E2, S1, E1);
}
function shapesOverlap(shapeA, shapeB) {
const vA = shapeA.vertices, vB = shapeB.vertices;
for (let i = 0; i < vA.length; i++)
for (let j = 0; j < vB.length; j++)
if (segmentsCrossStrict(vA[i], vA[(i+1)%vA.length], vB[j], vB[(j+1)%vB.length])) return true;
if (pointStrictlyInConvexPolygon(vA[0], vB) || pointStrictlyInConvexPolygon(vB[0], vA)) return true;
return false;
}
// =============================================================================
// GAME LOGIC & RENDERING
// =============================================================================
const SVG_BASE_POINTS = {
'tile0': [{x:-61.687, y:17.119}, {x:-14.563, y:-17.119}, {x:21.437, y:-17.119}], // Shape B
'tile1': [{x:-76.249, y:24.775}, {x:-0.001, y:-30.623}, {x:29.124, y:-9.463}], // Shape A
'tile2': [{x:-55.468, y:19.14}, {x:2.781, y:-23.18}, {x:31.906, y:-2.02}], // Shape C
'tile3': [{x:-61.687, y:17.119}, {x:-14.563, y:-17.119}, {x:21.437, y:-17.119}],
'tile4': [{x:-76.249, y:24.775}, {x:-0.001, y:-30.623}, {x:29.124, y:-9.463}],
'tile5': [{x:-55.468, y:19.14}, {x:2.781, y:-23.18}, {x:31.906, y:-2.02}]
};
const SHAPE_MAP = { tile0: 'B', tile1: 'A', tile2: 'C', tile3: 'B', tile4: 'A', tile5: 'C' };
let pieces = [];
let tempPieces = [];
let ghosts = [];
let piecesCount = { tile0: 6, tile1: 6, tile2: 6, tile3: 6, tile4: 6, tile5: 6 };
let isFlipped = { tile0: false, tile1: false, tile2: false, tile3: true, tile4: true, tile5: true };
let currentPlayer = 1;
let turnNumber = 1;
let scores = { 1: 0, 2: 0 };
let scoredVictims = new Set();
let aiEnabled = { 1: false, 2: false };
let selectedTile = null;
let targetOpponentPieceId = null;
let boardTransform = { tx: 240, ty: 240, scale: 1 };
let lastPlacedPiece = null;
const etanidrop = document.getElementById('etanidrop');
const boardSvg = document.getElementById('etanitrigolden');
function exactToScreen(p) {
const PI5 = Math.PI / 5;
const [a, b, c, d] = p;
const x = a + b*Math.cos(PI5) + c*Math.cos(2*PI5) + d*Math.cos(3*PI5);
const y = b*Math.sin(PI5) + c*Math.sin(2*PI5) + d*Math.sin(3*PI5);
return { x: 36 * x, y: -36 * y };
}
function getTransformString(svgId, exactVertices) {
const S = SVG_BASE_POINTS[svgId];
const T = exactVertices.map(exactToScreen);
const dxS1 = S[1].x - S[0].x, dyS1 = S[1].y - S[0].y;
const dxS2 = S[2].x - S[0].x, dyS2 = S[2].y - S[0].y;
const dxT1 = T[1].x - T[0].x, dyT1 = T[1].y - T[0].y;
const dxT2 = T[2].x - T[0].x, dyT2 = T[2].y - T[0].y;
const det = dxS1 * dyS2 - dyS1 * dxS2;
const a = (dxT1 * dyS2 - dxT2 * dyS1) / det;
const b = (dyT1 * dyS2 - dyT2 * dyS1) / det;
const c = (dxS1 * dxT2 - dxS2 * dxT1) / det;
const d = (dxS1 * dyT2 - dxS2 * dyT1) / det;
const tx = T[0].x - a * S[0].x - c * S[0].y;
const ty = T[0].y - b * S[0].x - d * S[0].y;
let deg = Math.atan2(b, d) * 180 / Math.PI;
let sx = (a * d - b * c) < 0 ? -1 : 1;
return `translate(${tx.toFixed(3)},${ty.toFixed(3)}) scale(${sx},1) rotate(${deg.toFixed(3)})`;
}
function updateUI() {
['tile0','tile1','tile2','tile3','tile4','tile5'].forEach(t => {
document.getElementById('count-'+t).innerText = piecesCount[t];
document.getElementById('box-'+t).classList.remove('selected');
const boxSvgUse = document.querySelector(`#box-${t} use`);
boxSvgUse.setAttribute('transform', isFlipped[t] ? 'scale(-1,1)' : 'scale(1,1)');
});
document.getElementById('score1').innerText = scores[1];
document.getElementById('score2').innerText = scores[2];
if (selectedTile) document.getElementById('box-'+selectedTile).classList.add('selected');
document.getElementById('panel-p1').classList.toggle('turn-active', currentPlayer === 1);
document.getElementById('panel-p2').classList.toggle('turn-active', currentPlayer === 2);
const cross = document.getElementById('btn-cross');
const check = document.getElementById('btn-check');
if (tempPieces.length > 0 || selectedTile) cross.classList.remove('disabled');
else cross.classList.add('disabled');
let requiredPieces = (turnNumber === 1 || Object.values(piecesCount).reduce((a,b)=>a+b) === 1) ? 1 : 2;
if (tempPieces.length === requiredPieces) check.classList.remove('disabled');
else check.classList.add('disabled');
// Move actions to active player panel for desktop (optional, handled by flex order visually)
// Actually, cross and check buttons are static in P1 panel by default.
// If mobile, they are in the bottom, which is P1.
// To strictly follow, just keep them there but functionally they serve the active player.
}
function renderBoard() {
etanidrop.innerHTML = '';
pieces.concat(tempPieces).forEach(p => {
const use = document.createElementNS('http://www.w3.org/2000/svg', 'use');
use.setAttribute('href', '#' + p.svgId);
use.setAttribute('transform', getTransformString(p.svgId, p.vertices));
use.classList.add('placed-piece');
if (p.owner !== currentPlayer && tempPieces.length < ((turnNumber===1||pieces.length===35)?1:2)) {
use.classList.remove('placed-piece');
use.classList.add('board-target');
use.onclick = (e) => { e.stopPropagation(); handleTargetClick(p.id); };
}
etanidrop.appendChild(use);
});
ghosts.forEach((g, i) => {
const use = document.createElementNS('http://www.w3.org/2000/svg', 'use');
use.setAttribute('href', '#' + g.svgId);
use.setAttribute('transform', getTransformString(g.svgId, g.vertices));
use.classList.add('ghost-piece');
use.onclick = (e) => { e.stopPropagation(); commitGhost(i); };
etanidrop.appendChild(use);
});
if (targetOpponentPieceId) {
let tp = pieces.find(x => x.id === targetOpponentPieceId);
if (tp) {
const centerExact = [
tp.vertices.reduce((s,v)=>s+v[0],0)/4,
tp.vertices.reduce((s,v)=>s+v[1],0)/4,
tp.vertices.reduce((s,v)=>s+v[2],0)/4,
tp.vertices.reduce((s,v)=>s+v[3],0)/4
];
const c = exactToScreen(centerExact);
const circle = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
circle.setAttribute('cx', c.x.toFixed(3)); circle.setAttribute('cy', c.y.toFixed(3));
circle.setAttribute('r', '3'); circle.setAttribute('fill', '#00bfff'); circle.setAttribute('stroke', 'none');
etanidrop.appendChild(circle);
}
}
if (lastPlacedPiece || (tempPieces.length > 0 && tempPieces[tempPieces.length-1])) {
let tp = tempPieces.length > 0 ? tempPieces[tempPieces.length-1] : lastPlacedPiece;
const centerExact = [
tp.vertices.reduce((s,v)=>s+v[0],0)/4,
tp.vertices.reduce((s,v)=>s+v[1],0)/4,
tp.vertices.reduce((s,v)=>s+v[2],0)/4,
tp.vertices.reduce((s,v)=>s+v[3],0)/4
];
const c = exactToScreen(centerExact);
const circle = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
circle.setAttribute('cx', c.x.toFixed(3)); circle.setAttribute('cy', c.y.toFixed(3));
circle.setAttribute('r', '3'); circle.setAttribute('fill', '#ffffff'); circle.setAttribute('stroke', 'none');
etanidrop.appendChild(circle);
}
}
function selectTile(player, tileId) {
if (player !== currentPlayer) return;
if (piecesCount[tileId] <= 0) return;
let requiredPieces = (turnNumber === 1 || pieces.length === 35) ? 1 : 2;
if (tempPieces.length >= requiredPieces) return;
if (selectedTile === tileId) {
isFlipped[tileId] = !isFlipped[tileId];
} else {
selectedTile = tileId;
}
ghosts = [];
// First turn logic
if (pieces.length === 0 && tempPieces.length === 0) {
let shape = SHAPES[SHAPE_MAP[tileId]];
let { vertices } = buildShapeVertices(ZERO, 0, shape, isFlipped[tileId]);
tempPieces = [{
id: 'p_' + Date.now(),
owner: currentPlayer,
type: SHAPE_MAP[tileId],
svgId: tileId,
isFlipped: isFlipped[tileId],
vertices: vertices
}];
selectedTile = null;
} else if (targetOpponentPieceId) {
generateGhosts();
}
updateUI();
renderBoard();
}
function handleTargetClick(pieceId) {
if (!selectedTile) return;
// Check if legal target (Exceptions logic)
let p = pieces.find(x => x.id === pieceId);
if (!p || p.owner === currentPlayer) return;
if (tempPieces.length === 1 && turnNumber > 1) {
let firstScored = checkTriGoldenScoring(tempPieces[0], true);
if (pieceId !== tempPieces[0].targetId && !firstScored) {
targetOpponentPieceId = pieceId; // temporary, will filter ghosts
} else {
targetOpponentPieceId = pieceId;
}
} else {
targetOpponentPieceId = pieceId;
}
generateGhosts();
updateUI();
renderBoard();
}
function generateGhosts() {
ghosts = [];
if (!selectedTile || !targetOpponentPieceId) return;
const tPiece = pieces.find(p => p.id === targetOpponentPieceId);
const selType = SHAPE_MAP[selectedTile];
const selMir = isFlipped[selectedTile];
let req2ndSame = false;
let restrictToScoring = false;
if (tempPieces.length === 1) {
let firstScored = checkTriGoldenScoring(tempPieces[0], true);
if (!firstScored && targetOpponentPieceId !== tempPieces[0].targetId) {
restrictToScoring = true;
}
}
let allP = pieces.concat(tempPieces);
for (let i=0; i<4; i++) {
for (let j=0; j<4; j++) {
let res = attachByEdge(selType, selMir, j, tPiece.vertices[i], tPiece.vertices[(i+1)%4]);
if (res) {
let gp = { vertices: res.vertices, type: selType, owner: currentPlayer, svgId: selectedTile };
// Validity checks
let valid = true;
let overlapArea = false;
let edgeOverlapCount = 0;
let perfectMatchCount = 0;
for (let exist of allP) {
if (shapesOverlap(gp, exist)) { valid = false; break; }
for (let ei=0; ei<4; ei++) {
for (let ej=0; ej<4; ej++) {
if (segmentsOverlap1D(gp.vertices[ei], gp.vertices[(ei+1)%4], exist.vertices[ej], exist.vertices[(ej+1)%4])) {
edgeOverlapCount++;
if (segmentsPerfectlyMatch(gp.vertices[ei], gp.vertices[(ei+1)%4], exist.vertices[ej], exist.vertices[(ej+1)%4])) {
perfectMatchCount++;
}
}
}
}
}
if (!valid || edgeOverlapCount !== 1 || perfectMatchCount !== 1) continue;
if (restrictToScoring && !checkTriGoldenScoring(gp, true)) continue;
ghosts.push(gp);
}
}
}
}
function commitGhost(index) {
let gp = ghosts[index];
gp.id = 'p_' + Date.now();
gp.targetId = targetOpponentPieceId;
tempPieces.push(gp);
piecesCount[selectedTile]--;
selectedTile = null;
targetOpponentPieceId = null;
ghosts = [];
updateUI();
renderBoard();
}
function actionCross() {
if (document.getElementById('btn-cross').classList.contains('disabled')) return;
// Restore counts
tempPieces.forEach(tp => { piecesCount[tp.svgId]++; });
tempPieces = [];
selectedTile = null;
targetOpponentPieceId = null;
ghosts = [];
updateUI();
renderBoard();
}
function actionCheck() {
if (document.getElementById('btn-check').classList.contains('disabled')) return;
// Compute scores
let allNewP = pieces.concat(tempPieces);
let currentScoredVictims = [];
for (let A of allNewP) {
for (let B of allNewP) {
if (A.id === B.id) continue;
for (let v of A.vertices) {
for (let e=0; e<4; e++) {
if (pointOnOpenSegment(v, B.vertices[e], B.vertices[(e+1)%4])) {
if (!scoredVictims.has(B.id) && !currentScoredVictims.includes(B.id)) {
let d = getGraphDistance(B, A, allNewP);
if (d > 0) {
scores[A.owner] += d;
currentScoredVictims.push(B.id);
}
}
}
}
}
}
}
currentScoredVictims.forEach(v => scoredVictims.add(v));
pieces = allNewP;
lastPlacedPiece = tempPieces[tempPieces.length - 1];
tempPieces = [];
selectedTile = null;
targetOpponentPieceId = null;
ghosts = [];
turnNumber++;
currentPlayer = currentPlayer === 1 ? 2 : 1;
updateUI();
renderBoard();
checkEndGame();
if (aiEnabled[currentPlayer]) {
setTimeout(playAI, 500);
}
}
function checkTriGoldenScoring(newPiece, testOnly=false) {
let allP = pieces.concat(tempPieces).concat([newPiece]);
for (let A of allP) {
for (let B of allP) {
if (A.id === B.id) continue;
for (let v of A.vertices) {
for (let e=0; e<4; e++) {
if (pointOnOpenSegment(v, B.vertices[e], B.vertices[(e+1)%4])) {
if (!scoredVictims.has(B.id)) return true;
}
}
}
}
}
return false;
}
function getGraphDistance(start, end, allP) {
let q = [{node: start, dist: 0}];
let visited = new Set([start.id]);
while(q.length > 0) {
let curr = q.shift();
if (curr.node.id === end.id) return curr.dist - 1;
for (let next of allP) {
if (!visited.has(next.id)) {
let connected = false;
for(let i=0;i<4;i++) for(let j=0;j<4;j++) {
if(segmentsPerfectlyMatch(curr.node.vertices[i], curr.node.vertices[(i+1)%4], next.vertices[j], next.vertices[(j+1)%4])) connected=true;
}
if (connected) {
visited.add(next.id);
q.push({node: next, dist: curr.dist + 1});
}
}
}
}
return 0;
}
function toggleAI(player) {
aiEnabled[player] = !aiEnabled[player];
document.getElementById('ai'+player).classList.toggle('active', aiEnabled[player]);
if (currentPlayer === player && aiEnabled[player]) playAI();
}
function playAI() {
if (!aiEnabled[currentPlayer] || pieces.length === 36) return;
let availTiles = [0,1,2].map(x => currentPlayer===1 ? 'tile'+x : 'tile'+(x+3)).filter(t => piecesCount[t] > 0);
if (availTiles.length === 0) return;
let reqPieces = (turnNumber === 1 || pieces.length === 35) ? 1 : 2;
for (let m = 0; m < reqPieces; m++) {
availTiles = [0,1,2].map(x => currentPlayer===1 ? 'tile'+x : 'tile'+(x+3)).filter(t => piecesCount[t] > 0);
let t = availTiles[Math.floor(Math.random() * availTiles.length)];
selectTile(currentPlayer, t);
if (turnNumber === 1) {
actionCheck();
return;
}
let oppPieces = pieces.filter(p => p.owner !== currentPlayer);
let targets = [...oppPieces].sort(()=>Math.random()-0.5);
let placed = false;
for (let tp of targets) {
handleTargetClick(tp.id);
if (ghosts.length > 0) {
commitGhost(Math.floor(Math.random() * ghosts.length));
placed = true;
break;
}
}
if (!placed) { actionCross(); return; } // AI failed to find move
}
actionCheck();
}
function checkEndGame() {
if (pieces.length === 36) {
let msg = `遊戲結束!\n總分: 先手方 ${scores[1]} - 後手方 ${scores[2]}\n`;
if (scores[1] > scores[2]) msg += "先手方贏了!";
else if (scores[2] > scores[1]) msg += "後手方贏了!";
else msg += "平局!";
if (confirm(msg + "\n\n是否換先再來一局?")) {
resetGame(true);
}
}
}
function resetGame(swap) {
pieces = []; tempPieces = []; ghosts = [];
piecesCount = { tile0: 6, tile1: 6, tile2: 6, tile3: 6, tile4: 6, tile5: 6 };
isFlipped = { tile0: false, tile1: false, tile2: false, tile3: true, tile4: true, tile5: true };
turnNumber = 1;
scoredVictims.clear();
lastPlacedPiece = null;
if (swap) {
// Logically swap player 1 and 2, UI remains in place
scores = { 1: scores[2], 2: scores[1] };
} else {
scores = { 1: 0, 2: 0 };
}
currentPlayer = 1;
boardTransform = { tx: 240, ty: 240, scale: 1 };
updateTransform();
updateUI();
renderBoard();
}
// Pan & Zoom
let isDragging = false;
let startX, startY;
boardSvg.addEventListener('pointerdown', e => {
if (pieces.length === 0 && tempPieces.length === 0) return;
isDragging = true;
startX = e.clientX - boardTransform.tx;
startY = e.clientY - boardTransform.ty;
});
boardSvg.addEventListener('pointermove', e => {
if (!isDragging) return;
boardTransform.tx = e.clientX - startX;
boardTransform.ty = e.clientY - startY;
updateTransform();
});
boardSvg.addEventListener('pointerup', () => isDragging = false);
boardSvg.addEventListener('pointerleave', () => isDragging = false);
boardSvg.addEventListener('wheel', e => {
e.preventDefault();
if (pieces.length === 0 && tempPieces.length === 0) return;
const ds = e.deltaY > 0 ? 0.9 : 1.1;
const nextScale = boardTransform.scale * ds;
if (nextScale < 0.2 || nextScale > 5) return;
const rect = boardSvg.getBoundingClientRect();
const mx = e.clientX - rect.left;
const my = e.clientY - rect.top;
boardTransform.tx = mx - (mx - boardTransform.tx) * ds;
boardTransform.ty = my - (my - boardTransform.ty) * ds;
boardTransform.scale = nextScale;
updateTransform();
});
let pinchStartDist = 0;
boardSvg.addEventListener('touchstart', e => {
if (e.touches.length === 2 && (pieces.length > 0 || tempPieces.length > 0)) {
isDragging = false;
pinchStartDist = Math.hypot(e.touches[0].clientX - e.touches[1].clientX, e.touches[0].clientY - e.touches[1].clientY);
}
});
boardSvg.addEventListener('touchmove', e => {
if (e.touches.length === 2 && (pieces.length > 0 || tempPieces.length > 0)) {
e.preventDefault();
const dist = Math.hypot(e.touches[0].clientX - e.touches[1].clientX, e.touches[0].clientY - e.touches[1].clientY);
const ds = dist / pinchStartDist;
const nextScale = boardTransform.scale * ds;
if (nextScale > 0.2 && nextScale < 5) {
const mx = (e.touches[0].clientX + e.touches[1].clientX) / 2 - boardSvg.getBoundingClientRect().left;
const my = (e.touches[0].clientY + e.touches[1].clientY) / 2 - boardSvg.getBoundingClientRect().top;
boardTransform.tx = mx - (mx - boardTransform.tx) * ds;
boardTransform.ty = my - (my - boardTransform.ty) * ds;
boardTransform.scale = nextScale;
updateTransform();
}
pinchStartDist = dist;
}
}, {passive: false});
function updateTransform() {
etanidrop.setAttribute('transform', `translate(${boardTransform.tx.toFixed(3)},${boardTransform.ty.toFixed(3)}) scale(${boardTransform.scale.toFixed(3)})`);
}
// Initialize
updateUI();
renderBoard();
</script>
</body>
</html>
```https://ejsoon.vip/
弈趣極光:享受思維樂趣
弈趣極光:享受思維樂趣
Re: 一個新的遊戲創意
修復與改進:
一,先手首回合的棋子應可更換或翻轉
當先手首回合,點擊一個棋子後,這個棋子將落在棋盤中心。這時如果再次點擊該棋子,則它將翻轉,如果點擊其它棋子,則棋盤中的棋子將被更換。
二,修復邊與邊貼合的邏輯
當在後手方的回合往後,一方點擊一個己方棋子後,再點擊一個對方場上已有棋子,所展示的預放棋子會出錯,比如該顯示的未能顯示,面積重疊。修復這個問題。
將預放棋子的fill-opacity設為0.36。
需要明確給出所要修改的地方和所要替換的代碼。
gemimi 3.1 pro + thinking
一,先手首回合的棋子應可更換或翻轉
當先手首回合,點擊一個棋子後,這個棋子將落在棋盤中心。這時如果再次點擊該棋子,則它將翻轉,如果點擊其它棋子,則棋盤中的棋子將被更換。
二,修復邊與邊貼合的邏輯
當在後手方的回合往後,一方點擊一個己方棋子後,再點擊一個對方場上已有棋子,所展示的預放棋子會出錯,比如該顯示的未能顯示,面積重疊。修復這個問題。
將預放棋子的fill-opacity設為0.36。
需要明確給出所要修改的地方和所要替換的代碼。
代码: 全选
修復與改進:
一,先手首回合的棋子應可更換或翻轉
當先手首回合,點擊一個棋子後,這個棋子將落在棋盤中心。這時如果再次點擊該棋子,則它將翻轉,如果點擊其它棋子,則棋盤中的棋子將被更換。
二,修復邊與邊貼合的邏輯
當在後手方的回合往後,一方點擊一個己方棋子後,再點擊一個對方場上已有棋子,所展示的預放棋子會出錯,比如該顯示的未能顯示,面積重疊。修復這個問題。
將預放棋子的fill-opacity設為0.36。
需要明確給出所要修改的地方和所要替換的代碼。代码: 全选
這幾個問題確實會大幅影響遊戲的流暢度與體驗,特別是第一步無法反悔或翻轉,以及後續邊緣貼合邏輯出錯導致無法正常放置棋子,這些都是在開發這類幾何拼接遊戲時常遇到的痛點。
這就為你提供精確的修復與改進方案。我們總共需要修改三個地方:
### 一、 修復並改進 `selectTile` (先手首回合可更換與翻轉)
原本的代碼在第一步時,選中棋子後就直接把 `selectedTile` 設為 `null`,這導致無法再次點擊觸發翻轉,且沒有正確扣除與退還棋子數量(會導致取消時棋子數量錯誤增加)。
請找到代碼中的 `function selectTile(player, tileId)`,將整個函數替換為以下代碼:
```javascript
function selectTile(player, tileId) {
if (player !== currentPlayer) return;
if (piecesCount[tileId] <= 0) return;
let requiredPieces = (turnNumber === 1 || pieces.length === 35) ? 1 : 2;
// 首回合第一步邏輯:允許無限次切換與翻轉
if (pieces.length === 0) {
// 退回前一次選擇的棋子數量(如果有的話)
if (tempPieces.length > 0) {
piecesCount[tempPieces[0].svgId]++;
}
if (selectedTile === tileId) {
isFlipped[tileId] = !isFlipped[tileId];
} else {
selectedTile = tileId;
}
ghosts = [];
let shape = SHAPES[SHAPE_MAP[tileId]];
let { vertices } = buildShapeVertices(ZERO, 0, shape, isFlipped[tileId]);
tempPieces = [{
id: 'p_' + Date.now(),
owner: currentPlayer,
type: SHAPE_MAP[tileId],
svgId: tileId,
isFlipped: isFlipped[tileId],
vertices: vertices
}];
// 扣除目前選擇的棋子數量
piecesCount[tileId]--;
} else {
if (tempPieces.length >= requiredPieces) return;
if (selectedTile === tileId) {
isFlipped[tileId] = !isFlipped[tileId];
} else {
selectedTile = tileId;
}
ghosts = [];
if (targetOpponentPieceId) {
generateGhosts();
}
}
updateUI();
renderBoard();
}
```
---
### 二、 修復 `generateGhosts` (邊與邊貼合判斷邏輯)
原代碼中要求 `edgeOverlapCount !== 1 || perfectMatchCount !== 1`。這個邏輯太過嚴苛,當一個預放棋子剛好卡在角落並完美貼合「兩個」敵方棋子時,這個數值會變成 2,反而會被判定為無效並過濾掉。正確的邏輯應該是:必須有完美貼合 (`perfectMatchCount > 0`),且所有邊緣重疊都必須是完美貼合 (`edgeOverlapCount === perfectMatchCount`),這樣就能避免部分面積重合或判定失效。
請在 `function generateGhosts()` 中找到以下這段驗證區塊:
```javascript
let valid = true;
let overlapArea = false;
let edgeOverlapCount = 0;
let perfectMatchCount = 0;
for (let exist of allP) {
if (shapesOverlap(gp, exist)) { valid = false; break; }
for (let ei=0; ei<4; ei++) {
for (let ej=0; ej<4; ej++) {
if (segmentsOverlap1D(gp.vertices[ei], gp.vertices[(ei+1)%4], exist.vertices[ej], exist.vertices[(ej+1)%4])) {
edgeOverlapCount++;
if (segmentsPerfectlyMatch(gp.vertices[ei], gp.vertices[(ei+1)%4], exist.vertices[ej], exist.vertices[(ej+1)%4])) {
perfectMatchCount++;
}
}
}
}
}
if (!valid || edgeOverlapCount !== 1 || perfectMatchCount !== 1) continue;
```
**將上述整段替換為:**
```javascript
let valid = true;
let edgeOverlapCount = 0;
let perfectMatchCount = 0;
for (let exist of allP) {
if (shapesOverlap(gp, exist)) { valid = false; break; }
for (let ei=0; ei<4; ei++) {
for (let ej=0; ej<4; ej++) {
if (segmentsOverlap1D(gp.vertices[ei], gp.vertices[(ei+1)%4], exist.vertices[ej], exist.vertices[(ej+1)%4])) {
edgeOverlapCount++;
if (segmentsPerfectlyMatch(gp.vertices[ei], gp.vertices[(ei+1)%4], exist.vertices[ej], exist.vertices[(ej+1)%4])) {
perfectMatchCount++;
}
}
}
}
}
if (!valid || perfectMatchCount === 0 || edgeOverlapCount !== perfectMatchCount) continue;
```
---
### 三、 修改 `renderBoard` (為預放棋子添加半透明效果)
為了區別已經確認擺放的棋子與當前正在操作的預放棋子 (`tempPieces`),我們在渲染生成 SVG `use` 標籤時,直接給預放棋子加上透明度。
請在 `function renderBoard()` 中找到以下這段代碼:
```javascript
pieces.concat(tempPieces).forEach(p => {
const use = document.createElementNS('http://www.w3.org/2000/svg', 'use');
use.setAttribute('href', '#' + p.svgId);
use.setAttribute('transform', getTransformString(p.svgId, p.vertices));
use.classList.add('placed-piece');
if (p.owner !== currentPlayer && tempPieces.length < ((turnNumber===1||pieces.length===35)?1:2)) {
use.classList.remove('placed-piece');
use.classList.add('board-target');
use.onclick = (e) => { e.stopPropagation(); handleTargetClick(p.id); };
}
etanidrop.appendChild(use);
});
```
**將上述整段替換為:**
```javascript
pieces.concat(tempPieces).forEach(p => {
const use = document.createElementNS('http://www.w3.org/2000/svg', 'use');
use.setAttribute('href', '#' + p.svgId);
use.setAttribute('transform', getTransformString(p.svgId, p.vertices));
// 判斷是否為尚未確認的預放棋子
if (tempPieces.includes(p)) {
use.setAttribute('fill-opacity', '0.36');
} else {
use.classList.add('placed-piece');
}
if (p.owner !== currentPlayer && tempPieces.length < ((turnNumber===1||pieces.length===35)?1:2)) {
use.classList.remove('placed-piece');
use.classList.add('board-target');
use.onclick = (e) => { e.stopPropagation(); handleTargetClick(p.id); };
}
etanidrop.appendChild(use);
});
```https://ejsoon.vip/
弈趣極光:享受思維樂趣
弈趣極光:享受思維樂趣
Re: 一個新的遊戲創意
以下說法是錯誤的:原代碼中要求 edgeOverlapCount !== 1 || perfectMatchCount !== 1。這個邏輯太過嚴苛,當一個預放棋子剛好卡在角落並完美貼合「兩個」敵方棋子時,這個數值會變成 2,反而會被判定為無效並過濾掉。正確的邏輯應該是:必須有完美貼合 (perfectMatchCount > 0),且所有邊緣重疊都必須是完美貼合 (edgeOverlapCount === perfectMatchCount),這樣就能避免部分面積重合或判定失效。
因為
本棋的規則是,所放下的棋子能且只能與場上對方棋子的一條邊完全貼合。
gemini 3.1 pro
因為
本棋的規則是,所放下的棋子能且只能與場上對方棋子的一條邊完全貼合。
gemini 3.1 pro
代码: 全选
感謝你的指正與對遊戲規則的深入分析!你提到的這一點非常關鍵。
根據《頂鑫棋 (TriGolden)》的核心機制,玩家落子時必須且只能與場上對手的**某一條邊**進行完全貼合(即單邊貼合),而不能同時黏貼多個邊或跨越多個不同的棋子邊緣。
因此,原本代碼中的嚴格限制 `edgeOverlapCount !== 1 || perfectMatchCount !== 1` 在遊戲規則上是正確的設計,用以確保每次落子只精準對應一條邊。如果放寬條件,可能會違反「只能與一條邊完全貼合」的規則。
為了維持正確的遊戲規則並確保邊緣貼合邏輯無誤,以下是調整後的 `generateGhosts` 驗證邏輯:
### 修正後的 `generateGhosts` 核心判斷
請將 `generateGhosts()` 中的驗證部分保持或恢復為嚴格單邊貼合的邏輯:
```javascript
let valid = true;
let edgeOverlapCount = 0;
let perfectMatchCount = 0;
for (let exist of allP) {
if (shapesOverlap(gp, exist)) { valid = false; break; }
for (let ei=0; ei<4; ei++) {
for (let ej=0; ej<4; ej++) {
if (segmentsOverlap1D(gp.vertices[ei], gp.vertices[(ei+1)%4], exist.vertices[ej], exist.vertices[(ej+1)%4])) {
edgeOverlapCount++;
if (segmentsPerfectlyMatch(gp.vertices[ei], gp.vertices[(ei+1)%4], exist.vertices[ej], exist.vertices[(ej+1)%4])) {
perfectMatchCount++;
}
}
}
}
}
// 嚴格確保只能且必須與對手的一條邊完全貼合
if (!valid || edgeOverlapCount !== 1 || perfectMatchCount !== 1) continue;
```https://ejsoon.vip/
弈趣極光:享受思維樂趣
弈趣極光:享受思維樂趣
-
- 相似主题
- 回复总数
- 阅读次数
- 最新帖子
在线用户
正浏览此版面之用户: 没有注册用户 和 1 访客