cli-othello 0.1.0__tar.gz → 1.0.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {cli_othello-0.1.0 → cli_othello-1.0.2}/PKG-INFO +1 -1
- {cli_othello-0.1.0 → cli_othello-1.0.2}/pyproject.toml +1 -1
- {cli_othello-0.1.0 → cli_othello-1.0.2}/src/cli_othello/__init__.py +1 -1
- cli_othello-1.0.2/src/cli_othello/ui.py +301 -0
- {cli_othello-0.1.0 → cli_othello-1.0.2}/src/cli_othello.egg-info/PKG-INFO +1 -1
- cli_othello-0.1.0/src/cli_othello/ui.py +0 -194
- {cli_othello-0.1.0 → cli_othello-1.0.2}/README.md +0 -0
- {cli_othello-0.1.0 → cli_othello-1.0.2}/setup.cfg +0 -0
- {cli_othello-0.1.0 → cli_othello-1.0.2}/src/cli_othello/ai.py +0 -0
- {cli_othello-0.1.0 → cli_othello-1.0.2}/src/cli_othello/board.py +0 -0
- {cli_othello-0.1.0 → cli_othello-1.0.2}/src/cli_othello/cli.py +0 -0
- {cli_othello-0.1.0 → cli_othello-1.0.2}/src/cli_othello.egg-info/SOURCES.txt +0 -0
- {cli_othello-0.1.0 → cli_othello-1.0.2}/src/cli_othello.egg-info/dependency_links.txt +0 -0
- {cli_othello-0.1.0 → cli_othello-1.0.2}/src/cli_othello.egg-info/entry_points.txt +0 -0
- {cli_othello-0.1.0 → cli_othello-1.0.2}/src/cli_othello.egg-info/requires.txt +0 -0
- {cli_othello-0.1.0 → cli_othello-1.0.2}/src/cli_othello.egg-info/top_level.txt +0 -0
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
"""curses-based terminal UI for cli-othello."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import curses
|
|
6
|
+
|
|
7
|
+
from .ai import LEVEL_LABELS, MAX_LEVEL, MIN_LEVEL, OthelloAI
|
|
8
|
+
from .board import BLACK, EMPTY, SIZE, WHITE, Board, opponent
|
|
9
|
+
|
|
10
|
+
PLAYER_NAMES = {BLACK: "黒", WHITE: "白"}
|
|
11
|
+
|
|
12
|
+
# Each cell is rendered as CELL_W columns x CELL_H rows so it reads as
|
|
13
|
+
# roughly square in a terminal (chars are about twice as tall as wide).
|
|
14
|
+
CELL_W = 5
|
|
15
|
+
CELL_H = 2
|
|
16
|
+
|
|
17
|
+
DISC_BLACK = "●"
|
|
18
|
+
DISC_WHITE = "○"
|
|
19
|
+
HINT_MARK = "+"
|
|
20
|
+
|
|
21
|
+
COLOR_BOARD = 1
|
|
22
|
+
COLOR_BLACK = 2
|
|
23
|
+
COLOR_WHITE = 3
|
|
24
|
+
COLOR_CURSOR_EMPTY = 4
|
|
25
|
+
COLOR_CURSOR_BLACK = 5
|
|
26
|
+
COLOR_CURSOR_WHITE = 6
|
|
27
|
+
COLOR_HINT = 7
|
|
28
|
+
COLOR_HEADER = 8
|
|
29
|
+
COLOR_STATUS = 9
|
|
30
|
+
COLOR_PANEL = 10
|
|
31
|
+
COLOR_GRID = 11
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def init_colors() -> None:
|
|
35
|
+
curses.start_color()
|
|
36
|
+
curses.use_default_colors()
|
|
37
|
+
curses.init_pair(COLOR_BOARD, curses.COLOR_GREEN, curses.COLOR_GREEN)
|
|
38
|
+
# Discs fill the whole cell with their own color for maximum visibility.
|
|
39
|
+
curses.init_pair(COLOR_BLACK, curses.COLOR_WHITE, curses.COLOR_BLACK)
|
|
40
|
+
curses.init_pair(COLOR_WHITE, curses.COLOR_BLACK, curses.COLOR_WHITE)
|
|
41
|
+
curses.init_pair(COLOR_CURSOR_EMPTY, curses.COLOR_BLACK, curses.COLOR_YELLOW)
|
|
42
|
+
curses.init_pair(COLOR_CURSOR_BLACK, curses.COLOR_YELLOW, curses.COLOR_BLACK)
|
|
43
|
+
curses.init_pair(COLOR_CURSOR_WHITE, curses.COLOR_YELLOW, curses.COLOR_WHITE)
|
|
44
|
+
curses.init_pair(COLOR_HINT, curses.COLOR_YELLOW, curses.COLOR_GREEN)
|
|
45
|
+
curses.init_pair(COLOR_HEADER, curses.COLOR_CYAN, -1)
|
|
46
|
+
curses.init_pair(COLOR_STATUS, curses.COLOR_YELLOW, -1)
|
|
47
|
+
curses.init_pair(COLOR_PANEL, curses.COLOR_WHITE, -1)
|
|
48
|
+
curses.init_pair(COLOR_GRID, curses.COLOR_GREEN, -1)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def select_level(stdscr: "curses._CursesWindow") -> int:
|
|
52
|
+
"""Let the user pick an AI difficulty level (1-5) with arrow keys."""
|
|
53
|
+
level = 3
|
|
54
|
+
curses.curs_set(0)
|
|
55
|
+
required_rows = 5 + MAX_LEVEL + 3
|
|
56
|
+
required_cols = 36
|
|
57
|
+
while True:
|
|
58
|
+
stdscr.erase()
|
|
59
|
+
max_y, max_x = stdscr.getmaxyx()
|
|
60
|
+
if max_y < required_rows or max_x < required_cols:
|
|
61
|
+
_draw_too_small(stdscr)
|
|
62
|
+
continue
|
|
63
|
+
stdscr.addstr(1, 2, "♟ CLI OTHELLO", curses.color_pair(COLOR_HEADER) | curses.A_BOLD)
|
|
64
|
+
stdscr.addstr(3, 2, "対戦するAIの強さを選んでください", curses.A_BOLD)
|
|
65
|
+
for i in range(MIN_LEVEL, MAX_LEVEL + 1):
|
|
66
|
+
label = LEVEL_LABELS[i]
|
|
67
|
+
if i == level:
|
|
68
|
+
stdscr.addstr(5 + i, 2, "> ", curses.A_BOLD)
|
|
69
|
+
stdscr.addstr(5 + i, 4, label, curses.A_REVERSE | curses.A_BOLD)
|
|
70
|
+
else:
|
|
71
|
+
stdscr.addstr(5 + i, 4, label, curses.A_NORMAL)
|
|
72
|
+
stdscr.addstr(
|
|
73
|
+
5 + MAX_LEVEL + 2,
|
|
74
|
+
2,
|
|
75
|
+
"↑/↓ で選択 Enter で決定 q で終了",
|
|
76
|
+
curses.color_pair(COLOR_STATUS),
|
|
77
|
+
)
|
|
78
|
+
stdscr.refresh()
|
|
79
|
+
|
|
80
|
+
key = stdscr.getch()
|
|
81
|
+
if key in (curses.KEY_UP, ord("k")):
|
|
82
|
+
level = MIN_LEVEL if level <= MIN_LEVEL else level - 1
|
|
83
|
+
elif key in (curses.KEY_DOWN, ord("j")):
|
|
84
|
+
level = MAX_LEVEL if level >= MAX_LEVEL else level + 1
|
|
85
|
+
elif key in (curses.KEY_ENTER, 10, 13):
|
|
86
|
+
return level
|
|
87
|
+
elif key in (ord("q"), ord("Q")):
|
|
88
|
+
raise SystemExit(0)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _cell_fill_attr(cell: int, is_cursor: bool) -> int:
|
|
92
|
+
"""Background attribute that fills the whole cell (all CELL_H rows)."""
|
|
93
|
+
if cell == BLACK:
|
|
94
|
+
return curses.color_pair(COLOR_CURSOR_BLACK if is_cursor else COLOR_BLACK) | curses.A_BOLD
|
|
95
|
+
if cell == WHITE:
|
|
96
|
+
return curses.color_pair(COLOR_CURSOR_WHITE if is_cursor else COLOR_WHITE) | curses.A_BOLD
|
|
97
|
+
if is_cursor:
|
|
98
|
+
return curses.color_pair(COLOR_CURSOR_EMPTY) | curses.A_BOLD
|
|
99
|
+
return curses.color_pair(COLOR_BOARD)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _cell_center_attr(cell: int, is_cursor: bool, is_hint: bool) -> tuple[int, str]:
|
|
103
|
+
"""Return (attribute, character) for the center row of a cell."""
|
|
104
|
+
fill = _cell_fill_attr(cell, is_cursor)
|
|
105
|
+
if cell == BLACK:
|
|
106
|
+
return fill, DISC_BLACK
|
|
107
|
+
if cell == WHITE:
|
|
108
|
+
return fill, DISC_WHITE
|
|
109
|
+
if is_hint:
|
|
110
|
+
attr = curses.color_pair(COLOR_CURSOR_EMPTY if is_cursor else COLOR_HINT) | curses.A_BOLD
|
|
111
|
+
return attr, HINT_MARK
|
|
112
|
+
return fill, " "
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _required_size() -> tuple[int, int]:
|
|
116
|
+
"""Return (rows, cols) needed to draw the board and panel, plus a
|
|
117
|
+
1-line/1-column margin (curses can refuse writes to the very last
|
|
118
|
+
row/column of the screen)."""
|
|
119
|
+
rows = 3 + 1 + SIZE * CELL_H + 6 + 1
|
|
120
|
+
cols = 4 + 3 + SIZE * CELL_W + 1
|
|
121
|
+
return rows, cols
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _draw_too_small(stdscr: "curses._CursesWindow") -> None:
|
|
125
|
+
rows, cols = _required_size()
|
|
126
|
+
stdscr.erase()
|
|
127
|
+
try:
|
|
128
|
+
stdscr.addstr(0, 0, "ターミナルが小さすぎます。")
|
|
129
|
+
stdscr.addstr(1, 0, f"必要サイズ: {cols}列 x {rows}行 以上")
|
|
130
|
+
stdscr.addstr(2, 0, "ウィンドウを大きくしてください。何かキーを押すと終了します。")
|
|
131
|
+
except curses.error:
|
|
132
|
+
pass
|
|
133
|
+
stdscr.refresh()
|
|
134
|
+
stdscr.getch()
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def _draw_board(
|
|
138
|
+
stdscr: "curses._CursesWindow",
|
|
139
|
+
board: Board,
|
|
140
|
+
cursor: tuple[int, int],
|
|
141
|
+
legal: set[tuple[int, int]],
|
|
142
|
+
human_player: int,
|
|
143
|
+
ai_level: int,
|
|
144
|
+
message: str,
|
|
145
|
+
top: int = 0,
|
|
146
|
+
) -> bool:
|
|
147
|
+
"""Draw the board. Returns False (and shows a hint) if the terminal is
|
|
148
|
+
too small to fit it."""
|
|
149
|
+
required_rows, required_cols = _required_size()
|
|
150
|
+
max_y, max_x = stdscr.getmaxyx()
|
|
151
|
+
if max_y < required_rows or max_x < required_cols:
|
|
152
|
+
_draw_too_small(stdscr)
|
|
153
|
+
return False
|
|
154
|
+
|
|
155
|
+
stdscr.erase()
|
|
156
|
+
stdscr.addstr(top, 2, "♟ CLI OTHELLO", curses.color_pair(COLOR_HEADER) | curses.A_BOLD)
|
|
157
|
+
stdscr.addstr(
|
|
158
|
+
top + 1,
|
|
159
|
+
2,
|
|
160
|
+
f"AI: {LEVEL_LABELS[ai_level]} あなた: {DISC_BLACK if human_player == BLACK else DISC_WHITE} {PLAYER_NAMES[human_player]}",
|
|
161
|
+
curses.color_pair(COLOR_PANEL),
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
board_top = top + 3
|
|
165
|
+
board_left = 4
|
|
166
|
+
grid_attr = curses.color_pair(COLOR_GRID)
|
|
167
|
+
|
|
168
|
+
# Column labels (A-H), centered above each cell.
|
|
169
|
+
header = " " * 3
|
|
170
|
+
for col in range(SIZE):
|
|
171
|
+
header += "ABCDEFGH"[col].center(CELL_W)
|
|
172
|
+
stdscr.addstr(board_top, board_left, header, curses.A_BOLD)
|
|
173
|
+
|
|
174
|
+
board_width = 3 + SIZE * CELL_W
|
|
175
|
+
for row in range(SIZE):
|
|
176
|
+
for line in range(CELL_H):
|
|
177
|
+
y = board_top + 1 + row * CELL_H + line
|
|
178
|
+
if line == CELL_H // 2:
|
|
179
|
+
row_label = f"{row + 1:>2} "
|
|
180
|
+
else:
|
|
181
|
+
row_label = " "
|
|
182
|
+
stdscr.addstr(y, board_left, row_label, curses.A_BOLD)
|
|
183
|
+
for col in range(SIZE):
|
|
184
|
+
cell = board.get(row, col)
|
|
185
|
+
is_cursor = (row, col) == cursor
|
|
186
|
+
is_hint = cell == EMPTY and (row, col) in legal
|
|
187
|
+
x = board_left + 3 + col * CELL_W
|
|
188
|
+
|
|
189
|
+
if line == CELL_H // 2:
|
|
190
|
+
attr, ch = _cell_center_attr(cell, is_cursor, is_hint)
|
|
191
|
+
text = ch.center(CELL_W)
|
|
192
|
+
else:
|
|
193
|
+
attr = _cell_fill_attr(cell, is_cursor)
|
|
194
|
+
text = " " * CELL_W
|
|
195
|
+
stdscr.addstr(y, x, text, attr)
|
|
196
|
+
|
|
197
|
+
board_bottom = board_top + 1 + SIZE * CELL_H
|
|
198
|
+
panel_left = board_left
|
|
199
|
+
panel_width = board_width
|
|
200
|
+
|
|
201
|
+
you_mark = DISC_BLACK if human_player == BLACK else DISC_WHITE
|
|
202
|
+
ai_mark = DISC_BLACK if human_player == WHITE else DISC_WHITE
|
|
203
|
+
|
|
204
|
+
stdscr.addstr(board_bottom + 1, panel_left, "─" * panel_width, grid_attr)
|
|
205
|
+
stdscr.addstr(
|
|
206
|
+
board_bottom + 2,
|
|
207
|
+
panel_left,
|
|
208
|
+
f"{DISC_BLACK} 黒 {board.count(BLACK):>2} {DISC_WHITE} 白 {board.count(WHITE):>2}"
|
|
209
|
+
f" あなた:{you_mark} AI:{ai_mark}",
|
|
210
|
+
curses.A_BOLD,
|
|
211
|
+
)
|
|
212
|
+
stdscr.addstr(board_bottom + 3, panel_left, "─" * panel_width, grid_attr)
|
|
213
|
+
stdscr.addstr(
|
|
214
|
+
board_bottom + 4, panel_left, message, curses.color_pair(COLOR_STATUS) | curses.A_BOLD
|
|
215
|
+
)
|
|
216
|
+
stdscr.addstr(
|
|
217
|
+
board_bottom + 5,
|
|
218
|
+
panel_left,
|
|
219
|
+
"矢印キー/hjkl: 移動 Enter/Space: 着手 q: 終了",
|
|
220
|
+
curses.color_pair(COLOR_STATUS),
|
|
221
|
+
)
|
|
222
|
+
stdscr.refresh()
|
|
223
|
+
return True
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def play_game(stdscr: "curses._CursesWindow", human_player: int, ai_level: int) -> None:
|
|
227
|
+
curses.curs_set(0)
|
|
228
|
+
board = Board()
|
|
229
|
+
ai_player = opponent(human_player)
|
|
230
|
+
ai = OthelloAI(ai_player, ai_level)
|
|
231
|
+
cursor = [3, 3]
|
|
232
|
+
turn = BLACK
|
|
233
|
+
message = ""
|
|
234
|
+
|
|
235
|
+
while True:
|
|
236
|
+
if board.is_game_over():
|
|
237
|
+
break
|
|
238
|
+
|
|
239
|
+
legal = set(board.legal_moves(turn))
|
|
240
|
+
if not legal:
|
|
241
|
+
message = f"{PLAYER_NAMES[turn]}はパスしました"
|
|
242
|
+
turn = opponent(turn)
|
|
243
|
+
continue
|
|
244
|
+
|
|
245
|
+
if turn == human_player:
|
|
246
|
+
message = "あなたの番です"
|
|
247
|
+
if not _draw_board(
|
|
248
|
+
stdscr, board, tuple(cursor), legal, human_player, ai_level, message
|
|
249
|
+
):
|
|
250
|
+
continue
|
|
251
|
+
key = stdscr.getch()
|
|
252
|
+
if key in (curses.KEY_UP, ord("k")):
|
|
253
|
+
cursor[0] = (cursor[0] - 1) % SIZE
|
|
254
|
+
elif key in (curses.KEY_DOWN, ord("j")):
|
|
255
|
+
cursor[0] = (cursor[0] + 1) % SIZE
|
|
256
|
+
elif key in (curses.KEY_LEFT, ord("h")):
|
|
257
|
+
cursor[1] = (cursor[1] - 1) % SIZE
|
|
258
|
+
elif key in (curses.KEY_RIGHT, ord("l")):
|
|
259
|
+
cursor[1] = (cursor[1] + 1) % SIZE
|
|
260
|
+
elif key in (curses.KEY_ENTER, 10, 13, ord(" ")):
|
|
261
|
+
if tuple(cursor) in legal:
|
|
262
|
+
board.play(cursor[0], cursor[1], turn)
|
|
263
|
+
turn = opponent(turn)
|
|
264
|
+
else:
|
|
265
|
+
message = "そこには置けません"
|
|
266
|
+
elif key in (ord("q"), ord("Q")):
|
|
267
|
+
return
|
|
268
|
+
else:
|
|
269
|
+
message = "AIが考えています…"
|
|
270
|
+
_draw_board(stdscr, board, tuple(cursor), legal, human_player, ai_level, message)
|
|
271
|
+
# If the terminal is too small, _draw_too_small() already
|
|
272
|
+
# blocked on a keypress above; proceed with the AI move
|
|
273
|
+
# regardless so the game keeps progressing once resized.
|
|
274
|
+
move = ai.choose_move(board)
|
|
275
|
+
if move is not None:
|
|
276
|
+
board.play(*move, turn)
|
|
277
|
+
turn = opponent(turn)
|
|
278
|
+
|
|
279
|
+
winner = board.winner()
|
|
280
|
+
if winner is None:
|
|
281
|
+
result = "引き分けです"
|
|
282
|
+
elif winner == human_player:
|
|
283
|
+
result = "あなたの勝ちです!"
|
|
284
|
+
else:
|
|
285
|
+
result = "AIの勝ちです"
|
|
286
|
+
|
|
287
|
+
while not _draw_board(
|
|
288
|
+
stdscr, board, tuple(cursor), set(), human_player, ai_level, result
|
|
289
|
+
):
|
|
290
|
+
pass
|
|
291
|
+
|
|
292
|
+
max_y, max_x = stdscr.getmaxyx()
|
|
293
|
+
footer_y = min(SIZE + 8, max_y - 1)
|
|
294
|
+
try:
|
|
295
|
+
stdscr.addstr(footer_y, 4, "何かキーを押すと終了します", curses.color_pair(COLOR_STATUS))
|
|
296
|
+
except curses.error:
|
|
297
|
+
pass
|
|
298
|
+
stdscr.refresh()
|
|
299
|
+
stdscr.getch()
|
|
300
|
+
|
|
301
|
+
|
|
@@ -1,194 +0,0 @@
|
|
|
1
|
-
"""curses-based terminal UI for cli-othello."""
|
|
2
|
-
|
|
3
|
-
from __future__ import annotations
|
|
4
|
-
|
|
5
|
-
import curses
|
|
6
|
-
|
|
7
|
-
from .ai import LEVEL_LABELS, MAX_LEVEL, MIN_LEVEL, OthelloAI
|
|
8
|
-
from .board import BLACK, EMPTY, SIZE, WHITE, Board, opponent
|
|
9
|
-
|
|
10
|
-
DISC_CHARS = {EMPTY: " ", BLACK: "●", WHITE: "○"}
|
|
11
|
-
PLAYER_NAMES = {BLACK: "黒", WHITE: "白"}
|
|
12
|
-
|
|
13
|
-
COLOR_BOARD = 1
|
|
14
|
-
COLOR_BLACK = 2
|
|
15
|
-
COLOR_WHITE = 3
|
|
16
|
-
COLOR_CURSOR = 4
|
|
17
|
-
COLOR_HINT = 5
|
|
18
|
-
COLOR_HEADER = 6
|
|
19
|
-
COLOR_STATUS = 7
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
def init_colors() -> None:
|
|
23
|
-
curses.start_color()
|
|
24
|
-
curses.use_default_colors()
|
|
25
|
-
curses.init_pair(COLOR_BOARD, curses.COLOR_WHITE, curses.COLOR_GREEN)
|
|
26
|
-
curses.init_pair(COLOR_BLACK, curses.COLOR_BLACK, curses.COLOR_GREEN)
|
|
27
|
-
curses.init_pair(COLOR_WHITE, curses.COLOR_WHITE, curses.COLOR_GREEN)
|
|
28
|
-
curses.init_pair(COLOR_CURSOR, curses.COLOR_BLACK, curses.COLOR_YELLOW)
|
|
29
|
-
curses.init_pair(COLOR_HINT, curses.COLOR_GREEN, curses.COLOR_BLACK)
|
|
30
|
-
curses.init_pair(COLOR_HEADER, curses.COLOR_CYAN, -1)
|
|
31
|
-
curses.init_pair(COLOR_STATUS, curses.COLOR_YELLOW, -1)
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
def select_level(stdscr: "curses._CursesWindow") -> int:
|
|
35
|
-
"""Let the user pick an AI difficulty level (1-5) with arrow keys."""
|
|
36
|
-
level = 3
|
|
37
|
-
curses.curs_set(0)
|
|
38
|
-
while True:
|
|
39
|
-
stdscr.erase()
|
|
40
|
-
stdscr.addstr(1, 2, "CLI OTHELLO", curses.color_pair(COLOR_HEADER) | curses.A_BOLD)
|
|
41
|
-
stdscr.addstr(3, 2, "対戦するAIの強さを選んでください", curses.color_pair(COLOR_HEADER))
|
|
42
|
-
for i in range(MIN_LEVEL, MAX_LEVEL + 1):
|
|
43
|
-
label = LEVEL_LABELS[i]
|
|
44
|
-
attr = curses.A_REVERSE | curses.A_BOLD if i == level else curses.A_NORMAL
|
|
45
|
-
stdscr.addstr(5 + i, 4, f"{label}", attr)
|
|
46
|
-
stdscr.addstr(
|
|
47
|
-
5 + MAX_LEVEL + 2,
|
|
48
|
-
2,
|
|
49
|
-
"↑/↓ で選択、Enter で決定、q で終了",
|
|
50
|
-
curses.color_pair(COLOR_STATUS),
|
|
51
|
-
)
|
|
52
|
-
stdscr.refresh()
|
|
53
|
-
|
|
54
|
-
key = stdscr.getch()
|
|
55
|
-
if key in (curses.KEY_UP, ord("k")):
|
|
56
|
-
level = MIN_LEVEL if level <= MIN_LEVEL else level - 1
|
|
57
|
-
elif key in (curses.KEY_DOWN, ord("j")):
|
|
58
|
-
level = MAX_LEVEL if level >= MAX_LEVEL else level + 1
|
|
59
|
-
elif key in (curses.KEY_ENTER, 10, 13):
|
|
60
|
-
return level
|
|
61
|
-
elif key in (ord("q"), ord("Q")):
|
|
62
|
-
raise SystemExit(0)
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
def _draw_board(
|
|
66
|
-
stdscr: "curses._CursesWindow",
|
|
67
|
-
board: Board,
|
|
68
|
-
cursor: tuple[int, int],
|
|
69
|
-
legal: set[tuple[int, int]],
|
|
70
|
-
human_player: int,
|
|
71
|
-
ai_level: int,
|
|
72
|
-
message: str,
|
|
73
|
-
top: int = 0,
|
|
74
|
-
) -> None:
|
|
75
|
-
stdscr.erase()
|
|
76
|
-
stdscr.addstr(top, 2, "CLI OTHELLO", curses.color_pair(COLOR_HEADER) | curses.A_BOLD)
|
|
77
|
-
stdscr.addstr(
|
|
78
|
-
top,
|
|
79
|
-
16,
|
|
80
|
-
f"AI: {LEVEL_LABELS[ai_level]} あなた: {DISC_CHARS[human_player]}{PLAYER_NAMES[human_player]}",
|
|
81
|
-
curses.color_pair(COLOR_STATUS),
|
|
82
|
-
)
|
|
83
|
-
|
|
84
|
-
board_top = top + 2
|
|
85
|
-
board_left = 4
|
|
86
|
-
|
|
87
|
-
stdscr.addstr(board_top, board_left + 2, " ".join("ABCDEFGH"))
|
|
88
|
-
for row in range(SIZE):
|
|
89
|
-
stdscr.addstr(board_top + 1 + row, board_left, f"{row + 1} ")
|
|
90
|
-
for col in range(SIZE):
|
|
91
|
-
cell = board.get(row, col)
|
|
92
|
-
char = DISC_CHARS[cell]
|
|
93
|
-
x = board_left + 2 + col * 3
|
|
94
|
-
y = board_top + 1 + row
|
|
95
|
-
|
|
96
|
-
if (row, col) == cursor:
|
|
97
|
-
attr = curses.color_pair(COLOR_CURSOR) | curses.A_BOLD
|
|
98
|
-
elif cell == BLACK:
|
|
99
|
-
attr = curses.color_pair(COLOR_BLACK) | curses.A_BOLD
|
|
100
|
-
elif cell == WHITE:
|
|
101
|
-
attr = curses.color_pair(COLOR_WHITE) | curses.A_BOLD
|
|
102
|
-
else:
|
|
103
|
-
attr = curses.color_pair(COLOR_BOARD)
|
|
104
|
-
|
|
105
|
-
display = char
|
|
106
|
-
if cell == EMPTY and (row, col) in legal:
|
|
107
|
-
display = "·"
|
|
108
|
-
if (row, col) != cursor:
|
|
109
|
-
attr = curses.color_pair(COLOR_HINT)
|
|
110
|
-
|
|
111
|
-
stdscr.addstr(y, x, f" {display} ", attr)
|
|
112
|
-
|
|
113
|
-
score_y = board_top + SIZE + 2
|
|
114
|
-
stdscr.addstr(
|
|
115
|
-
score_y,
|
|
116
|
-
board_left,
|
|
117
|
-
f"● 黒: {board.count(BLACK)} ○ 白: {board.count(WHITE)}",
|
|
118
|
-
curses.A_BOLD,
|
|
119
|
-
)
|
|
120
|
-
stdscr.addstr(score_y + 2, board_left, message, curses.color_pair(COLOR_STATUS))
|
|
121
|
-
stdscr.addstr(
|
|
122
|
-
score_y + 3,
|
|
123
|
-
board_left,
|
|
124
|
-
"矢印キー: 移動 Enter/Space: 着手 q: 終了",
|
|
125
|
-
curses.color_pair(COLOR_STATUS),
|
|
126
|
-
)
|
|
127
|
-
stdscr.refresh()
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
def play_game(stdscr: "curses._CursesWindow", human_player: int, ai_level: int) -> None:
|
|
131
|
-
curses.curs_set(0)
|
|
132
|
-
board = Board()
|
|
133
|
-
ai_player = opponent(human_player)
|
|
134
|
-
ai = OthelloAI(ai_player, ai_level)
|
|
135
|
-
cursor = [3, 3]
|
|
136
|
-
turn = BLACK
|
|
137
|
-
message = ""
|
|
138
|
-
|
|
139
|
-
while True:
|
|
140
|
-
if board.is_game_over():
|
|
141
|
-
break
|
|
142
|
-
|
|
143
|
-
legal = set(board.legal_moves(turn))
|
|
144
|
-
if not legal:
|
|
145
|
-
message = f"{PLAYER_NAMES[turn]}はパスしました"
|
|
146
|
-
turn = opponent(turn)
|
|
147
|
-
continue
|
|
148
|
-
|
|
149
|
-
if turn == human_player:
|
|
150
|
-
message = "あなたの番です"
|
|
151
|
-
_draw_board(stdscr, board, tuple(cursor), legal, human_player, ai_level, message)
|
|
152
|
-
key = stdscr.getch()
|
|
153
|
-
if key in (curses.KEY_UP, ord("k")):
|
|
154
|
-
cursor[0] = (cursor[0] - 1) % SIZE
|
|
155
|
-
elif key in (curses.KEY_DOWN, ord("j")):
|
|
156
|
-
cursor[0] = (cursor[0] + 1) % SIZE
|
|
157
|
-
elif key in (curses.KEY_LEFT, ord("h")):
|
|
158
|
-
cursor[1] = (cursor[1] - 1) % SIZE
|
|
159
|
-
elif key in (curses.KEY_RIGHT, ord("l")):
|
|
160
|
-
cursor[1] = (cursor[1] + 1) % SIZE
|
|
161
|
-
elif key in (curses.KEY_ENTER, 10, 13, ord(" ")):
|
|
162
|
-
if tuple(cursor) in legal:
|
|
163
|
-
board.play(cursor[0], cursor[1], turn)
|
|
164
|
-
turn = opponent(turn)
|
|
165
|
-
else:
|
|
166
|
-
message = "そこには置けません"
|
|
167
|
-
elif key in (ord("q"), ord("Q")):
|
|
168
|
-
return
|
|
169
|
-
else:
|
|
170
|
-
message = "AIが考えています…"
|
|
171
|
-
_draw_board(stdscr, board, tuple(cursor), legal, human_player, ai_level, message)
|
|
172
|
-
move = ai.choose_move(board)
|
|
173
|
-
if move is not None:
|
|
174
|
-
board.play(*move, turn)
|
|
175
|
-
turn = opponent(turn)
|
|
176
|
-
|
|
177
|
-
winner = board.winner()
|
|
178
|
-
if winner is None:
|
|
179
|
-
result = "引き分けです"
|
|
180
|
-
elif winner == human_player:
|
|
181
|
-
result = "あなたの勝ちです!"
|
|
182
|
-
else:
|
|
183
|
-
result = "AIの勝ちです"
|
|
184
|
-
_draw_board(stdscr, board, tuple(cursor), set(), human_player, ai_level, result)
|
|
185
|
-
stdscr.addstr(
|
|
186
|
-
SIZE + 8,
|
|
187
|
-
4,
|
|
188
|
-
"何かキーを押すと終了します",
|
|
189
|
-
curses.color_pair(COLOR_STATUS),
|
|
190
|
-
)
|
|
191
|
-
stdscr.refresh()
|
|
192
|
-
stdscr.getch()
|
|
193
|
-
|
|
194
|
-
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|