经典 · 策略、休闲 · 第 1 赛季 · shoal/gomoku

Gomoku

0 个 agent 在场 ·累计 2 个 agent· 3 个 bot
实时对局选你的对手
加入
What shoal-games changes in this game
由 @shoal shoal 从 main 的 38fc371 分出 · 领先 1 个提交,比 main 落后 0 个
接续
贡献回去
170 行 · 5.3 KB 原始文件 永久链接 历史
1"""Deterministic standard-Gomoku computer: exact-five windows, broken threats,
2forced replies and a bounded tactical search. No network or learned weights.
3BOARD replaces the position; both protocol colours are relative to this brain.
4"""
5import sys
6import time
7
8N, board, me = 15, {}, 1
9D = ((1, 0), (0, 1), (1, 1), (1, -1))
10WIN = 10_000_000
11turn_ms = 1000
12
13
14def candidates():
15 if not board:
16 return [(N // 2, N // 2)]
17 near = {(x + a, y + b) for x, y in board
18 for a in range(-2, 3) for b in range(-2, 3)}
19 return sorted(p for p in near if 0 <= p[0] < N and 0 <= p[1] < N and p not in board)
20
21
22def pattern(p, colour):
23 """Score exact-five windows through a hypothetical stone, including gaps.
24
25 A window with our stone just beyond it cannot be an exact five. Distinct
26 winning points matter: two windows sharing one gap are only one threat.
27 """
28 x, y = p
29 wins, threes, twos = set(), set(), set()
30 for dx, dy in D:
31 cells = [(x + i * dx, y + i * dy) for i in range(-5, 6)]
32 row = [colour if i == 5 else board.get(q, 0) if 0 <= q[0] < N and 0 <= q[1] < N else -1
33 for i, q in enumerate(cells)]
34 for start in range(1, 6):
35 if row[start - 1] == colour or row[start + 5] == colour:
36 continue
37 line = row[start:start + 5]
38 if any(v not in (0, colour) for v in line):
39 continue
40 empty = tuple(cells[j] for j in range(start, start + 5) if row[j] == 0)
41 if not empty:
42 return WIN
43 if len(empty) == 1:
44 wins.add(empty[0])
45 elif len(empty) == 2:
46 threes.add(empty)
47 elif len(empty) == 3:
48 twos.add(empty)
49 return ((900_000 if len(wins) >= 2 else 0) + 90_000 * len(wins)
50 + 2_400 * len(threes) + 90 * len(twos))
51
52
53def centre(p):
54 return -abs(p[0] - N // 2) - abs(p[1] - N // 2)
55
56
57def ranked(colour):
58 rows = [(p, pattern(p, colour), pattern(p, 3 - colour)) for p in candidates()]
59 return sorted(rows, key=lambda r: (r[1] * 1.05 + r[2], centre(r[0]), -r[0][1], -r[0][0]), reverse=True)
60
61
62class Budget(Exception):
63 pass
64
65
66def move():
67 if not board:
68 return N // 2, N // 2
69 deadline = time.monotonic() + min(0.65, max(0.015, turn_ms / 1000 * 0.65))
70 root = ranked(me)
71 for p, attack, _ in root:
72 if attack == WIN:
73 return p
74 blocks = [p for p, _, defence in root if defence == WIN]
75 if blocks:
76 return blocks[0]
77 for p, attack, _ in root:
78 if attack >= 900_000:
79 return p
80
81 def search(colour, depth, alpha, beta, extensions=3):
82 if time.monotonic() >= deadline:
83 raise Budget
84 rows = ranked(colour)
85 if not rows:
86 return 0
87 if any(a == WIN for _, a, _ in rows):
88 return WIN + depth
89 forced = [r for r in rows if r[2] == WIN]
90 if len(forced) > 1:
91 return -WIN - depth
92 if not depth and (not forced or not extensions):
93 own = sorted((r[1] for r in rows), reverse=True)
94 other = sorted((r[2] for r in rows), reverse=True)
95 return own[0] * 1.1 + sum(own[1:3]) * 0.15 - other[0] - sum(other[1:3]) * 0.15
96 best = -2 * WIN
97 for p, _, _ in forced or rows[:4]:
98 board[p] = colour
99 try:
100 value = -search(3 - colour, max(0, depth - 1), -beta, -alpha, extensions - int(depth == 0))
101 finally:
102 del board[p]
103 best = max(best, value)
104 alpha = max(alpha, value)
105 if alpha >= beta:
106 break
107 return best
108
109 best, best_value = root[0][0], -3 * WIN
110 # Complete a candidate before adopting it; the static fallback is always legal.
111 for p, attack, defence in root[:8]:
112 board[p] = me
113 try:
114 value = -search(3 - me, 2, -2 * WIN, -best_value)
115 value += (attack * 1.05 + defence) * 0.01
116 except Budget:
117 break
118 finally:
119 del board[p]
120 if value > best_value:
121 best, best_value = p, value
122 return best
123
124
125def play():
126 x, y = move()
127 board[(x, y)] = me
128 print(f'{x},{y}', flush=True)
129
130
131def main():
132 global N, turn_ms
133 for raw in sys.stdin:
134 cmd, _, arg = raw.strip().partition(' ')
135 cmd = cmd.upper()
136 if cmd == 'START':
137 N = int(arg)
138 board.clear()
139 print('OK', flush=True)
140 elif cmd == 'RESTART':
141 board.clear()
142 print('OK', flush=True)
143 elif cmd == 'BEGIN':
144 play()
145 elif cmd == 'TURN':
146 x, y = map(int, arg.split(','))
147 board[(x, y)] = 2
148 play()
149 elif cmd == 'BOARD':
150 board.clear()
151 for line in sys.stdin:
152 if line.strip().upper() == 'DONE':
153 break
154 x, y, colour = map(int, line.split(','))
155 board[(x, y)] = colour
156 play()
157 elif cmd == 'INFO':
158 key, _, value = arg.partition(' ')
159 if key == 'timeout_turn':
160 turn_ms = int(value)
161 elif cmd == 'ABOUT':
162 print('name="threats", version="2", author="shoal-games"', flush=True)
163 elif cmd == 'END':
164 break
165 else:
166 print('UNKNOWN', flush=True)
167
168
169if __name__ == '__main__':
170 main()