polyclash 0.0.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. polyclash-0.0.1/LICENSE +21 -0
  2. polyclash-0.0.1/PKG-INFO +110 -0
  3. polyclash-0.0.1/README.md +93 -0
  4. polyclash-0.0.1/polyclash/__init__.py +0 -0
  5. polyclash-0.0.1/polyclash/client.py +31 -0
  6. polyclash-0.0.1/polyclash/data/__init__.py +0 -0
  7. polyclash-0.0.1/polyclash/data/data.py +87 -0
  8. polyclash-0.0.1/polyclash/game/__init__.py +0 -0
  9. polyclash-0.0.1/polyclash/game/board.py +301 -0
  10. polyclash-0.0.1/polyclash/game/controller.py +153 -0
  11. polyclash-0.0.1/polyclash/game/player.py +71 -0
  12. polyclash-0.0.1/polyclash/game/timer.py +37 -0
  13. polyclash-0.0.1/polyclash/gui/__init__.py +0 -0
  14. polyclash-0.0.1/polyclash/gui/constants.py +13 -0
  15. polyclash-0.0.1/polyclash/gui/dialogs.py +292 -0
  16. polyclash-0.0.1/polyclash/gui/main.py +179 -0
  17. polyclash-0.0.1/polyclash/gui/mesh.py +47 -0
  18. polyclash-0.0.1/polyclash/gui/overly_info.py +63 -0
  19. polyclash-0.0.1/polyclash/gui/overly_map.py +68 -0
  20. polyclash-0.0.1/polyclash/gui/view_sphere.py +158 -0
  21. polyclash-0.0.1/polyclash/server.py +274 -0
  22. polyclash-0.0.1/polyclash/util/__init__.py +0 -0
  23. polyclash-0.0.1/polyclash/util/api.py +151 -0
  24. polyclash-0.0.1/polyclash/util/logging.py +31 -0
  25. polyclash-0.0.1/polyclash/util/storage.py +395 -0
  26. polyclash-0.0.1/polyclash/workers/__init__.py +0 -0
  27. polyclash-0.0.1/polyclash/workers/ai_play.py +55 -0
  28. polyclash-0.0.1/polyclash/workers/network.py +69 -0
  29. polyclash-0.0.1/polyclash.egg-info/PKG-INFO +110 -0
  30. polyclash-0.0.1/polyclash.egg-info/SOURCES.txt +35 -0
  31. polyclash-0.0.1/polyclash.egg-info/dependency_links.txt +1 -0
  32. polyclash-0.0.1/polyclash.egg-info/entry_points.txt +3 -0
  33. polyclash-0.0.1/polyclash.egg-info/requires.txt +11 -0
  34. polyclash-0.0.1/polyclash.egg-info/top_level.txt +1 -0
  35. polyclash-0.0.1/setup.cfg +10 -0
  36. polyclash-0.0.1/setup.py +53 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Mingli Yuan
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,110 @@
1
+ Metadata-Version: 2.1
2
+ Name: polyclash
3
+ Version: 0.0.1
4
+ Summary: A python 3d spherical Go game on a snub dodecahedron board
5
+ Home-page: https://github.com/spherical-go/polyclash
6
+ Author: Mingli Yuan
7
+ Author-email: mingli.yuan@gmail.com
8
+ Project-URL: Documentation, https://github.com/spherical-go/polyclash
9
+ Project-URL: Source, https://github.com/spherical-go/polyclash
10
+ Project-URL: Tracker, https://github.com/spherical-go/polyclash/issues
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Operating System :: OS Independent
14
+ Requires-Python: >=3.10
15
+ Description-Content-Type: text/markdown
16
+ License-File: LICENSE
17
+
18
+ # PolyClash: A Go-like game on sphere by using a snub dodecahedron board
19
+
20
+ ## Introduction
21
+
22
+ Like mathematical truth, Go is an eternal game in the universe. Similarly, the snub dodecahedron is also an eternal geometric shape, which is the Archimedean polyhedron with the most sphericity.
23
+ We combine these two to create a new game: PolyClash.
24
+
25
+ Can we create a set of rules that are as simple as possible while making this game very interesting? So that this game is also an eternal game. This is our goal.
26
+
27
+ ## Install
28
+
29
+ ```bash
30
+ pip install polyclash
31
+ ```
32
+
33
+ ## Usage
34
+
35
+ The client can be started by running the following command:
36
+
37
+ ```bash
38
+ polyclash-client
39
+ ```
40
+
41
+ The client is a Qt application that allows you to play the game.
42
+
43
+ The local server can be started by running the following command:
44
+
45
+ ```bash
46
+ polyclash-server
47
+ ```
48
+
49
+ It should be noted that the server is not necessary to play the game.
50
+ The server is only needed if you want to play the game with other players in a local network.
51
+
52
+ ## Deployment on a production server
53
+
54
+ You can also set up a production server, which can be accessed by other players on the internet.
55
+ We recommend using a reverse proxy like Nginx to set up the production server, and using uwsgi or similar tools
56
+ to run the server.
57
+
58
+ below is an example of how to run the server using uwsgi:
59
+
60
+ ```bash
61
+ uwsgi --http :7763 --gevent 100 --http-websockets --master --wsgi polyclash.server:app --logto ~/.polyclash/uwsgi.log
62
+ ```
63
+
64
+ and the Nginx configuration:
65
+
66
+ ```nginx
67
+ server {
68
+ listen 80;
69
+ server_name polyclash.example.com;
70
+
71
+ location /sphgo {
72
+ proxy_pass http://127.0.0.1:7763;
73
+ proxy_set_header Host $host;
74
+ proxy_set_header X-Real-IP $remote_addr;
75
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
76
+ proxy_set_header X-Forwarded-Proto $scheme;
77
+ }
78
+
79
+ location /socket.io {
80
+ proxy_redirect off;
81
+ proxy_buffering off;
82
+
83
+ proxy_set_header Host $host;
84
+ proxy_set_header X-Real-IP $remote_addr;
85
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
86
+
87
+ proxy_http_version 1.1;
88
+ proxy_set_header Upgrade $http_upgrade;
89
+ proxy_set_header Connection "Upgrade";
90
+
91
+ proxy_pass http://127.0.0.1:7763/socket.io;
92
+ }
93
+ }
94
+ ```
95
+
96
+ ## Development
97
+
98
+ How to release a new version:
99
+
100
+ ```bash
101
+ python3 setup.py sdist bdist_wheel
102
+ python3 -m twine upload dist/*
103
+
104
+ git tag va.b.c master
105
+ git push origin va.b.c
106
+ ```
107
+
108
+ ## License
109
+
110
+ This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
@@ -0,0 +1,93 @@
1
+ # PolyClash: A Go-like game on sphere by using a snub dodecahedron board
2
+
3
+ ## Introduction
4
+
5
+ Like mathematical truth, Go is an eternal game in the universe. Similarly, the snub dodecahedron is also an eternal geometric shape, which is the Archimedean polyhedron with the most sphericity.
6
+ We combine these two to create a new game: PolyClash.
7
+
8
+ Can we create a set of rules that are as simple as possible while making this game very interesting? So that this game is also an eternal game. This is our goal.
9
+
10
+ ## Install
11
+
12
+ ```bash
13
+ pip install polyclash
14
+ ```
15
+
16
+ ## Usage
17
+
18
+ The client can be started by running the following command:
19
+
20
+ ```bash
21
+ polyclash-client
22
+ ```
23
+
24
+ The client is a Qt application that allows you to play the game.
25
+
26
+ The local server can be started by running the following command:
27
+
28
+ ```bash
29
+ polyclash-server
30
+ ```
31
+
32
+ It should be noted that the server is not necessary to play the game.
33
+ The server is only needed if you want to play the game with other players in a local network.
34
+
35
+ ## Deployment on a production server
36
+
37
+ You can also set up a production server, which can be accessed by other players on the internet.
38
+ We recommend using a reverse proxy like Nginx to set up the production server, and using uwsgi or similar tools
39
+ to run the server.
40
+
41
+ below is an example of how to run the server using uwsgi:
42
+
43
+ ```bash
44
+ uwsgi --http :7763 --gevent 100 --http-websockets --master --wsgi polyclash.server:app --logto ~/.polyclash/uwsgi.log
45
+ ```
46
+
47
+ and the Nginx configuration:
48
+
49
+ ```nginx
50
+ server {
51
+ listen 80;
52
+ server_name polyclash.example.com;
53
+
54
+ location /sphgo {
55
+ proxy_pass http://127.0.0.1:7763;
56
+ proxy_set_header Host $host;
57
+ proxy_set_header X-Real-IP $remote_addr;
58
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
59
+ proxy_set_header X-Forwarded-Proto $scheme;
60
+ }
61
+
62
+ location /socket.io {
63
+ proxy_redirect off;
64
+ proxy_buffering off;
65
+
66
+ proxy_set_header Host $host;
67
+ proxy_set_header X-Real-IP $remote_addr;
68
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
69
+
70
+ proxy_http_version 1.1;
71
+ proxy_set_header Upgrade $http_upgrade;
72
+ proxy_set_header Connection "Upgrade";
73
+
74
+ proxy_pass http://127.0.0.1:7763/socket.io;
75
+ }
76
+ }
77
+ ```
78
+
79
+ ## Development
80
+
81
+ How to release a new version:
82
+
83
+ ```bash
84
+ python3 setup.py sdist bdist_wheel
85
+ python3 -m twine upload dist/*
86
+
87
+ git tag va.b.c master
88
+ git push origin va.b.c
89
+ ```
90
+
91
+ ## License
92
+
93
+ This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
File without changes
@@ -0,0 +1,31 @@
1
+ import sys
2
+
3
+ from PyQt5.QtWidgets import QApplication
4
+
5
+ from polyclash.game.board import BLACK, WHITE
6
+ from polyclash.game.controller import SphericalGoController
7
+ from polyclash.game.player import HUMAN, AI
8
+ from polyclash.gui.main import MainWindow
9
+
10
+
11
+ if __name__ == "__main__":
12
+ app = QApplication(sys.argv)
13
+
14
+ controller = SphericalGoController()
15
+ controller.add_player(BLACK, kind=HUMAN)
16
+ controller.add_player(WHITE, kind=AI)
17
+
18
+ window = MainWindow(controller=controller)
19
+
20
+ screen = app.primaryScreen().geometry()
21
+ width = screen.width() // 5 * 4
22
+ height = screen.height() // 5 * 4
23
+ x = (screen.width() - width) // 2
24
+ y = (screen.height() - height) // 2
25
+ window.move(x, y)
26
+ window.resize(width, height)
27
+
28
+ controller.board.reset()
29
+ window.delayed_resize(width+1, height+1)
30
+ window.show()
31
+ sys.exit(app.exec_())
File without changes
@@ -0,0 +1,87 @@
1
+ import numpy as np
2
+ import pickle as pkl
3
+ import os.path as osp
4
+
5
+ from scipy.spatial import cKDTree
6
+
7
+ data_path = osp.abspath(osp.join(osp.dirname(__file__), "board.npz"))
8
+ pkl_path = osp.abspath(osp.join(osp.dirname(__file__), "board.pkl"))
9
+ idx_path = osp.abspath(osp.join(osp.dirname(__file__), "index.pkl"))
10
+
11
+ npz_data = np.load(data_path)
12
+ pentagons = npz_data['pentagons']
13
+ triangles = npz_data['triangles']
14
+ polysmalls = npz_data['polysmalls']
15
+ polylarges = npz_data['polylarges']
16
+
17
+ cities = npz_data['cities']
18
+ triangle2faces = npz_data['triangle2faces']
19
+ pentagon2faces = npz_data['pentagon2faces']
20
+
21
+ neighbors = pkl.load(open(pkl_path, "rb"))
22
+
23
+ indexer = pkl.load(open(idx_path, "rb"))
24
+
25
+ # encoding -> position
26
+ decoder = {}
27
+ for k, v in indexer.items():
28
+ if len(k) == 1:
29
+ decoder[tuple([k[0]])] = v
30
+ if len(k) == 2:
31
+ decoder[tuple([k[0], k[1]])] = v
32
+ decoder[tuple([k[1], k[0]])] = v
33
+ if len(k) == 3:
34
+ decoder[tuple([k[0], k[1], k[2]])] = v
35
+ decoder[tuple([k[1], k[2], k[0]])] = v
36
+ decoder[tuple([k[2], k[0], k[1]])] = v
37
+ if len(k) == 5:
38
+ decoder[tuple([k[0], k[1], k[2], k[3], k[4]])] = v
39
+ decoder[tuple([k[1], k[2], k[3], k[4], k[0]])] = v
40
+ decoder[tuple([k[2], k[3], k[4], k[0], k[1]])] = v
41
+ decoder[tuple([k[3], k[4], k[0], k[1], k[2]])] = v
42
+ decoder[tuple([k[4], k[0], k[1], k[2], k[3]])] = v
43
+
44
+ # position -> encoding
45
+ encoder = list([tuple() for i in range(302)])
46
+ for k, v in indexer.items():
47
+ encoder[v] = tuple(sorted(k))
48
+
49
+
50
+ # Use a kdTree manage all the cities, when a stone is placed, find the nearest city
51
+ class CityManager:
52
+ def __init__(self, cities):
53
+ self.cities = cities
54
+ self.kd_tree = cKDTree(self.cities)
55
+
56
+ def find_nearest_city(self, position):
57
+ return self.kd_tree.query(position)[1]
58
+
59
+
60
+ city_manager = CityManager(cities)
61
+
62
+
63
+ axis = np.zeros((8, 3), dtype=np.float_)
64
+ axis[0] = np.mean(cities[0:15], axis=0)
65
+ axis[1] = np.mean(cities[15:30], axis=0)
66
+ axis[2] = np.mean(cities[30:45], axis=0)
67
+ axis[3] = np.mean(cities[45:60], axis=0)
68
+ axis[4] = - axis[0]
69
+ axis[5] = - axis[1]
70
+ axis[6] = - axis[2]
71
+ axis[7] = - axis[3]
72
+ axis = axis / np.linalg.norm(axis, axis=1)[:, np.newaxis]
73
+
74
+
75
+ def get_areas():
76
+ phi = (1 + np.sqrt(5)) / 2
77
+ coefficients = np.array([1, 2, 0, -phi**2], dtype=np.float64)
78
+ roots = np.roots(coefficients)
79
+ xi = roots[np.isreal(roots)].real[0]
80
+ length = 2 * xi * np.sqrt(1 - xi)
81
+ triangle_area = np.sqrt(3) / 4 * length**2
82
+ pentagon_area = np.sqrt(25 + 10 * np.sqrt(5)) * length**2 / 4
83
+ total_area = 80 * triangle_area + 12 * pentagon_area
84
+ return triangle_area / 3, pentagon_area / 5, total_area
85
+
86
+
87
+ polysmall_area, polylarge_area, total_area = get_areas()
File without changes
@@ -0,0 +1,301 @@
1
+
2
+
3
+ import numpy as np
4
+ import math
5
+
6
+ from random import sample
7
+ from collections import OrderedDict
8
+ from polyclash.data.data import cities, neighbors, polysmalls, polylarges, polylarge_area, polysmall_area, total_area, \
9
+ encoder
10
+
11
+ BLACK = 1
12
+ WHITE = -1
13
+
14
+
15
+ def calculate_area(boarddata, piece, area):
16
+ black_area, white_area, unclaimed_area = 0, 0, 0
17
+ parties = boarddata[piece]
18
+ parties_set = set(parties)
19
+ if BLACK not in parties_set and WHITE not in parties_set:
20
+ unclaimed_area += area
21
+ if BLACK in parties_set and WHITE not in parties_set:
22
+ black_area += area
23
+ if WHITE in parties_set and BLACK not in parties_set:
24
+ white_area += area
25
+ if BLACK in parties_set and WHITE in parties_set:
26
+ black_side, white_side = 0, 0
27
+ for part in parties:
28
+ if part == BLACK:
29
+ black_side += 1
30
+ if part == WHITE:
31
+ white_side += 1
32
+ black_area += area / (black_side + white_side) * black_side
33
+ white_area += area / (black_side + white_side) * white_side
34
+ return black_area, white_area, unclaimed_area
35
+
36
+
37
+ def calculate_distance(point1, point2):
38
+ city1 = cities[point1]
39
+ city2 = cities[point2]
40
+ return np.linalg.norm(city1 - city2)
41
+
42
+
43
+ def calculate_potential(board, point, counter):
44
+ potential = 0
45
+ for i, stone in enumerate(board):
46
+ if stone != 0:
47
+ distance = calculate_distance(point, i)
48
+ if distance > 0:
49
+ potential += (1 / distance) * np.tanh(0.5 - counter / 302)
50
+ return potential
51
+
52
+
53
+ class Board:
54
+ def __init__(self):
55
+ self.board_size = 302
56
+ self.board = np.zeros([self.board_size])
57
+ self.current_player = BLACK
58
+ self.neighbors = neighbors
59
+ self.latest_player = None
60
+ self.latest_removes = [[]]
61
+ self.black_suicides = set()
62
+ self.white_suicides = set()
63
+
64
+ self.turns = OrderedDict()
65
+
66
+ self._observers = []
67
+ self.notification_enabled = True
68
+
69
+ self.simulator = None
70
+
71
+ @property
72
+ def counter(self):
73
+ return len(self.turns)
74
+
75
+ def register_observer(self, observer):
76
+ if observer not in self._observers:
77
+ self._observers.append(observer)
78
+
79
+ def unregister_observer(self, observer):
80
+ self._observers.remove(observer)
81
+
82
+ def enable_notification(self):
83
+ self.notification_enabled = True
84
+
85
+ def disable_notification(self):
86
+ self.notification_enabled = False
87
+
88
+ def notify_observers(self, message, **kwargs):
89
+ for observer in self._observers:
90
+ if self.notification_enabled:
91
+ observer.handle_notification(message, **kwargs)
92
+
93
+ def has_liberty(self, point, color=None, visited=None):
94
+ if color is None:
95
+ color = self.board[point]
96
+
97
+ if visited is None:
98
+ visited = set() # 用于记录已经检查过的点
99
+
100
+ if point in visited:
101
+ return False # 如果已经访问过这个点,不再重复检查
102
+ visited.add(point)
103
+
104
+ for neighbor in self.neighbors[point]:
105
+ if self.board[neighbor] == 0: # 如果邻居是空的,则有气
106
+ return True
107
+ elif self.board[neighbor] == color and self.has_liberty(neighbor, color, visited):
108
+ # 如果邻居是同色,并且递归发现有气,那么这个点也有气
109
+ return True
110
+
111
+ # 如果所有路径都检查完毕,仍然没有发现有气,返回False
112
+ return False
113
+
114
+ def remove_stone(self, point):
115
+ color = self.board[point]
116
+ self.board[point] = 0
117
+ self.latest_removes[-1].append(point)
118
+ self.notify_observers("remove_stone", point=point, score=self.score())
119
+
120
+ for neighbor in self.neighbors[point]:
121
+ if self.board[neighbor] == color:
122
+ self.remove_stone(neighbor)
123
+
124
+ def reset(self):
125
+ self.board = np.zeros([self.board_size])
126
+ self.current_player = BLACK
127
+ self.latest_removes = [[]]
128
+ self.black_suicides = set()
129
+ self.white_suicides = set()
130
+ self.turns = OrderedDict()
131
+ self.notify_observers("reset", **{})
132
+
133
+ def switch_player(self):
134
+ self.current_player = -self.current_player
135
+ self.notify_observers("switch_player", side=self.current_player)
136
+
137
+ def play(self, point, player, turn_check=True):
138
+ if self.latest_player and self.latest_player == player:
139
+ return
140
+
141
+ if self.board[point] != 0:
142
+ raise ValueError("Invalid move: position already occupied.")
143
+
144
+ if point >= 302:
145
+ raise ValueError("Invalid move: position not on the board.")
146
+
147
+ if player == BLACK and self.counter % 2 == 1:
148
+ raise ValueError("Invalid move: not the player's turn.")
149
+
150
+ if player == WHITE and self.counter % 2 == 0:
151
+ raise ValueError("Invalid move: not the player's turn.")
152
+
153
+ if turn_check and player != self.current_player:
154
+ raise ValueError("Invalid move: not the player's turn.")
155
+
156
+ if self.latest_removes and len(self.latest_removes[-1]) == 1 and point == self.latest_removes[-1][0]:
157
+ raise ValueError("Invalid move: ko rule violation.")
158
+
159
+ # if player == BLACK and point in self.white_suicides:
160
+ # self.white_suicides.remove(point)
161
+
162
+ # if player == WHITE and point in self.black_suicides:
163
+ # self.black_suicides.remove(point)
164
+
165
+ self.board[point] = player
166
+ # print(point, encoder[point], self.neighbors[point])
167
+
168
+ for neighbor in self.neighbors[point]:
169
+ if self.board[neighbor] == -player: # Opponent's stone
170
+ if not self.has_liberty(neighbor, -player):
171
+ self.remove_stone(neighbor)
172
+
173
+ if not self.has_liberty(point):
174
+ # 如果自己的棋也没有气,则为自杀棋,撤回落子
175
+ self.board[point] = 0
176
+ if player == BLACK:
177
+ self.black_suicides.add(point)
178
+ else:
179
+ self.white_suicides.add(point)
180
+ raise ValueError("Invalid move: suicide is not allowed.")
181
+
182
+ self.turns[self.counter] = encoder[point]
183
+ self.notify_observers("add_stone", point=point, player=player, score=self.score())
184
+ self.latest_player = player
185
+
186
+ def get_empties(self, player):
187
+ empty_points = set([ix for ix, point in enumerate(self.board) if point == 0])
188
+ if self.latest_removes and len(self.latest_removes[-1]) == 1 and self.latest_removes[-1][0] in empty_points:
189
+ empty_points.remove(self.latest_removes[-1][0])
190
+ if player == BLACK:
191
+ for point in self.black_suicides:
192
+ empty_points.remove(point)
193
+ if player == WHITE:
194
+ for point in self.white_suicides:
195
+ empty_points.remove(point)
196
+ return list(empty_points)
197
+
198
+ def score(self):
199
+ total_black_area, total_white_area, total_unclaimed_area = 0, 0, 0
200
+ for piece in polysmalls:
201
+ black_area, white_area, unclaimed_area = calculate_area(self.board, piece, polysmall_area)
202
+ total_black_area += black_area
203
+ total_white_area += white_area
204
+ total_unclaimed_area += unclaimed_area
205
+ for piece in polylarges:
206
+ black_area, white_area, unclaimed_area = calculate_area(self.board, piece, polylarge_area)
207
+ total_black_area += black_area
208
+ total_white_area += white_area
209
+ total_unclaimed_area += unclaimed_area
210
+
211
+ return total_black_area / total_area, total_white_area / total_area, total_unclaimed_area / total_area
212
+
213
+ def is_game_over(self):
214
+ return len(self.get_empties(self.current_player)) == 0
215
+
216
+ def result(self):
217
+ return {}
218
+
219
+ def genmove(self, player):
220
+ if self.simulator is None:
221
+ self.simulator = SimulatedBoard()
222
+ self.simulator.redirect(self)
223
+ return self.simulator.genmove(player)
224
+
225
+
226
+ class SimulatedBoard(Board):
227
+ def __init__(self):
228
+ super().__init__()
229
+
230
+ def redirect(self, board):
231
+ self.board = board.board.copy()
232
+ self.current_player = board.current_player
233
+ self.latest_removes = board.latest_removes.copy()
234
+ self.black_suicides = board.black_suicides.copy()
235
+ self.white_suicides = board.white_suicides.copy()
236
+ self.orginal_counter = board.counter
237
+ self.turns = board.turns.copy()
238
+
239
+ def genmove(self, player):
240
+ best_score = -math.inf
241
+ best_potential = math.inf
242
+ best_move = None
243
+
244
+ for point in self.get_empties(player):
245
+ simulated_score, gain = self.simulate_score(0, point, player)
246
+ simulated_score = simulated_score + 2 * gain
247
+ if simulated_score > best_score:
248
+ best_score = simulated_score
249
+ best_potential = calculate_potential(self.board, point, self.counter)
250
+ best_move = point
251
+ elif simulated_score == best_score:
252
+ potential = calculate_potential(self.board, point, self.counter)
253
+ if potential < best_potential:
254
+ best_potential = potential
255
+ best_move = point
256
+
257
+ return best_move
258
+
259
+ def simulate_score(self, depth, point, player):
260
+ if depth == 1:
261
+ return 0, 0
262
+
263
+ trail = 2
264
+ self.latest_removes.append([])
265
+ black_area_ratio, white_area_ratio, unclaimed_area_ratio = 0, 0, 0
266
+ mean_rival_area_ratio, gain, mean_rival_gain = 0, 0, 0
267
+ try:
268
+ # 假设在 point 落子,计算得分,需要考虑复原棋盘的状态
269
+ self.play(point, player, turn_check=False) # 模拟落子
270
+ black_area_ratio, white_area_ratio, unclaimed_area_ratio = self.score() # 计算得分
271
+
272
+ empty_points = sample(self.get_empties(-player), trail)
273
+ total_rival_area_ratio, total_rival_gain = 0, 0
274
+ for rival_point in empty_points:
275
+ rival_area_ratio, rival_gain = self.simulate_score(depth + 1, rival_point, -player) # 递归计算对手的得分
276
+ total_rival_area_ratio += rival_area_ratio
277
+ total_rival_gain += rival_gain
278
+ mean_rival_area_ratio = total_rival_area_ratio / trail
279
+ mean_rival_gain = total_rival_gain / trail
280
+ except ValueError as e:
281
+ print(e)
282
+ if 'suicide' in str(e):
283
+ raise e
284
+
285
+ self.board[point] = 0 # 恢复棋盘状态
286
+ gain = 0
287
+ if self.latest_removes and len(self.latest_removes) > 0:
288
+ for removed in self.latest_removes[-1]:
289
+ self.board[removed] = -self.current_player
290
+ gain = len(self.latest_removes[-1]) / len(self.board)
291
+ self.latest_removes.pop()
292
+
293
+ if self.counter > self.orginal_counter:
294
+ self.turns.pop(self.counter - 1)
295
+
296
+ if player == BLACK:
297
+ # print(black_area_ratio, mean_rival_area_ratio, gain, mean_rival_gain)
298
+ return black_area_ratio - mean_rival_area_ratio, gain - mean_rival_gain
299
+ else:
300
+ # print(white_area_ratio, mean_rival_area_ratio, gain, mean_rival_gain)
301
+ return white_area_ratio - mean_rival_area_ratio, gain - mean_rival_gain