Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- #!/usr/bin/env python3
- """
- Whale Shogi XBoard-compatible engine
- ======================================
- Rules: https://en.wikipedia.org/wiki/Whale_shogi
- Board: 6×6
- Goal: Capture the opponent's White Whale
- Author: Generated for XBoard / WinBoard protocol v2
- Piece movements (Black's "forward" = higher row numbers):
- W - White Whale : steps 1 in any of the 8 directions (King)
- G - Grey Whale : slides forward; slides diagonally backward
- P - Porpoise : steps 1 square sideways (left or right)
- K - Killer Whale : slides orthogonally; steps diagonally (Dragon King)
- N - Narwhal : jumps 2 squares forward; steps backward/sideways
- H - Humpback : steps diagonally (all 4); steps backward
- B - Blue Whale : steps forward, backward, diagonally-forward
- D - Dolphin : steps 1 forward; if on farthest rank, slides diagonally backward
- Drop rules:
- • Porpoise is never dropped – when captured it instantly promotes
- to Killer Whale and is held in hand as K.
- • Dolphin cannot be dropped on the farthest rank.
- • Dolphin cannot be dropped in a file that already has ≥ 2 friendly dolphins.
- • Dolphin cannot be dropped to give immediate checkmate ("dolphin-drop mate").
- """
- import sys
- import random
- import re
- # ── Colour constants ──────────────────────────────────────────────────────────
- BLACK, WHITE = 0, 1
- # ── Piece-type constants ──────────────────────────────────────────────────────
- WW = 1 # White Whale (king)
- PP = 2 # Porpoise (promotes to Killer Whale on capture)
- HH = 3 # Humpback
- GG = 4 # Grey Whale
- NN = 5 # Narwhal
- BB = 6 # Blue Whale
- DD = 7 # Dolphin
- KK = 8 # Killer Whale (promoted Porpoise)
- P2C = {WW: 'W', PP: 'P', HH: 'H', GG: 'G', NN: 'N', BB: 'B', DD: 'D', KK: 'K'}
- C2P = {v: k for k, v in P2C.items()}
- # Approximate piece values for evaluation (White Whale = ∞ treated as 10 000)
- VALUE = {WW: 10000, KK: 8, GG: 6, NN: 5, HH: 4, BB: 4, PP: 3, DD: 1}
- # ── Coordinate helpers ────────────────────────────────────────────────────────
- def sq2str(col: int, row: int) -> str:
- """(col 0-5, row 0-5) → 'a1' … 'f6'"""
- return chr(ord('a') + col) + str(row + 1)
- def str2sq(s: str):
- """'a1' → (col=0, row=0), 'f6' → (col=5, row=5)"""
- return ord(s[0]) - ord('a'), int(s[1]) - 1
- # ── Board ─────────────────────────────────────────────────────────────────────
- class Board:
- """
- cells[row][col] = (piece_type, colour) | None
- row 0 = rank 1 = Black's back rank
- row 5 = rank 6 = White's back rank
- """
- __slots__ = ('cells', 'hands', 'to_move')
- # ── Construction / reset ──────────────────────────────────────────────────
- def __init__(self):
- self.cells: list = [[None] * 6 for _ in range(6)]
- self.hands: dict = {BLACK: [], WHITE: []}
- self.to_move: int = BLACK
- self._setup()
- def _setup(self):
- # Black – rank 1 (row 0): H G W P N B
- for col, piece in enumerate([HH, GG, WW, PP, NN, BB]):
- self.cells[0][col] = (piece, BLACK)
- # Black – rank 2 (row 1): six dolphins
- for col in range(6):
- self.cells[1][col] = (DD, BLACK)
- # White – rank 6 (row 5): B N P W G H (mirror of Black)
- for col, piece in enumerate([BB, NN, PP, WW, GG, HH]):
- self.cells[5][col] = (piece, WHITE)
- # White – rank 5 (row 4): six dolphins
- for col in range(6):
- self.cells[4][col] = (DD, WHITE)
- # ── Copy ─────────────────────────────────────────────────────────────────
- def copy(self) -> 'Board':
- b = object.__new__(Board)
- b.cells = [row[:] for row in self.cells]
- b.hands = {BLACK: self.hands[BLACK][:], WHITE: self.hands[WHITE][:]}
- b.to_move = self.to_move
- return b
- # ── Low-level helpers ─────────────────────────────────────────────────────
- @staticmethod
- def _fwd(color: int) -> int:
- """Row-delta for 'forward' direction."""
- return 1 if color == BLACK else -1
- @staticmethod
- def _far_row(color: int) -> int:
- """Index of the farthest rank for the given colour."""
- return 5 if color == BLACK else 0
- @staticmethod
- def _inbounds(r: int, c: int) -> bool:
- return 0 <= r < 6 and 0 <= c < 6
- def _step(self, r, c, dr, dc, color, out):
- """Add one-step destination if legal."""
- r2, c2 = r + dr, c + dc
- if self._inbounds(r2, c2):
- tgt = self.cells[r2][c2]
- if tgt is None or tgt[1] != color:
- out.append((r2, c2))
- def _slide(self, r, c, dr, dc, color, out):
- """Add all slide-destinations in direction (dr,dc)."""
- r2, c2 = r + dr, c + dc
- while self._inbounds(r2, c2):
- tgt = self.cells[r2][c2]
- if tgt is None:
- out.append((r2, c2))
- elif tgt[1] != color:
- out.append((r2, c2))
- break
- else:
- break
- r2 += dr
- c2 += dc
- # ── Piece destinations (no check-filter) ─────────────────────────────────
- def get_dests(self, row: int, col: int) -> list:
- """
- Return all squares reachable by the piece at (row, col).
- Does NOT check whether the move leaves own king in check.
- """
- cell = self.cells[row][col]
- if not cell:
- return []
- piece, color = cell
- f = self._fwd(color)
- out = []
- if piece == WW: # King – all 8 neighbours
- for dr in (-1, 0, 1):
- for dc in (-1, 0, 1):
- if dr or dc:
- self._step(row, col, dr, dc, color, out)
- elif piece == GG: # Grey Whale: forward-slide + diag-backward-slide
- self._slide(row, col, f, 0, color, out)
- self._slide(row, col, -f, -1, color, out)
- self._slide(row, col, -f, 1, color, out)
- elif piece == PP: # Porpoise: step left or right
- self._step(row, col, 0, -1, color, out)
- self._step(row, col, 0, 1, color, out)
- elif piece == KK: # Killer Whale: orthogonal-slide + diagonal-step
- for dr, dc in ((1, 0), (-1, 0), (0, 1), (0, -1)):
- self._slide(row, col, dr, dc, color, out)
- for dr, dc in ((1, 1), (1, -1), (-1, 1), (-1, -1)):
- self._step(row, col, dr, dc, color, out)
- elif piece == NN: # Narwhal: jump 2f + step backward + step sideways
- r2, c2 = row + 2 * f, col
- if self._inbounds(r2, c2):
- tgt = self.cells[r2][c2]
- if tgt is None or tgt[1] != color:
- out.append((r2, c2)) # jump ignores intermediate square
- self._step(row, col, -f, 0, color, out)
- self._step(row, col, 0, -1, color, out)
- self._step(row, col, 0, 1, color, out)
- elif piece == HH: # Humpback: all 4 diagonals + backward
- for dr, dc in ((1, 1), (1, -1), (-1, 1), (-1, -1)):
- self._step(row, col, dr, dc, color, out)
- self._step(row, col, -f, 0, color, out)
- elif piece == BB: # Blue Whale: step f/b/diag-f
- self._step(row, col, f, 0, color, out)
- self._step(row, col, -f, 0, color, out)
- self._step(row, col, f, 1, color, out)
- self._step(row, col, f, -1, color, out)
- elif piece == DD: # Dolphin: step forward; on farthest rank slide diag-backward
- if row == self._far_row(color):
- self._slide(row, col, -f, -1, color, out)
- self._slide(row, col, -f, 1, color, out)
- else:
- self._step(row, col, f, 0, color, out)
- return out
- # ── Move generation ───────────────────────────────────────────────────────
- def _board_moves(self, color: int) -> list:
- moves = []
- for r in range(6):
- for c in range(6):
- cell = self.cells[r][c]
- if cell and cell[1] == color:
- for tr, tc in self.get_dests(r, c):
- moves.append(('mv', r, c, tr, tc))
- return moves
- def _drop_moves(self, color: int) -> list:
- moves = []
- far = self._far_row(color)
- pieces = list(set(self.hands[color]))
- for piece in pieces:
- for r in range(6):
- for c in range(6):
- if self.cells[r][c] is not None:
- continue
- # ── Dolphin restrictions ────────────────────────────────
- if piece == DD:
- if r == far:
- continue # not on farthest rank
- # ≤ 1 other friendly dolphin already in this file
- dolphins_in_file = sum(
- 1 for rr in range(6)
- if self.cells[rr][c] == (DD, color)
- )
- if dolphins_in_file >= 2:
- continue
- moves.append(('dr', piece, r, c))
- return moves
- def legal_moves(self) -> list:
- """All legal moves for the side to move."""
- color = self.to_move
- candidates = self._board_moves(color) + self._drop_moves(color)
- legal = []
- for mv in candidates:
- # ── Dolphin-drop-mate restriction ────────────────────────────────
- if mv[0] == 'dr' and mv[1] == DD:
- nb = self._apply_raw(mv)
- if self._is_mated_in(nb, 1 - color):
- continue
- # ── Must not leave own White Whale in check ──────────────────────
- nb = self._apply_raw(mv)
- kp = nb._king_pos(color)
- if kp and not nb._is_attacked(kp[0], kp[1], 1 - color):
- legal.append(mv)
- return legal
- # ── Apply a move ──────────────────────────────────────────────────────────
- def _apply_raw(self, mv) -> 'Board':
- """Apply move without legality checks; return new Board."""
- b = self.copy()
- if mv[0] == 'mv':
- _, fr, fc, tr, tc = mv
- piece, color = b.cells[fr][fc]
- tgt = b.cells[tr][tc]
- if tgt:
- cap = tgt[0]
- if cap == PP:
- cap = KK # Porpoise → Killer Whale in hand
- b.hands[color].append(cap)
- b.cells[fr][fc] = None
- b.cells[tr][tc] = (piece, color)
- else: # drop
- _, piece, tr, tc = mv
- color = b.to_move
- b.hands[color].remove(piece)
- b.cells[tr][tc] = (piece, color)
- b.to_move ^= 1
- return b
- apply = _apply_raw # public alias
- # ── Check / mate helpers ──────────────────────────────────────────────────
- def _king_pos(self, color: int):
- """(row, col) of colour's White Whale, or None."""
- for r in range(6):
- for c in range(6):
- cell = self.cells[r][c]
- if cell and cell[0] == WW and cell[1] == color:
- return r, c
- return None
- def _is_attacked(self, row: int, col: int, by_color: int) -> bool:
- """True if any piece of by_color can reach (row, col)."""
- for r in range(6):
- for c in range(6):
- cell = self.cells[r][c]
- if cell and cell[1] == by_color:
- if (row, col) in self.get_dests(r, c):
- return True
- return False
- def in_check(self, color: int) -> bool:
- kp = self._king_pos(color)
- return kp is None or self._is_attacked(kp[0], kp[1], 1 - color)
- @staticmethod
- def _is_mated_in(board: 'Board', color: int) -> bool:
- """True if 'color' is in check and has no legal escape on board."""
- if not board.in_check(color):
- return False
- old_tm = board.to_move
- board.to_move = color
- for mv in board._board_moves(color) + board._drop_moves(color):
- nb = board._apply_raw(mv)
- if not nb.in_check(color):
- board.to_move = old_tm
- return False
- board.to_move = old_tm
- return True
- def winner(self):
- """Colour that has won, or None."""
- if self._king_pos(BLACK) is None:
- return WHITE
- if self._king_pos(WHITE) is None:
- return BLACK
- return None
- # ── Evaluation ────────────────────────────────────────────────────────────
- def evaluate(self) -> float:
- """Signed material score: positive = Black is ahead."""
- score = 0.0
- for r in range(6):
- for c in range(6):
- cell = self.cells[r][c]
- if cell:
- v = VALUE[cell[0]]
- score += v if cell[1] == BLACK else -v
- for p in self.hands[BLACK]:
- score += VALUE[p] * 0.9
- for p in self.hands[WHITE]:
- score -= VALUE[p] * 0.9
- return score
- # ── Search (alpha-beta, depth 2) ─────────────────────────────────────────
- def best_move(self, depth: int = 2):
- """Return the best move found by alpha-beta search."""
- color = self.to_move
- moves = self.legal_moves()
- if not moves:
- return None
- random.shuffle(moves) # break ties / add variety
- sign = 1 if color == BLACK else -1
- best_score = -1_000_000
- best = moves[0]
- for mv in moves:
- nb = self._apply_raw(mv)
- w = nb.winner()
- if w == color:
- return mv # immediate winning capture
- score = sign * self._ab(nb, depth - 1, -1_000_000, 1_000_000)
- if score > best_score:
- best_score = score
- best = mv
- return best
- def _ab(self, board: 'Board', depth: int, alpha: float, beta: float) -> float:
- """Alpha-beta minimax, returns score from Black's perspective."""
- w = board.winner()
- if w == BLACK:
- return 10_000.0
- if w == WHITE:
- return -10_000.0
- if depth == 0:
- return board.evaluate()
- moves = board.legal_moves()
- if not moves:
- return board.evaluate()
- if board.to_move == BLACK:
- v = -1_000_000.0
- for mv in moves:
- v = max(v, self._ab(board._apply_raw(mv), depth - 1, alpha, beta))
- alpha = max(alpha, v)
- if beta <= alpha:
- break
- return v
- else:
- v = 1_000_000.0
- for mv in moves:
- v = min(v, self._ab(board._apply_raw(mv), depth - 1, alpha, beta))
- beta = min(beta, v)
- if beta <= alpha:
- break
- return v
- # ── Move encoding / decoding ──────────────────────────────────────────────
- def move_str(self, mv) -> str:
- """Internal move tuple → XBoard string (e.g. 'c1d2' or 'K@d4')."""
- if mv[0] == 'mv':
- _, fr, fc, tr, tc = mv
- return sq2str(fc, fr) + sq2str(tc, tr)
- else:
- _, piece, tr, tc = mv
- return P2C[piece] + '@' + sq2str(tc, tr)
- def parse_move(self, s: str):
- """XBoard string → internal move tuple, or None on failure."""
- s = s.strip()
- # Drop: P@d4 or P*d4
- m = re.match(r'^([WPHGNBDKwphgnbdk])[@*]([a-f][1-6])$', s)
- if m:
- piece = C2P[m.group(1).upper()]
- col, row = str2sq(m.group(2))
- return ('dr', piece, row, col)
- # Normal move: c1d2
- if (len(s) == 4
- and s[0].isalpha() and s[1].isdigit()
- and s[2].isalpha() and s[3].isdigit()):
- try:
- fc, fr = str2sq(s[0:2])
- tc, tr = str2sq(s[2:4])
- return ('mv', fr, fc, tr, tc)
- except (ValueError, IndexError):
- pass
- return None
- # ── Debug display ─────────────────────────────────────────────────────────
- def display(self) -> str:
- lines = [' a b c d e f']
- for r in range(5, -1, -1):
- row_str = f'{r+1} '
- for c in range(6):
- cell = self.cells[r][c]
- if cell is None:
- row_str += '. '
- else:
- ch = P2C[cell[0]]
- row_str += (ch.lower() + ' ') if cell[1] == WHITE else (ch + ' ')
- lines.append(row_str)
- lines.append(f'Black hand: {[P2C[p] for p in self.hands[BLACK]]}')
- lines.append(f'White hand: {[P2C[p] for p in self.hands[WHITE]]}')
- lines.append(f'Side to move: {"Black" if self.to_move == BLACK else "White"}')
- return '\n'.join(lines)
- # ── XBoard protocol engine loop ───────────────────────────────────────────────
- def main():
- board = Board()
- engine_color = WHITE # engine plays White after 'new'
- force_mode = True # do not auto-play until 'new' or 'go'
- move_history = [] # list of (board_before, move_str) for undo
- def out(msg: str):
- sys.stdout.write(msg + '\n')
- sys.stdout.flush()
- def engine_move():
- nonlocal board
- mv = board.best_move(depth=2)
- if mv is None:
- out('resign')
- return
- ms = board.move_str(mv)
- move_history.append((board.copy(), ms))
- board = board._apply_raw(mv)
- out('move ' + ms)
- w = board.winner()
- if w == BLACK:
- out('RESULT 0-1 {Black wins by capturing the White Whale}')
- elif w == WHITE:
- out('RESULT 1-0 {White wins by capturing the White Whale}')
- while True:
- try:
- raw = sys.stdin.readline()
- except (EOFError, KeyboardInterrupt):
- break
- if not raw:
- break
- line = raw.strip()
- tokens = line.split()
- if not tokens:
- continue
- cmd = tokens[0]
- # ── Protocol handshake ────────────────────────────────────────────────
- if cmd == 'xboard':
- pass
- elif cmd == 'protover':
- out('feature myname="Whale Shogi Engine"')
- out('feature variants="whale-shogi"')
- out('feature usermove=1')
- out('feature reuse=1')
- out('feature done=1')
- elif cmd in ('accepted', 'rejected'):
- pass
- # ── Game control ──────────────────────────────────────────────────────
- elif cmd == 'new':
- board = Board()
- engine_color = WHITE
- force_mode = False
- move_history.clear()
- elif cmd == 'variant':
- pass # accept any variant name; we only know whale-shogi
- elif cmd == 'force':
- force_mode = True
- elif cmd == 'go':
- force_mode = False
- engine_color = board.to_move
- engine_move()
- elif cmd == 'usermove':
- if len(tokens) >= 2:
- mv = board.parse_move(tokens[1])
- if mv is not None:
- move_history.append((board.copy(), tokens[1]))
- board = board._apply_raw(mv)
- if not force_mode and board.to_move == engine_color:
- engine_move()
- else:
- out(f'Error (illegal move): {tokens[1]}')
- # ── Undo ──────────────────────────────────────────────────────────────
- elif cmd == 'undo':
- if move_history:
- board, _ = move_history.pop()
- elif cmd == 'remove': # take back 2 moves
- for _ in range(2):
- if move_history:
- board, _ = move_history.pop()
- # ── Misc protocol commands ────────────────────────────────────────────
- elif cmd == 'ping':
- n = tokens[1] if len(tokens) > 1 else '0'
- out(f'pong {n}')
- elif cmd == 'result':
- pass # game over notification; nothing to do
- elif cmd == 'setboard':
- pass # not implemented; would need FEN parser for this variant
- elif cmd in ('hint', 'bk', 'draw', 'hard', 'easy', 'level', 'st',
- 'sd', 'nps', 'time', 'otim', 'edit', 'analyze', 'exit',
- 'computer', 'white', 'black', 'random', 'post', 'nopost',
- 'cores', 'memory', 'option'):
- pass # silently ignore time/analysis/config commands
- elif cmd == 'quit':
- break
- else:
- # Older XBoard protocol sends raw move strings without 'usermove'
- mv = board.parse_move(cmd)
- if mv is not None:
- move_history.append((board.copy(), cmd))
- board = board._apply_raw(mv)
- if not force_mode and board.to_move == engine_color:
- engine_move()
- # Unknown commands are silently ignored to keep the protocol clean
- sys.exit(0)
- if __name__ == '__main__':
- main()
Advertisement
Add Comment
Please, Sign In to add comment