Last active
March 22, 2020 22:38
-
-
Save shiracamus/db0ddc34b5e117f5a258e1a7dd94bf2f to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import random | |
from time import sleep | |
import tkinter as tk | |
from tkinter import messagebox | |
TITLE = "Othello" | |
class Stone: | |
NONE = "." | |
CANDIDATE = "?" | |
BLACK = "X" | |
WHITE = "O" | |
class Cell: | |
def __init__(self): | |
self.stone = Stone.NONE | |
def __repr__(self): | |
return self.stone | |
def __eq__(self, stone): | |
return self.stone == stone | |
def is_none(self): | |
return self.stone in (Stone.NONE, Stone.CANDIDATE) | |
def set_candidate(self): | |
if self.stone == Stone.NONE: | |
self.stone = Stone.CANDIDATE | |
def reset_candidate(self): | |
if self.stone == Stone.CANDIDATE: | |
self.stone = Stone.NONE | |
class Board: | |
def __init__(self): | |
self._cells = [[Cell() for x in range(8)] for y in range(8)] | |
self[3, 3], self[3, 4] = Stone.WHITE, Stone.BLACK | |
self[4, 3], self[4, 4] = Stone.BLACK, Stone.WHITE | |
def __repr__(self): | |
return repr(self._cells) | |
def __iter__(self): | |
return (tuple(row) for row in self._cells) | |
def __getitem__(self, pos): | |
x, y = pos | |
return self._cells[y][x].stone | |
def __setitem__(self, pos, stone): | |
x, y = pos | |
self._cells[y][x].stone = stone | |
def __contains__(self, pos): | |
x, y = pos | |
return 0 <= y < len(self._cells) and 0 <= x < len(self._cells[y]) | |
def is_used(self, x, y): | |
return not self._cells[y][x].is_none() | |
def set_candidates(self, candidates): | |
for x, y in candidates: | |
self._cells[y][x].set_candidate() | |
def reset_candidates(self): | |
for row in self: | |
for cell in row: | |
cell.reset_candidate() | |
class Action: | |
def __init__(self, board, stone, points): | |
self._board = board | |
self._stone = stone | |
self._points = points | |
def __repr__(self): | |
return f"Action<stone={self._stone}, points={self._points}>" | |
def put(self): | |
for x, y in self._points: | |
self._board[x, y] = self._stone | |
class Othello: | |
DIRS = (-1, -1), (0, -1), (1, -1), (-1, 0), (1, 0), (-1, 1), (0, 1), (1, 1) | |
def __init__(self, player1=Stone.BLACK, player2=Stone.WHITE): | |
self.board = Board() | |
self.player1 = player1 | |
self.player2 = player2 | |
self.player = player1 | |
self.opponent = player2 | |
self._play_listeners = [] | |
self._locked_to_play = False | |
def __repr__(self): | |
return '\n'.join(' '.join(map(str, row)) for row in self.board) | |
def __iter__(self): | |
return iter(self.board) | |
def reversibles(self, x, y, stone): | |
"""石を置いたときに反転できる座標、座標が一つでもあれば石を置ける""" | |
if self.board.is_used(x, y): | |
return () # 空地でなければ座標なし | |
def one_direction(dx, dy): | |
points = [] # 8方向のうちの1方向での反転可能座標 | |
cx, cy = x + dx, y + dy | |
while (cx, cy) in self.board and self.board.is_used(cx, cy): | |
if self.board[cx, cy] == stone: | |
# 自分のコマに辿り着いた、反転できるコマの座標を返す | |
return points | |
# 相手のコマ、自分のコマに辿り着いたときに反転できる | |
points.append((cx, cy)) | |
cx, cy = cx + dx, cy + dy | |
return [] # 盤の端に到達したか空地に到達したら反転できない | |
return tuple(sum((one_direction(dx, dy) for dx, dy in self.DIRS), [])) | |
def is_playable(self): | |
"""石を置ける場所があるならTrue""" | |
return any(self.reversibles(x, y, stone) | |
for y, row in enumerate(self) | |
for x, _ in enumerate(row) | |
for stone in (Stone.WHITE, Stone.BLACK)) | |
def _candidates(self, stone): | |
"""石を置ける座標と、置いたときの操作の辞書データ""" | |
return {(x, y): Action(self.board, stone, ((x, y),) + reversibles) | |
for y, row in enumerate(self) | |
for x, _ in enumerate(row) | |
for reversibles in [self.reversibles(x, y, stone)] | |
if reversibles} | |
def candidates(self): | |
return tuple(self._candidates(self.player)) | |
def show_candidates(self, candidates): | |
self.board.set_candidates(candidates) | |
def hide_candidates(self): | |
self.board.reset_candidates() | |
def after(self, delay_ms, callback): | |
sleep(delay / 1000) | |
callback() | |
def on_play(self, listener): | |
self._play_listeners.append(listener) | |
def play(self): | |
if self._locked_to_play: | |
return | |
if not self.is_playable(): | |
return | |
candidates = self.candidates() | |
for listener in self._play_listeners: | |
listener(self, candidates) | |
def put(self, stone, x, y, delay=300): | |
if stone != self.player: | |
return | |
candidates = self._candidates(stone) | |
if (x, y) not in candidates: | |
return | |
candidates[x, y].put() | |
self._turn(delay) | |
def pass_(self, stone, delay=0): | |
if stone != self.player: | |
return | |
candidates = self._candidates(stone) | |
if candidates: | |
return | |
self._turn(delay) | |
def _turn(self, delay): | |
self.hide_candidates() | |
self.player, self.opponent = self.opponent, self.player | |
self._locked_to_play = True | |
self.after(delay, self._unlock_and_play) | |
def _unlock_and_play(self): | |
self._locked_to_play = False | |
self.play() | |
def count(self, stone): | |
return sum(cell == stone for row in self for cell in row) | |
def winner(self): | |
stones1 = self.count(self.player1) | |
stones2 = self.count(self.player2) | |
return (self.player1 if stones1 > stones2 else | |
self.player2 if stones1 < stones2 else | |
None) | |
EVALUATION = ( | |
(+30, -12, +0, -1, -1, +0, -12, +30), | |
(-12, -15, -3, -3, -3, -3, -15, -12), | |
(+ 0, - 3, +0, -1, -1, +0, - 3, + 0), | |
(- 1, - 3, -1, -1, -1, -1, - 3, - 1), | |
(- 1, - 3, -1, -1, -1, -1, - 3, - 1), | |
(+ 0, - 3, +0, -1, -1, +0, - 3, + 0), | |
(-12, -15, -3, -3, -3, -3, -15, -12), | |
(+30, -12, +0, -1, -1, +0, -12, +30), | |
) | |
class Strategy: | |
def play(self, othello, player, candidates): | |
pass | |
def put(self, othello, player, x, y): | |
pass | |
def pass_(self, othello, player): | |
pass | |
class GUIHumanStrategy(Strategy): | |
def play(self, othello, player, candidates): | |
othello.show_candidates(candidates) | |
self.candidates = tuple(candidates) | |
def put(self, othello, player, x, y): | |
if (x, y) in self.candidates: | |
self.candidates = [] | |
othello.hide_candidates() | |
othello.put(player.stone, x, y) | |
def pass_(self, othello, player): | |
messagebox.showwarning(TITLE, "Pass your turn") | |
class RandomStrategy(Strategy): | |
def play(self, othello, player, candidates): | |
othello.put(player.stone, *random.choice(candidates)) | |
class CornerStrategy(Strategy): | |
def play(self, othello, player, candidates): | |
# 角に置ければ角をとる | |
corner = [(x, y) for x, y in candidates if EVALUATION[y][x] == 30] | |
if corner: | |
othello.put(player.stone, *random.choice(corner)) | |
return | |
# 角周辺以外があれば、角周辺を避けて置く | |
inside = [(x, y) for x, y in candidates if EVALUATION[y][x] > -10] | |
if inside: # 角周りを避ける | |
othello.put(player.stone, *random.choice(inside)) | |
return | |
# 角周辺から選ぶ | |
othello.put(player.stone, *random.choice(candidates)) | |
class EvaluationStrategy(Strategy): | |
def play(self, othello, player, candidates): | |
# 評価値が高い場所を選ぶ | |
x, y = sorted(candidates, reverse=True, | |
key=lambda xy: EVALUATION[xy[1]][xy[0]])[0] | |
best = EVALUATION[y][x] | |
stronger = [(x, y) for x, y in candidates if EVALUATION[y][x] == best] | |
othello.put(player.stone, *random.choice(stronger)) | |
STRATEGY = { | |
"human": GUIHumanStrategy(), | |
"random": RandomStrategy(), | |
"corner": CornerStrategy(), | |
"evaluation": EvaluationStrategy(), | |
} | |
class Player: | |
def __init__(self, stone, name): | |
self.stone = stone | |
self.name = name | |
self.strategy = STRATEGY["human"] | |
def __str__(self): | |
return f"{self.name}({self.stone})" | |
def play(self, othello, candidates): | |
if othello.player != self.stone: | |
return | |
if candidates: | |
self.strategy.play(othello, self, candidates) | |
else: | |
self.strategy.pass_(othello, self) | |
othello.pass_(self.stone) | |
def put(self, othello, x, y): | |
self.strategy.put(othello, self, x, y) | |
def pass_(self, othello): | |
self.startegy.pass_(othello, self) | |
class CellUI: | |
def __init__(self, canvas, cell, x, y, size=70, stone_scale=0.7): | |
self._canvas = canvas | |
self._cell = cell | |
rect_x = x * size | |
rect_y = y * size | |
rect = rect_x, rect_y, rect_x + size, rect_y + size | |
oval_size = int(size * stone_scale) | |
oval_offset = (size - oval_size) // 2 | |
oval_x = rect_x + oval_offset | |
oval_y = rect_y + oval_offset | |
oval = oval_x, oval_y, oval_x + oval_size, oval_y + oval_size | |
self.tag = { | |
Stone.NONE: f"s{x}{y}", | |
Stone.CANDIDATE: f"c{x}{y}", | |
Stone.BLACK: f"b{x}{y}", | |
Stone.WHITE: f"w{x}{y}", | |
} | |
canvas.create_oval(*oval, fill="black", tag=self.tag[Stone.BLACK]) | |
canvas.create_oval(*oval, fill="white", tag=self.tag[Stone.WHITE]) | |
canvas.create_oval(*oval, fill="green", outline="red", | |
tag=self.tag[Stone.CANDIDATE]) | |
# the last created object (this rectangle) will be shown at top | |
canvas.create_rectangle(*rect, fill="green", outline="black", | |
tag=self.tag[Stone.NONE]) | |
def show(self): | |
self._canvas.tag_raise(self.tag[self._cell.stone]) | |
class BoardUI: | |
def __init__(self, window, board, cell_size=70): | |
self._board = board | |
self.cell_size = cell_size | |
canvas = tk.Canvas(window, width=cell_size * 8, height=cell_size * 8) | |
canvas.pack() | |
canvas.bind("<Button-1>", self._clicked) | |
self._cells = [[CellUI(canvas, cell, x, y) | |
for x, cell in enumerate(row)] | |
for y, row in enumerate(board)] | |
self._click_listeners = [] | |
def __getitem__(self, pos): | |
x, y = pos | |
return self._cells[y][x] | |
def _clicked(self, event): | |
x = event.x // self.cell_size | |
y = event.y // self.cell_size | |
for listener in self._click_listeners: | |
listener(x, y) | |
def on_click(self, listener): | |
self._click_listeners.append(listener) | |
def show(self): | |
for row in self._cells: | |
for cell in row: | |
cell.show() | |
class Status: | |
def __init__(self, othello): | |
self.othello = othello | |
self.stone1 = tk.IntVar() | |
self.stone2 = tk.IntVar() | |
self.judge = tk.StringVar() | |
self.player1 = tk.StringVar() | |
self.player2 = tk.StringVar() | |
self._turn = { | |
othello.player1: self._turn_player1, | |
othello.player2: self._turn_player2, | |
} | |
self._finished = { | |
othello.player1: self._win_player1, | |
othello.player2: self._win_player2, | |
None: self._draw, | |
} | |
def _turn_player1(self): | |
self.player1.set("☞") | |
self.player2.set(" ") | |
def _turn_player2(self): | |
self.player1.set(" ") | |
self.player2.set("☜") | |
def _win_player1(self): | |
self.player1.set("☺") | |
self.player2.set("☹") | |
def _win_player2(self): | |
self.player1.set("☹") | |
self.player2.set("☺") | |
def _draw(self): | |
self.player1.set("☹") | |
self.player2.set("☹") | |
def update(self): | |
stone1 = self.othello.count(self.othello.player1) | |
stone2 = self.othello.count(self.othello.player2) | |
self.stone1.set(stone1) | |
self.stone2.set(stone2) | |
self.judge.set(stone1 < stone2 and '<' or stone1 > stone2 and '>' or '=') | |
if self.othello.is_playable(): | |
self._turn[self.othello.player]() | |
else: | |
self._finished[self.othello.winner()]() | |
class StatusUI: | |
def __init__(self, window, status): | |
self.status = status | |
f = tk.Frame(window) | |
f.pack(fill=tk.X) | |
font = ("", 28) | |
tk.Label(f, textvariable=status.player1, font=font, fg="red" | |
).pack(side=tk.LEFT) | |
tk.Label(f, text="Black:", font=font).pack(side=tk.LEFT) | |
tk.Label(f, textvariable=status.stone1, font=font).pack(side=tk.LEFT) | |
tk.Label(f, textvariable=status.judge, font=font).pack(side=tk.LEFT, | |
expand=1) | |
tk.Label(f, textvariable=status.player2, font=font, fg="red" | |
).pack(side=tk.RIGHT) | |
tk.Label(f, textvariable=status.stone2, font=font).pack(side=tk.RIGHT) | |
tk.Label(f, text="White:", font=font).pack(side=tk.RIGHT) | |
def show(self): | |
self.status.update() | |
class StrategySelector: | |
SELECTION = { | |
0: STRATEGY["human"], | |
1: STRATEGY["random"], | |
2: STRATEGY["corner"], | |
3: STRATEGY["evaluation"], | |
} | |
def __init__(self, othello, player): | |
self.othello = othello | |
self.player = player | |
self.var = tk.IntVar(value=0) | |
self.var.trace("w", self._selected) | |
def _selected(self, *args): | |
self.player.strategy = self.SELECTION[self.var.get()] | |
self.othello.play() | |
class StrategySelectorUI: | |
def __init__(self, window, selector1, selector2): | |
self._selector_ui(window, selector1).pack(side=tk.LEFT) | |
self._selector_ui(window, selector2).pack(side=tk.RIGHT) | |
def _selector_ui(self, window, selector): | |
font = ("", 16) | |
frame = tk.Frame(window) | |
tk.Label(frame, text=f"{selector.player.name}:", font=font | |
).pack(anchor=tk.W) | |
tk.Radiobutton(frame, text="Human", font=font, | |
var=selector.var, value=0).pack(anchor=tk.W) | |
tk.Radiobutton(frame, text="Computer(weak)", font=font, | |
var=selector.var, value=1).pack(anchor=tk.W) | |
tk.Radiobutton(frame, text="Computer(little strong)", font=font, | |
var=selector.var, value=2).pack(anchor=tk.W) | |
tk.Radiobutton(frame, text="Computer(strong)", font=font, | |
var=selector.var, value=3).pack(anchor=tk.W) | |
return frame | |
class OthelloUI: | |
def __init__(self, window, othello, player1, player2): | |
self._othello = othello | |
self._status = StatusUI(window, Status(othello)) | |
self._board = BoardUI(window, othello.board) | |
selector1 = StrategySelector(othello, player1) | |
selector2 = StrategySelector(othello, player2) | |
self._selector = StrategySelectorUI(window, selector1, selector2) | |
self.players = { | |
othello.player1: player1, | |
othello.player2: player2, | |
} | |
self._board.on_click(self._put) | |
othello.after = window.after # cannot use time.sleep in tkinter thread | |
othello.on_play(self._play) | |
def _put(self, x, y): | |
self.players[self._othello.player].put(self._othello, x, y) | |
self.show() | |
def _play(self, othello, candidates): | |
self.players[othello.player].play(othello, candidates) | |
self.show() | |
def show(self): | |
self._board.show() | |
self._status.show() | |
class OthelloApp: | |
def __init__(self): | |
self.othello = othello = Othello() | |
player1 = Player(othello.player1, "Black") | |
player2 = Player(othello.player2, "White") | |
self.window = window = tk.Tk() | |
window.title("Othello") | |
self.ui = OthelloUI(window, othello, player1, player2) | |
def run(self): | |
self.ui.show() | |
self.othello.play() | |
self.window.mainloop() | |
if __name__ == "__main__": | |
try: | |
OthelloApp().run() | |
except KeyboardInterrupt: | |
print() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment