Шашки¶
import streamlit as st
from dataclasses import dataclass
from typing import List, Tuple, Optional
import math
import functools
import sys
Print banner
st.set_page_config(
page_title="Checkers"
)
Board
Board = List[List[str]] # 8x8
@dataclass
class Move:
# Для взятий seq содержит последовательность позиций (r,c) приземлений
# start хранит начальную клетку (для удобства печати/применения)
start: Tuple[int, int]
seq: List[Tuple[int, int]]
captured: List[Tuple[int, int]] # клетки сбитых фигур по порядку
is_capture: bool
DIRECTIONS_MAN_WHITE = [(1, -1), (1, 1)] # обычные ходы белой простой
DIRECTIONS_MAN_BLACK = [(-1, -1), (-1, 1)]
DIAGONALS = [(-1,-1),(-1,1),(1,-1),(1,1)] # для взятий и дамки
def clone(board: Board) -> Board:
return [row[:] for row in board]
def inside(r, c): return 0 <= r < 8 and 0 <= c < 8
def is_white(p): return p in ('w','W')
def is_black(p): return p in ('b','B')
def is_king(p): return p in ('W','B')
def side_matches(p: str, side: str) -> bool:
return (side == 'w' and is_white(p)) or (side == 'b' and is_black(p))
def enemy_of(side: str) -> str:
return 'b' if side == 'w' else 'w'
def parse_board(s: str) -> Board:
# 8 строк, разделённых \n
rows = [list(line.strip()) for line in s.strip().splitlines()]
assert len(rows) == 8 and all(len(r)==8 for r in rows)
rows.reverse()
return rows
def board_to_str(board: Board) -> str:
return "\n".join("".join(r) for r in board)
def promote_if_needed(piece: str, r: int, side: str) -> str:
if piece == 'w' and r == 7:
return 'W'
if piece == 'b' and r == 0:
return 'B'
return piece
Применить ход
def apply_move(board: Board, move: Move, side: str) -> Board:
b = clone(board)
sr, sc = move.start
piece = b[sr][sc]
b[sr][sc] = '.'
r, c = sr, sc
if move.is_capture:
for idx, (nr, nc) in enumerate(move.seq):
# убираем сбитую фигуру
cr, cc = move.captured[idx]
b[cr][cc] = '.'
r, c = nr, nc
# промежуточная промоция: простая может стать дамкой посреди удара
if not is_king(piece):
piece = promote_if_needed(piece, r, side)
b[r][c] = piece
else:
(r, c) = move.seq[-1]
# финальная промоция
piece = promote_if_needed(piece, r, side)
b[r][c] = piece
return b\
Сгенерировать возможные ходы
def generate_moves(board: Board, side: str) -> List[Move]:
captures = []
quiets = []
for r in range(8):
for c in range(8):
p = board[r][c]
if p == '.' or not side_matches(p, side):
continue
captures.extend(gen_captures_from(board, r, c, side))
if captures:
# правило большинства: оставить только с макс количеством взятий
max_cap = max(len(m.captured) for m in captures)
return [m for m in captures if len(m.captured) == max_cap]
# иначе — простые ходы
for r in range(8):
for c in range(8):
p = board[r][c]
if p == '.' or not side_matches(p, side):
continue
quiets.extend(gen_quiets_from(board, r, c, side))
return quiets
Сгенерировать простые ходы
def gen_quiets_from(board: Board, r: int, c: int, side: str) -> List[Move]:
p = board[r][c]
res = []
if is_king(p):
# дамка: любое расстояние по диагонали до первой преграды
for dr, dc in DIAGONALS:
nr, nc = r + dr, c + dc
while inside(nr, nc) and board[nr][nc] == '.':
res.append(Move((r,c), [(nr,nc)], [], False))
nr += dr; nc += dc
else:
dirs = DIRECTIONS_MAN_WHITE if side == 'w' else DIRECTIONS_MAN_BLACK
for dr, dc in dirs:
nr, nc = r + dr, c + dc
if inside(nr, nc) and board[nr][nc] == '.':
res.append(Move((r,c), [(nr,nc)], [], False))
return res
Сгенерировать возможные взятия
def gen_captures_from(board: Board, r: int, c: int, side: str) -> List[Move]:
p = board[r][c]
results: List[Move] = []
visited = set()
def rec(bd: Board, cr: int, cc: int, piece: str,
path: List[Tuple[int,int]], caps: List[Tuple[int,int]]):
key = (cr,cc, tuple(path), tuple(caps), piece)
if key in visited:
return
visited.add(key)
found = False
if is_king(piece):
# дамка: можем "перепрыгнуть" ровно через одну вражескую и встать дальше на любую пустую
for dr, dc in DIAGONALS:
nr, nc = cr + dr, cc + dc
# идём, пока пусто
while inside(nr, nc) and bd[nr][nc] == '.':
nr += dr; nc += dc
if inside(nr, nc) and bd[nr][nc] != '.' and side_matches(bd[nr][nc], enemy_of(side)):
# нашли вражескую; за ней должны быть пустые клетки приземления
j_r, j_c = nr + dr, nc + dc
while inside(j_r, j_c) and bd[j_r][j_c] == '.':
# выполнить прыжок
nb = clone(bd)
nb[cr][cc] = '.'
nb[nr][nc] = '.'
nb[j_r][j_c] = piece
# продолжить
rec(nb, j_r, j_c, piece, path + [(j_r,j_c)], caps + [(nr,nc)])
found = True
j_r += dr; j_c += dc
else:
# простая: прыгаем на 2 клетки (в любом из 4 диагональных направлений), если там враг и за ним пусто
for dr, dc in DIAGONALS:
mr, mc = cr + dr, cc + dc
jr, jc = cr + 2*dr, cc + 2*dc
if inside(jr, jc) and inside(mr, mc) and side_matches(bd[mr][mc], enemy_of(side)) and bd[jr][jc] == '.':
nb = clone(bd)
nb[cr][cc] = '.'
nb[mr][mc] = '.'
# приземляемся
new_piece = promote_if_needed(piece, jr, side)
nb[jr][jc] = new_piece
# если стали дамкой — продолжим как дамка
rec(nb, jr, jc, new_piece, path + [(jr,jc)], caps + [(mr,mc)])
found = True
if not found and caps:
# конец цепочки взятий
results.append(Move((r,c), path, caps, True))
rec(board, r, c, p, [], [])
if not results:
return results
# Фильтрация: убираем те последовательности, множество взятых фигур которых
# является строгим подмножеством множества взятых фигур другой последовательности.
filtered: List[Move] = []
captured_sets = [set(m.captured) for m in results]
for i, m in enumerate(results):
mset = captured_sets[i]
# строгий поднабор?
if any(mset < otherset for j, otherset in enumerate(captured_sets) if i != j):
continue
filtered.append(m)
# Дополнительно устраним возможные дубликаты одинаковых путей (seq) после фильтра.
unique = {}
for mv in filtered:
key = tuple(mv.seq), tuple(mv.captured)
unique[key] = mv # последний побеждает, безразлично
return list(unique.values())
Оценка позиции
def evaluate(board: Board, side_to_move: str) -> int:
# простая и понятная эвристика
score = 0
for r in range(8):
for c in range(8):
p = board[r][c]
if p == '.':
continue
val = 0
if p in ('w','b'):
val = 100
elif p in ('W','B'):
val = 300
# небольшие бонусы за продвижение простых и мобильность
if p == 'w': val += r*3
if p == 'b': val += (7-r)*3
score += val if is_white(p) else -val
# лёгкий бонус стороне хода
if side_to_move == 'w':
score += 5
else:
score -= 5
return score
Альфа-бета
@functools.lru_cache(maxsize=100000)
def _hashable_position(board_str: str, side: str, depth: int, alpha: int, beta: int):
# Заглушка для lru_cache-совместимости
return board_str, side, depth, alpha, beta
def alphabeta(board: Board, side: str, depth: int, alpha: int, beta: int) -> Tuple[int, Optional[Move]]:
moves = generate_moves(board, side)
if depth == 0 or not moves:
return evaluate(board, side), None
best_move = None
if side == 'w':
value = -math.inf
# упорядочивание: сначала длинные взятия
moves.sort(key=lambda m: (m.is_capture, len(m.captured)), reverse=True)
for m in moves:
nb = apply_move(board, m, side)
score, _ = alphabeta(nb, 'b', depth-1, alpha, beta)
if score > value:
value = score
best_move = m
alpha = max(alpha, value)
if alpha >= beta:
break
return int(value), best_move
else:
value = math.inf
moves.sort(key=lambda m: (m.is_capture, len(m.captured)), reverse=True)
for m in moves:
nb = apply_move(board, m, side)
score, _ = alphabeta(nb, 'w', depth-1, alpha, beta)
if score < value:
value = score
best_move = m
beta = min(beta, value)
if alpha >= beta:
break
return int(value), best_move
Утилита решения задачи (найти лучший ход за указанную сторону)
def solve_position(board_text: str, side: str, depth: int = 8):
board = parse_board(board_text)
score, move = alphabeta(board, side, depth, -10**9, 10**9)
return score, move
Read example position from file
BOARD_EMPTY = """
_._._._.
._._._._
_._._._.
._._._._
_._._._.
._._._._
_._._._.
._._._._
""".strip()
BOARD_INITIAL = """
_._._b_.
._w_b_._
_._._._w
._._._w_
_b_._w_.
._b_w_._
_b_._._.
._._._._
""".strip()
if len(sys.argv) > 1:
filename = sys.argv[1]
with open(filename, "r") as f:
example = f.read().strip()
else:
example = BOARD_INITIAL
col1, col2, col3 = st.columns([7,1,7])
with col1:
if st.button("New", width="stretch"):
example = BOARD_EMPTY
if st.button("Start", width="stretch"):
example = BOARD_INITIAL
with col2:
row_nums = ""
for i in range(7, -1, -1):
row_nums += str(i) + " \n"
st.text_area(f".", value=row_nums, height=300)
with col3:
example = st.text_area(f"Position", value=example + "\n01234567", height=300)
if st.button("Solve", type="primary", width="stretch"):
sc, mv = solve_position(example.replace("\n01234567", ""), 'w', depth=8)
st.write("Score:", sc)
if mv:
st.write("From:", mv.start)
st.write("Seq:", mv.seq)
st.write("Captured:", mv.captured)
st.write("Capture?" , mv.is_capture)