| 1 | """Deterministic Gomocup brain: exact-five tactics and open/closed line scores. |
| 2 | |
| 3 | Derived from the repository's starter protocol loop. Each hypothetical move |
| 4 | is evaluated for both colours. A five-cell window is useful only when neither |
| 5 | adjacent cell extends it into an overline. No opponent files are inspected. |
| 6 | """ |
| 7 | import sys |
| 8 | |
| 9 | N = 15 |
| 10 | board = {} |
| 11 | DIRECTIONS = ((1, 0), (0, 1), (1, 1), (1, -1)) |
| 12 | |
| 13 | |
| 14 | def line_at(x, y, colour, dx, dy): |
| 15 | cells = [] |
| 16 | for offset in range(-5, 6): |
| 17 | p = (x + offset * dx, y + offset * dy) |
| 18 | if offset == 0: |
| 19 | cells.append('X') |
| 20 | elif not (0 <= p[0] < N and 0 <= p[1] < N): |
| 21 | cells.append('#') |
| 22 | else: |
| 23 | cells.append('X' if board.get(p) == colour else '#' if p in board else '.') |
| 24 | return ''.join(cells) |
| 25 | |
| 26 | |
| 27 | def line_value(line): |
| 28 | """Return (exact five, immediate winning offsets, positional score).""" |
| 29 | left = right = 5 |
| 30 | while left > 0 and line[left - 1] == 'X': |
| 31 | left -= 1 |
| 32 | while right < 10 and line[right + 1] == 'X': |
| 33 | right += 1 |
| 34 | length = right - left + 1 |
| 35 | if length == 5: |
| 36 | return True, set(), 0 |
| 37 | if length > 5: |
| 38 | return False, set(), 0 |
| 39 | ends = int(left > 0 and line[left - 1] == '.') + int(right < 10 and line[right + 1] == '.') |
| 40 | weights = {(4, 2): 100000, (4, 1): 10000, (3, 2): 5000, |
| 41 | (3, 1): 400, (2, 2): 300, (2, 1): 40, |
| 42 | (1, 2): 10, (1, 1): 2} |
| 43 | positional = weights.get((length, ends), 0) |
| 44 | wins = set() |
| 45 | for start in range(1, 6): |
| 46 | window = line[start:start + 5] |
| 47 | if '#' in window or line[start - 1] == 'X' or line[start + 5] == 'X': |
| 48 | continue |
| 49 | count = window.count('X') |
| 50 | if count == 4: |
| 51 | wins.add(start + window.index('.') - 5) |
| 52 | elif count == 3: |
| 53 | positional = max(positional, 400) |
| 54 | elif count == 2: |
| 55 | positional = max(positional, 40) |
| 56 | |
| 57 | for pattern in ('.XXX.', '.XX.X.', '.X.XX.'): |
| 58 | for start in range(len(line) - len(pattern) + 1): |
| 59 | if start <= 5 < start + len(pattern) and line[start:start + len(pattern)] == pattern: |
| 60 | positional = max(positional, 5000) |
| 61 | return False, wins, positional |
| 62 | |
| 63 | |
| 64 | def evaluate(p, colour): |
| 65 | win = False |
| 66 | winning_points = set() |
| 67 | score = 0 |
| 68 | open_threes = 0 |
| 69 | for dx, dy in DIRECTIONS: |
| 70 | exact, offsets, value = line_value(line_at(*p, colour, dx, dy)) |
| 71 | win |= exact |
| 72 | winning_points.update((p[0] + i * dx, p[1] + i * dy) for i in offsets) |
| 73 | score += value |
| 74 | open_threes += value >= 5000 |
| 75 | |
| 76 | if len(winning_points) >= 2: |
| 77 | score += 1000000 |
| 78 | elif winning_points: |
| 79 | score += 60000 |
| 80 | elif open_threes >= 2: |
| 81 | score += 20000 |
| 82 | return win, score |
| 83 | |
| 84 | |
| 85 | def move(): |
| 86 | if not board: |
| 87 | return N // 2, N // 2 |
| 88 | near = {(x + a, y + b) for x, y in board |
| 89 | for a in range(-2, 3) for b in range(-2, 3)} |
| 90 | candidates = sorted(p for p in near |
| 91 | if 0 <= p[0] < N and 0 <= p[1] < N and p not in board) |
| 92 | if not candidates: |
| 93 | raise ValueError('No legal move') |
| 94 | ranked = [] |
| 95 | for p in candidates: |
| 96 | own_win, attack = evaluate(p, 1) |
| 97 | their_win, defence = evaluate(p, 2) |
| 98 | centrality = -abs(p[0] - N // 2) - abs(p[1] - N // 2) |
| 99 | ranked.append(((own_win, their_win, attack * 11 + defence * 10, centrality), p)) |
| 100 | return max(ranked)[1] |
| 101 | |
| 102 | |
| 103 | def play(): |
| 104 | x, y = move() |
| 105 | board[(x, y)] = 1 |
| 106 | print(f'{x},{y}', flush=True) |
| 107 | |
| 108 | |
| 109 | def main(): |
| 110 | global N |
| 111 | for raw in sys.stdin: |
| 112 | cmd, _, arg = raw.strip().partition(' ') |
| 113 | cmd = cmd.upper() |
| 114 | if cmd == 'START': |
| 115 | N = int(arg) |
| 116 | board.clear() |
| 117 | print('OK', flush=True) |
| 118 | elif cmd == 'RESTART': |
| 119 | board.clear() |
| 120 | print('OK', flush=True) |
| 121 | elif cmd == 'BEGIN': |
| 122 | play() |
| 123 | elif cmd == 'TURN': |
| 124 | x, y = map(int, arg.split(',')) |
| 125 | board[(x, y)] = 2 |
| 126 | play() |
| 127 | elif cmd == 'BOARD': |
| 128 | board.clear() |
| 129 | for line in sys.stdin: |
| 130 | if line.strip().upper() == 'DONE': |
| 131 | break |
| 132 | x, y, colour = map(int, line.split(',')) |
| 133 | board[(x, y)] = colour |
| 134 | play() |
| 135 | elif cmd == 'ABOUT': |
| 136 | print('name="exact-line-threats", version="1", author="codex-golive"', flush=True) |
| 137 | elif cmd == 'END': |
| 138 | return |
| 139 | elif cmd != 'INFO': |
| 140 | print('UNKNOWN', flush=True) |
| 141 | |
| 142 | |
| 143 | if __name__ == '__main__': |
| 144 | main() |