├── README.md ├── bot.py ├── common.py ├── config.py ├── game.py ├── logger.py ├── my_rules.py ├── mybot.py └── rules.py /README.md: -------------------------------------------------------------------------------- 1 | # Connect6 for Python 2 | [Connect6](https://en.wikipedia.org/wiki/Connect6) game implemented in Python. 3 | This is not for playable purpose - rather, it's tool for experiment and developing Connect6 AI.  4 | 5 | ![Screenshot](http://i.imgur.com/nhqJCYF.png) 6 | 7 | 8 | ## Features 9 | 10 | * It's Python. NOTE THAT Python is cool and awesome. 11 | * Bot (AI) implementation / test support 12 | * You can implement, test, and match with your own bots. 13 | * Game record logging 14 | 15 | ## Prerequisites 16 | 17 | * Python 3 18 | 19 | ## Usage 20 | In non-windows OS, just: 21 | ``` 22 | $ ./game.py 23 | ``` 24 | 25 | In Windows, Please use Python 3 interpreter explicitly. 26 | 27 | ``` 28 | > python3 game.py 29 | ``` 30 | 31 | 32 | ## How to implement bot? 33 | 34 | First, implement your own bot. Then modify `config.py` : 35 | 36 | ```python 37 | from my_awesome_bot import MyAwesomeAIBot 38 | 39 | # change AIBot to your bot. 40 | AIBot = MyAwesomeAIBot 41 | ``` 42 | 43 | Also, you can modify `game.py` to support multiple bots. 44 | 45 | ## Logging 46 | 47 | After you played game, the gameplay log will be stored in `logs/` directory. 48 | A log is like this: 49 | 50 | ```python 51 | ("Move",2,6,5) # Black at G6 (6,5) 52 | ("Move",1,0,3) # White at A4 (0,3) 53 | ("Move",1,0,4) # White at A5 (0,4) 54 | ("Res", 1) # White won. 55 | ``` 56 | As you can see, it's python code. You can read and process the log data by evaluating them line by line. 57 | 58 | 59 | ## License: MIT 60 | -------------------------------------------------------------------------------- /bot.py: -------------------------------------------------------------------------------- 1 | 2 | import random 3 | 4 | class Bot: 5 | """ A bot class. Override this class to implement your own Connect6 AI. """ 6 | 7 | def __init__(self, player=1): 8 | self.player = player 9 | 10 | def move(self, board, nth_move): 11 | raise NotImplementedError("Implement this to build your own AI.") 12 | pass 13 | 14 | @property 15 | def bot_kind(self): 16 | return self.__class__.__name__ 17 | 18 | 19 | class RandomBot(Bot): 20 | 21 | """ Example bot that runs randomly. """ 22 | def move(self, board, nth_move): 23 | x = random.randrange(0, 19) 24 | y = random.randrange(0, 19) 25 | input() 26 | return x, y 27 | 28 | -------------------------------------------------------------------------------- /common.py: -------------------------------------------------------------------------------- 1 | 2 | from collections import namedtuple 3 | 4 | X_TO_CHAR = {i : chr(ord('A') + i) for i in range(19)} 5 | CHAR_TO_X = {chr(ord('A') + i): i for i in range(19)} 6 | 7 | 8 | def repr_direction(dir_func): 9 | dx, dy = dir_func(0, 0) # differentiate 10 | return '({}, {})'.format(dy, dx) 11 | 12 | 13 | class Point(namedtuple('Point', ['x', 'y'])): 14 | 15 | @staticmethod 16 | def from_name(name): 17 | x = CHAR_TO_X[name.capitalize()[0]] 18 | y = int(name[1:])-1 19 | return Point(x, y) 20 | 21 | @property 22 | def name(self): 23 | return X_TO_CHAR[self.x] + str(self.y+1) 24 | 25 | # some memory optimization 26 | __slots__ = () 27 | 28 | def __str__(self): 29 | return '{} ({},{})'.format(self.name, self.x, self.y) 30 | 31 | 32 | class Debugger: 33 | def __init__(self, enable_log=False): 34 | self.enable_log = enable_log 35 | 36 | def log(self, str): 37 | if self.enable_log: 38 | print(str) 39 | 40 | 41 | def stop(self): 42 | if self.enable_log: 43 | input('[Press any key to resume]') 44 | 45 | -------------------------------------------------------------------------------- /config.py: -------------------------------------------------------------------------------- 1 | 2 | from bot import RandomBot 3 | from mybot import MySmartBot 4 | 5 | # TODO: Change it to your own AI implementation class. 6 | #AIBot = RandomBot 7 | AIBot = MySmartBot 8 | -------------------------------------------------------------------------------- /game.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import os 3 | from common import Point 4 | from logger import MoveLogger 5 | from rules import Referee 6 | from bot import Bot 7 | from config import AIBot 8 | 9 | # constants 10 | STONE_CHAR = ['.', 'O', 'X'] 11 | STONE_NAME = ['', 'White (O)', 'Black (X)'] 12 | CHAR_TO_X = {chr(ord('A') + i) : i for i in range(19)} 13 | X_TO_CHAR = {i: chr(ord('A')+i) for i in range(19)} 14 | 15 | # console helper methods 16 | def cls(): 17 | os.system('cls' if os.name == 'nt' else 'clear') 18 | 19 | def darktext(str): 20 | return str if os.name == 'nt' else '\x1b[0;30m{}\x1b[0m'.format(str) 21 | 22 | 23 | def draw_board(board, player=0, nth_move=0): 24 | cls() 25 | print('Move : {}'.format(nth_move)) 26 | print('{} turn.'.format(STONE_NAME[player])) 27 | print() 28 | print(' A B C D E F G H I J K L M N O P Q R S ') 29 | print(' +---------------------------------------+') 30 | 31 | for y in range(19): 32 | print(' {:>2d} |'.format(y+1), end='') # line no. 33 | for x in range(19): 34 | stone = board[y][x] 35 | if stone != 0: print(' ' + STONE_CHAR[board[y][x]], end='') 36 | else: print(darktext(' '+X_TO_CHAR[x].lower()), end='') 37 | print(' |') 38 | 39 | print(' +---------------------------------------+') 40 | print() 41 | 42 | 43 | class Player(Bot): 44 | """ 플레이어도 봇으로 취급하지만, 사용자로부터 입력을 받음. """ 45 | 46 | def move(self, board, nth_move): 47 | move = input('{} turn : '.format(STONE_NAME[self.player])) 48 | return Point.from_name(move) 49 | 50 | 51 | def exit_game(logger: MoveLogger, won_bot=None): 52 | if won_bot is not None: 53 | logger.log_winner(won_bot.player) 54 | print('{} ({}) won!!'.format(STONE_NAME[won_bot.player], won_bot.bot_kind)) 55 | else: 56 | print('No one won.') 57 | 58 | logger.save_to_file() 59 | 60 | 61 | def main(bots): 62 | # to align index with player variable. 63 | bot_set = [None] + bots 64 | 65 | board = [[0 for x in range(19)] for y in range(19)] 66 | referee = Referee(board) 67 | 68 | nth_move = 1 69 | player = 2 # 1=white 2=black. black moves first 70 | player_moved_count = 1 # at first time, black can only move once. 71 | logger = MoveLogger() 72 | 73 | while True: 74 | draw_board(board, player, nth_move) 75 | 76 | # input loop. 77 | while True: 78 | try: 79 | x, y = bot_set[player].move(board, nth_move) 80 | able_to_place, msg = referee.can_place(x, y) 81 | if not able_to_place: 82 | print('{}. Try again in another place.'.format(msg)) 83 | continue 84 | break 85 | 86 | except KeyboardInterrupt: 87 | print('\n' + 'Bye...') 88 | exit_game(logger) 89 | return 90 | 91 | except Exception as e: 92 | raise e 93 | print('Wrong input.') 94 | continue 95 | 96 | # place stone 97 | board[y][x] = player 98 | logger.log(x, y, player) 99 | referee.update(x, y, player) 100 | 101 | won_player = referee.determine() 102 | if won_player is not None: 103 | exit_game(logger, bot_set[won_player]) 104 | return 105 | 106 | player_moved_count += 1 107 | if player_moved_count == 2: 108 | # Change turn : a player can move 2 times per turn. 109 | nth_move += 1 110 | player_moved_count = 0 111 | player = 2 if player == 1 else 1 112 | 113 | 114 | if __name__ == '__main__': 115 | print('Welcome to TherneConnect6.') 116 | print('Choose player slot. (1=Player 2=AI)') 117 | 118 | black_choice = input(' Black (1 or 2) : ') 119 | white_choice = input(' White (1 or 2) : ') 120 | 121 | whitebot = Player(1) if white_choice == '1' else AIBot(1) 122 | blackbot = Player(2) if black_choice == '1' else AIBot(2) 123 | 124 | main([whitebot, blackbot]) 125 | -------------------------------------------------------------------------------- /logger.py: -------------------------------------------------------------------------------- 1 | 2 | from common import Point 3 | import os 4 | 5 | class Move: 6 | def __init__(self, x, y, player): 7 | self.x = x 8 | self.y = y 9 | self.player = player 10 | 11 | def __repr__(self): 12 | name = 'White' if self.player == 1 else 'Black' 13 | coord = Point(self.x, self.y) 14 | return '("Move",{},{},{}) # {} at {}'.format(self.player, self.x, self.y, name, coord) 15 | 16 | 17 | class MoveLogger: 18 | 19 | def __init__(self, logdir='logs/'): 20 | self.moves = [] 21 | 22 | self.logdir = logdir 23 | if not os.path.exists(self.logdir): 24 | os.makedirs(logdir) 25 | 26 | self.session_name = self._get_session_name() 27 | 28 | def _get_session_name(self): 29 | session_name = 1 30 | try: 31 | with open(self.logdir + 'session', 'r') as file: 32 | session_name = int(file.read()) 33 | session_name += 1 # new session. 34 | except: pass 35 | 36 | self._save_session_name(session_name) 37 | return session_name 38 | 39 | def _save_session_name(self, session_name): 40 | with open(self.logdir + 'session', 'w') as file: 41 | file.write(str(session_name)) 42 | 43 | def log(self, x, y, player): 44 | move = Move(x, y, player) 45 | self.moves.append(move) 46 | 47 | def undo(self): 48 | self.moves.pop(len(self.moves)-1) 49 | 50 | def log_winner(self, player): 51 | name = 'White' if player == 1 else 'Black' 52 | self.moves.append('("Res", {}) # {} won'.format(player, name)) 53 | 54 | def save_to_file(self): 55 | with open('logs/log.{}.txt'.format(self.session_name), 'w') as file: 56 | file.writelines('{}\n'.format(move) for move in self.moves) 57 | -------------------------------------------------------------------------------- /my_rules.py: -------------------------------------------------------------------------------- 1 | 2 | from common import Point, Debugger, repr_direction 3 | debug = Debugger(enable_log=False) 4 | 5 | class StragiticStatus: 6 | def __init__(self): 7 | self.a = 0 8 | self.a_point = [] 9 | self.b = 0 10 | self.b_point = [] 11 | 12 | # 8방향 13 | DIRECTIONS = [ 14 | (1, 0), 15 | (1, 1), 16 | (1, -1), 17 | (0, 1), 18 | (0, -1), 19 | (-1, 0), 20 | (-1, 1), 21 | (-1, -1) 22 | ] 23 | 24 | 25 | def reverse_of(dir): 26 | dx, dy = dir 27 | return (-dx, -dy) 28 | 29 | 30 | def is_outta_range(x, y): 31 | return x < 0 or x >= 19 or y < 0 or y >= 19 32 | 33 | 34 | def track(board, start_x, start_y, dir_func): 35 | pass 36 | 37 | def scan_from_last(board, last_points, player): 38 | for point in last_points: 39 | x, y = point 40 | 41 | # check 8 directions and start backtracking. 42 | for dir in DIRECTIONS: 43 | dx, dy = dir 44 | nx, ny = x+dx, y+dy 45 | if is_outta_range(nx, ny): continue 46 | 47 | if board[ny][nx] == board[y][x]: 48 | debug.log('Direction {}'.format(dir)) 49 | debug.log('Start at {}'.format(Point(x, y))) 50 | 51 | # to check properly, go to the end of direction 52 | while board[ny][nx] == board[y][x]: 53 | nx += dx 54 | ny += dy 55 | if is_outta_range(nx, ny): break 56 | 57 | dx, dy = reverse_of(dir) 58 | 59 | debug.log('End of direction : {}'.format(Point(nx, ny))) 60 | 61 | is_end = track(board, nx, ny, reverse_of(dir)) 62 | if is_end: 63 | # returns player who won. 64 | return board[ny][nx] 65 | 66 | debug.stop() 67 | 68 | def scan_full(board) -> StragiticStatus: 69 | 70 | pass -------------------------------------------------------------------------------- /mybot.py: -------------------------------------------------------------------------------- 1 | # 여기서부터 소스가 심각하게 더러워짐. 2 | 3 | from bot import Bot 4 | from common import Point, Debugger 5 | from rules import Referee 6 | from my_rules import scan_from_last, scan_full 7 | import math 8 | import time 9 | import random 10 | 11 | dbg = Debugger(enable_log=True) 12 | 13 | def convert_stone_like_samsung(stone, me): 14 | if stone == me: return 1 15 | elif stone == 0: return 0 16 | elif stone == 3: return 3 17 | return 2 18 | 19 | class MySmartBot(Bot): 20 | 21 | def __init__(self, player=1): 22 | super(MySmartBot, self).__init__(player=1) 23 | self.samsung_moves = [] 24 | self.samsung_index = 0 25 | 26 | 27 | def move(self, board, nth_move): 28 | if self.samsung_index == 0: 29 | s_board = self.convert_board_like_samsung(board) 30 | self.samsung_moves = samsung_like_move(s_board, nth_move) 31 | 32 | # 왜 samsung-like-move냐? 대회 C++파일은 한번에 두개씩두는데 이건 하나씩임. 그래서 저장해야댐 33 | ret = self.samsung_moves[self.samsung_index*2:(self.samsung_index+1)*2] 34 | 35 | self.samsung_index += 1 36 | if self.samsung_index == 2 or nth_move == 1: 37 | self.samsung_index = 0 38 | 39 | return ret 40 | 41 | def convert_board_like_samsung(self, board): 42 | me = self.player 43 | return [[convert_stone_like_samsung(board[y][x], me) for x in range(19)] for y in range(19)] 44 | 45 | #============================ 46 | # C++ Binding? for faster porting 47 | #============================= 48 | def memcpy(board, size): 49 | return [[board[y][x] for x in range(19)] for y in range(19)] 50 | 51 | def strlen(str): 52 | return len(str) 53 | 54 | def list_unique(items): 55 | from collections import OrderedDict 56 | return list(OrderedDict.fromkeys(items)) 57 | 58 | def chrono_now(): 59 | return int(round(time.time() * 1000)) 60 | 61 | 62 | #============================================================ 63 | # Real algorithm part 64 | #============================================================ 65 | 66 | SEARCH_SPACE_DIRECTIONS = [ # int{}{} 67 | (-2, -2), (0, -2), (2, -2), 68 | (-1, -1), (0, -1), (1, -1), 69 | (-2, 0), (-1, 0), (1, 0), (2, 0), 70 | (-1, 1), (0, 1), (1, 1), 71 | (-2, 2), (0, 2), (2, 2) 72 | ] 73 | 74 | SIMULATE_TIMES = 100 75 | MAX_PLAYOUT_DEPTH = 20 76 | MAX_ROLLOUT_DEPTH = 100 77 | C = 5 # C = UCB parameter 78 | TIMEOUT = 5000/2 # 5 second timeout. 2번돌려야되니까 나누기 2 79 | LAMBDA = 0.4 # 시뮬레이션 승률의 비중. 80 | BOARD_SIZE = 19*19*4 # 4 = sizeof(int) 81 | 82 | class Node: 83 | 84 | def __init__(self, parent, policy_prob: float): 85 | # prior_p: Policy가 결정한 값 86 | self.parent = parent 87 | self.children = {} 88 | self.num_visits = 0 89 | self.q = 0 90 | self.policy_prob = policy_prob 91 | self.u = policy_prob 92 | 93 | def backpropagate(self, leaf_value): 94 | if self.parent: 95 | self.parent.backpropagate(leaf_value) 96 | 97 | self._update(leaf_value) 98 | 99 | def select(self): 100 | # Greedy : 밸류 최고값을 가진놈 선택 101 | max_value = -100 102 | max_child = None 103 | max_move = None 104 | for move, child in self.children.items(): 105 | value = child.getvalue() 106 | if max_value < value: 107 | max_value = value 108 | max_child = child 109 | max_move = move 110 | 111 | return max_move, max_child 112 | 113 | 114 | def expand(self, policies): 115 | for move, prob in policies: 116 | if move not in self.children: 117 | self.children[move] = Node(parent=self, policy_prob=prob) 118 | 119 | def _update(self, leaf_value): 120 | self.num_visits += 1 121 | self.q += (leaf_value - self.q) / self.num_visits # update Q. (q = Action-value (Average reward(value) of all visits).) 122 | if self.parent: 123 | # update u. (u = UCB value) 124 | self.u = self.policy_prob * C * math.sqrt(self.parent.num_visits) / (1 + self.num_visits) 125 | 126 | def getvalue(self): 127 | return self.q + self.u 128 | 129 | def is_leaf(self): 130 | return self.children == {} 131 | 132 | 133 | win_count = 0 134 | fail_count = 0 135 | draw_count = 0 136 | 137 | class MCTS: 138 | def __init__(self): 139 | self.root = Node(None, 1.0) 140 | 141 | def move(self, state) -> Point: 142 | t = 0 143 | playout_count = 0 144 | last_time = chrono_now() 145 | 146 | # 시간이 끝날때까지 playout 진행 147 | while t < TIMEOUT: 148 | copy_state = state.copy() 149 | self.playout(copy_state) 150 | 151 | t += chrono_now() - last_time 152 | last_time = chrono_now() 153 | del copy_state 154 | 155 | playout_count += 1 156 | 157 | dbg.log('Playout count : {}'.format(playout_count)) 158 | 159 | # 가장 많이 방문된 (= 값이 높은)노드를 선택함. 160 | max_visit = -100 161 | max_child = None 162 | max_move = None 163 | for move, child in self.root.children.items(): 164 | if max_visit < child.num_visits: 165 | max_visit = child.num_visits 166 | max_child = child 167 | max_move = move 168 | 169 | dbg.log('Max visited : {}'.format(max_visit)) 170 | 171 | # Root node를 업데이트함. 그 형제들은 전부 없애버림. 172 | # TODO: 형제의 자식들은?ㅜㅜ 173 | for _, child in self.root.children.items(): 174 | if child != max_child: 175 | del child 176 | # TODO: del self.root 177 | self.root = max_child 178 | self.root.parent = None 179 | 180 | return max_move 181 | 182 | def playout(self, state: State): 183 | """ 1. Select한다. """ 184 | node = self.root 185 | for d in range(MAX_PLAYOUT_DEPTH): 186 | if node.is_leaf(): 187 | # Leaf라면 방문하지 않은 노드이다. 2. Expand한다. 188 | policies = sample_from_policy(state) 189 | 190 | if len(policies) == 0: 191 | # end of game 192 | # TODO: 이걸로 break시킬것인가 아니면 state를 통해 break시킬것인가? 193 | break 194 | 195 | # dbg.log(' Policy : {}'.format(policies)) 196 | node.expand(policies) 197 | 198 | # go deeper 199 | action, node = node.select() 200 | state.do(action) 201 | 202 | # 하드코딩한 승률과 시뮬레이션 상 승률을 조합함. 203 | winrate = get_winrate(state) 204 | simulated_winrate = self.rollout(state) 205 | # value = (1-LAMBDA)*winrate + LAMBDA*simulated_winrate 206 | value = simulated_winrate 207 | 208 | # 4. Backpropagate 209 | node.backpropagate(value) 210 | 211 | def rollout(self, state: State) -> float: 212 | global win_count, draw_count, fail_count 213 | 214 | """ 3. Simulation. state는 copy안하고 레퍼런스로 가져간다 - 이젠 수정해도됨. """ 215 | for d in range(MAX_ROLLOUT_DEPTH / 2): 216 | move1, move2 = sample_two_from_rollout_policy(state) 217 | if move1 is None: break 218 | state.do(move1) 219 | 220 | if move2 is None: break 221 | state.do(move2) 222 | 223 | # simulation result. 224 | if state.winner == 1: 225 | win_count += 1 226 | return 1 227 | elif state.winner == 0: 228 | draw_count += 1 229 | return 0 230 | else: 231 | fail_count += 1 232 | return -1 233 | 234 | 235 | class State: 236 | def __init__(self, board=[[0 for x in range(19)] for y in range(19)], nth_move=0): 237 | self.board = board 238 | self.player = 1 # 삼성 코드기준으로 나는 항상 1임. 239 | self.turn_count = 0 240 | self.nth_move = nth_move 241 | 242 | self.search_space = [] 243 | self.search_space_mark = [[0 for x in range(19)] for y in range(19)] # int{}{}. TODO: BLOCKED 반영!!! 244 | self.last_enemy_moves = [] 245 | 246 | self.referee = Referee(board) # TODO: only for python!! 247 | self.winner = 0 248 | 249 | def copy(self): 250 | new_board = memcpy(self.board, BOARD_SIZE) 251 | new_state = State(new_board, self.nth_move) 252 | new_state.search_space_mark = memcpy(self.search_space_mark, BOARD_SIZE) 253 | new_state.search_space = self.search_space 254 | new_state.turn_count = self.turn_count 255 | new_state.player = self.player 256 | 257 | def do(self, move: Point): 258 | self.board[move.y][move.x] = self.player 259 | self.referee.update(move.x, move.y, self.player) 260 | self.expand_search_space(move) 261 | 262 | self.turn_count += 1 263 | if self.turn_count == 2: 264 | # change turn. 265 | self.turn_count = 0 266 | self.player = 2 if self.player == 1 else 1 267 | self.nth_move += 1 268 | 269 | def do_enemy(self, point): 270 | self.player = 2 271 | self.last_enemy_moves.append(point) 272 | self.do(point) 273 | self.player = 1 274 | 275 | def update_enemy_board(self, board, nth_move): 276 | self.nth_move = nth_move 277 | self.last_enemy_moves.clear() 278 | 279 | # 상대편이 수를 둔 후의 보드를 업데이트한다. O(N^2). 280 | for y in range(19): 281 | for x in range(19): 282 | if self.board[y][x] != board[y][x]: 283 | point = Point(x,y) 284 | self.do_enemy(point) 285 | 286 | # 이제 my turn이 된다. 287 | self.player = 1 288 | self.turn_count = 0 289 | 290 | def has_winner(self): 291 | won_player = self.referee.determine() 292 | if won_player: 293 | self.winner = won_player 294 | return True 295 | return False 296 | 297 | 298 | def expand_search_space(self, p: Point): 299 | # expand search space 300 | for dx, dy in SEARCH_SPACE_DIRECTIONS: 301 | nx, ny = p.x+dx, p.y+dy 302 | if self.search_space_mark[ny][nx] == 0: 303 | self.search_space.append(Point(nx, ny)) 304 | self.search_space_mark[ny][nx] = 1 305 | 306 | # Cannot place in already placed area. 307 | self.search_space_mark[p.y][p.x] = -1 308 | 309 | 310 | def can_search(self, point): 311 | return self.search_space_mark[point.y][point.x] == 1 312 | 313 | 314 | def distance_factor(self, your_move): 315 | sum = 0.0 316 | for p in self.last_enemy_moves: 317 | sum += distance(your_move, p) 318 | 319 | return 1 - 0.7 * sum / len(self.last_enemy_moves) 320 | 321 | 322 | 323 | def sample_from_policy(state: State, player=1): 324 | """ 어떤 수를 둬야할지 결정한다. """ 325 | policies = [] 326 | for point in state.search_space: 327 | if not state.can_search(point): continue 328 | 329 | # TODO: evaluate value 330 | value = random.uniform(0, 1) 331 | 332 | policy_value = value * state.distance_factor(point) 333 | policies.append((point, policy_value)) 334 | 335 | return policies 336 | 337 | def get_winrate(state: State) -> float: 338 | # if state.has_winner() 339 | # TODO: implement 340 | return 0 341 | 342 | 343 | def sample_two_from_rollout_policy(state: State) -> Point: 344 | # if state.player == 1: 345 | # 346 | # 347 | # elif state.player == 2: 348 | # 349 | 350 | strstats = scan_full(state.board) 351 | enemy_strstats = strstats[1] if state.player == 1 else strstats[0] 352 | my_strstats = strstats[0] if state.player == 1 else strstats[1] 353 | 354 | if enemy_strstats.a > 0: 355 | # 체크 - 무슨 수를 써서라도 막음 356 | return enemy_strstats.a_point.pop() 357 | 358 | elif enemy_strstats.b > 0: 359 | # 덜 위험한 체크 - 남은 수를 준다 360 | return enemy_strstats.b_point.pop() 361 | 362 | # TODO: implement! 363 | return random.choice(state.search_space) 364 | 365 | 366 | 367 | mcts = MCTS() 368 | state = State() 369 | 370 | def samsung_like_move(board, nth_move): 371 | state.update_enemy_board(board, nth_move) 372 | 373 | # TODO: cnt는 c로 옮기지 마시오. 374 | cnt = 2 375 | if nth_move == 1: 376 | state.turn_count = 1 377 | cnt = 1 # 1번만 돌아야함. 378 | 379 | moves = [] 380 | for _ in range(cnt): 381 | move = mcts.move(state) 382 | moves += move 383 | state.do(move) 384 | 385 | dbg.log('Win: {} Draw: {} Fail: {}'.format(win_count, draw_count, fail_count)) 386 | dbg.stop() 387 | 388 | return moves 389 | 390 | def distance(p1, p2): 391 | # chebyshev distance. 392 | return max(abs(p2.y - p1.y), abs(p2.x - p1.x)) 393 | -------------------------------------------------------------------------------- /rules.py: -------------------------------------------------------------------------------- 1 | 2 | from common import Point, Debugger, repr_direction 3 | debug = Debugger(enable_log=False) 4 | 5 | # 8방향에 대한 함수들 6 | DIRECTIONS = [ 7 | lambda x, y: (x+1, y), 8 | lambda x, y: (x+1, y+1), 9 | lambda x, y: (x+1, y-1), 10 | lambda x, y: (x-1, y), 11 | lambda x, y: (x-1, y+1), 12 | lambda x, y: (x-1, y-1), 13 | lambda x, y: (x, y-1), 14 | lambda x, y: (x, y+1), 15 | ] 16 | 17 | 18 | def reverse_of(dir_func): 19 | """ 방향함수에 대한 역방향 함수를 리턴한다. """ 20 | dx, dy = dir_func(0, 0) # differentiate 21 | return lambda x, y: (x-dx, y-dy) 22 | 23 | 24 | def is_outta_range(x, y): 25 | return x < 0 or x >= 19 or y < 0 or y >= 19 26 | 27 | 28 | class Referee: 29 | """ 그 자리에 돌을 놓을 수 있는지, 누가 이겼는지를 판단하는 심판 클래스이다. """ 30 | 31 | def __init__(self, initial_board): 32 | self.board = initial_board 33 | self.last_move = (0, 0) 34 | 35 | def update(self, x, y, player): 36 | self.board[y][x] = player 37 | self.last_move = (x, y) 38 | 39 | def determine(self) -> int: 40 | """ Determine who won. 41 | :return: player number who won. None if there's no winner (game isn't finished). 42 | """ 43 | board = self.board 44 | x, y = self.last_move 45 | 46 | # check 8 directions and start backtracking. 47 | for dir_func in DIRECTIONS: 48 | nx, ny = dir_func(x, y) 49 | if is_outta_range(nx, ny): continue 50 | 51 | if board[ny][nx] == board[y][x]: 52 | debug.log('Direction : ' + repr_direction(dir_func)) 53 | debug.log('Start at {}'.format(Point(x, y))) 54 | 55 | # to check properly, go to the end of direction 56 | while board[ny][nx] == board[y][x]: 57 | nx, ny = dir_func(nx, ny) 58 | if is_outta_range(nx, ny): break 59 | 60 | reverse_dir_func = reverse_of(dir_func) 61 | nx, ny = reverse_dir_func(nx, ny) # one step back. 62 | 63 | debug.log('End of direction : {}'.format(Point(nx, ny))) 64 | 65 | is_end = self._track(nx, ny, reverse_dir_func) 66 | if is_end: 67 | # returns player who won. 68 | return board[ny][nx] 69 | 70 | debug.stop() 71 | 72 | 73 | def _track(self, start_x, start_y, dir_func): 74 | x, y = start_x, start_y 75 | original_player = self.board[y][x] 76 | debug.log('Track started at {}'.format(Point(x, y))) 77 | 78 | step = 1 79 | while True: 80 | x, y = dir_func(x, y) 81 | if is_outta_range(x, y) or self.board[y][x] != original_player: 82 | if step == 6: return True 83 | debug.log('Track finished at step {}'.format(step)) 84 | return False 85 | step += 1 86 | 87 | if step > 6: 88 | debug.log('Track success, but too many steps (step={})'.format(step)) 89 | return False 90 | 91 | return True 92 | 93 | 94 | def can_place(self, x, y): 95 | if self.board[y][x] != 0: 96 | return False, 'Duplicated move' 97 | 98 | return True, 'Ok' 99 | --------------------------------------------------------------------------------