Skip to content

Instantly share code, notes, and snippets.

@shiracamus
Last active March 22, 2020 22:38
Show Gist options
  • Save shiracamus/db0ddc34b5e117f5a258e1a7dd94bf2f to your computer and use it in GitHub Desktop.
Save shiracamus/db0ddc34b5e117f5a258e1a7dd94bf2f to your computer and use it in GitHub Desktop.
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