Skip to content

Schi1zoid/go-game-python

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

6 Commits
 
 
 
 
 
 
 
 

Repository files navigation

go-game-python

""" Простейшая консольная реализация игры ГО на Python. Поддерживает:

  • Размер доски (по умолчанию 9x9, можно указать 13 или 19)
  • Ходы двух игроков (черные 'B' и белые 'W')
  • Правила захвата (группы, свободы)
  • Простая защита от кохэ (ko) — запрещает вернуться к предыдущему положению доски
  • Pass и resign
  • Подсчёт очков: камни + территория (флуд-филл пустых областей, если область окружена только одним цветом, то она присуждается этому цвету)

Ограничения / упрощения:

  • Нет поддержки коми (можно легко добавить)
  • Нет AI — игра для двух людей в консоли
  • Ko-правило реализовано только как сравнение с последним состоянием (не длинная история)

Запуск: python go_game.py """

from copy import deepcopy

class GoGame: EMPTY = '.' BLACK = 'B' WHITE = 'W'

def __init__(self, size=9):
    assert size in (9, 13, 19) or (size >= 5 and size <= 25), "Рекомендуемые размеры: 9, 13, 19"
    self.size = size
    self.board = [[self.EMPTY for _ in range(size)] for _ in range(size)]
    self.to_move = self.BLACK
    self.captured = {self.BLACK: 0, self.WHITE: 0}
    self.previous_board = None  # для простого ko
    self.passes = 0

def copy_board(self):
    return tuple(''.join(row) for row in self.board)

def display(self):
    # Печать координат
    header = '   ' + ' '.join(chr(ord('A') + i + (1 if i >= 8 else 0)) for i in range(self.size))
    # (Пропускаем букву 'I' чтобы удобнее читать, принятная в Go нотация)
    print(header)
    for r in range(self.size):
        row_num = self.size - r
        print(f"{row_num:2d}", ' '.join(self.board[r]), f"{row_num:2d}")
    print(header)
    print(f"Следующий ход: {'Чёрные' if self.to_move==self.BLACK else 'Белые'}")
    print(f"Захвачено: Чёрные={self.captured[self.BLACK]}, Белые={self.captured[self.WHITE]}")

def in_bounds(self, r, c):
    return 0 <= r < self.size and 0 <= c < self.size

def neighbors(self, r, c):
    for dr, dc in ((1,0),(-1,0),(0,1),(0,-1)):
        nr, nc = r+dr, c+dc
        if self.in_bounds(nr, nc):
            yield nr, nc

def get_group(self, r, c, visited=None):
    if visited is None:
        visited = set()
    color = self.board[r][c]
    stack = [(r,c)]
    group = set()
    while stack:
        x,y = stack.pop()
        if (x,y) in group:
            continue
        group.add((x,y))
        for nx, ny in self.neighbors(x,y):
            if self.board[nx][ny] == color and (nx,ny) not in group:
                stack.append((nx,ny))
    return group

def liberties(self, group):
    libs = set()
    for (r,c) in group:
        for nr,nc in self.neighbors(r,c):
            if self.board[nr][nc] == self.EMPTY:
                libs.add((nr,nc))
    return libs

def remove_group(self, group):
    if not group:
        return 0
    color = self.board[next(iter(group))[0]][next(iter(group))[1]]
    for r,c in group:
        self.board[r][c] = self.EMPTY
    return len(group)

def is_valid_move(self, r, c, color):
    if not self.in_bounds(r,c):
        return False, 'Выход за границы доски'
    if self.board[r][c] != self.EMPTY:
        return False, 'Клетка не пуста'
    # Создадим временную доску чтобы проверить самопат
    tmp = deepcopy(self.board)
    tmp[r][c] = color
    # удаляем вражеские группы без свобод
    enemy = self.BLACK if color == self.WHITE else self.WHITE
    to_remove = []
    for nr,nc in self.neighbors(r,c):
        if tmp[nr][nc] == enemy:
            grp = self._get_group_on_board(tmp, nr, nc)
            if not self._liberties_on_board(tmp, grp):
                to_remove.append(grp)
    for grp in to_remove:
        for x,y in grp:
            tmp[x][y] = self.EMPTY
    # теперь проверим свободы своей группы
    grp_self = self._get_group_on_board(tmp, r, c)
    if not self._liberties_on_board(tmp, grp_self):
        return False, 'Ход самопроигрышный (suicide)'
    # ko: не позволяем вернуться в предыдущее состояние
    tmp_repr = tuple(''.join(row) for row in tmp)
    if self.previous_board is not None and tmp_repr == self.previous_board:
        return False, 'Запрещён ход из-за ko (повтор предыдущей позиции)'
    return True, ''

def _get_group_on_board(self, board, r, c):
    color = board[r][c]
    stack = [(r,c)]
    group = set()
    while stack:
        x,y = stack.pop()
        if (x,y) in group:
            continue
        group.add((x,y))
        for nx, ny in ((x+1,y),(x-1,y),(x,y+1),(x,y-1)):
            if 0 <= nx < self.size and 0 <= ny < self.size and board[nx][ny] == color:
                stack.append((nx,ny))
    return group

def _liberties_on_board(self, board, group):
    for (r,c) in group:
        for nr,nc in ((r+1,c),(r-1,c),(r,c+1),(r,c-1)):
            if 0 <= nr < self.size and 0 <= nc < self.size and board[nr][nc] == self.EMPTY:
                return True
    return False

def play_move(self, r, c):
    color = self.to_move
    valid, reason = self.is_valid_move(r,c,color)
    if not valid:
        return False, reason
    # Сохраняем доску для ko-проверки
    self.previous_board = self.copy_board()
    # ставим камень
    self.board[r][c] = color
    # удаляем вражеские группы без свобод
    enemy = self.BLACK if color == self.WHITE else self.WHITE
    total_captured = 0
    for nr,nc in list(self.neighbors(r,c)):
        if self.board[nr][nc] == enemy:
            grp = self.get_group(nr,nc)
            if not self.liberties(grp):
                removed = self.remove_group(grp)
                total_captured += removed
    self.captured[color] += total_captured
    # переключаем очередь
    self.to_move = enemy
    self.passes = 0
    return True, ''

def pass_move(self):
    self.previous_board = self.copy_board()
    self.passes += 1
    self.to_move = self.BLACK if self.to_move == self.WHITE else self.WHITE

def resign(self):
    winner = self.WHITE if self.to_move == self.BLACK else self.BLACK
    return winner

def score(self):
    # Простое подсчитывание: камни + территория
    stones = {self.BLACK:0, self.WHITE:0}
    for r in range(self.size):
        for c in range(self.size):
            if self.board[r][c] == self.BLACK:
                stones[self.BLACK] += 1
            elif self.board[r][c] == self.WHITE:
                stones[self.WHITE] += 1
    territory = {self.BLACK:0, self.WHITE:0}
    visited = set()
    for r in range(self.size):
        for c in range(self.size):
            if self.board[r][c] != self.EMPTY or (r,c) in visited:
                continue
            # flood fill пустой региона
            queue = [(r,c)]
            region = set()
            bordering = set()
            while queue:
                x,y = queue.pop()
                if (x,y) in region:
                    continue
                region.add((x,y))
                for nx,ny in self.neighbors(x,y):
                    if self.board[nx][ny] == self.EMPTY and (nx,ny) not in region:
                        queue.append((nx,ny))
                    elif self.board[nx][ny] in (self.BLACK, self.WHITE):
                        bordering.add(self.board[nx][ny])
            visited |= region
            if len(bordering) == 1:
                owner = next(iter(bordering))
                territory[owner] += len(region)
    final = {self.BLACK: stones[self.BLACK]+territory[self.BLACK],
             self.WHITE: stones[self.WHITE]+territory[self.WHITE]}
    return {
        'stones': stones,
        'territory': territory,
        'final': final,
    }

def parse_coord(s, size): # Ожидается: буква+число, буква латинская без I (I пропущена) s = s.strip().upper() if s == 'PASS': return 'PASS' if s == 'RESIGN': return 'RESIGN' # отделим буквы и цифры import re m = re.match(r"^([A-Z]+)([0-9]+)$", s) if not m: return None col_s, row_s = m.groups() # Обрабатываем колонку: учитываем пропуск 'I' col = 0 for ch in col_s: if ch == 'I': return None if ch > 'Z' or ch < 'A': return None val = ord(ch) - ord('A') if ch > 'I': val -= 1 col = col*26 + val try: row = int(row_s) except: return None # convert to 0-indexed matrix coords r = size - row c = col if r < 0 or r >= size or c < 0 or c >= size: return None return r, c

def human_play_loop(size=9): game = GoGame(size=size) print(f"Запуск игры GO {size}x{size}. Ходы: введите координату (например D4), PASS, или RESIGN.") while True: game.display() if game.passes >= 2: print('Оба игрока пасовали подряд — игра закончена. Подсчёт очков.') break player = 'Чёрные' if game.to_move==game.BLACK else 'Белые' s = input(f"{player} ({game.to_move}) ход: ").strip() parsed = parse_coord(s, game.size) if parsed is None: print('Неверный ввод, попробуйте ещё раз. Формат: A1, D4, PASS, RESIGN') continue if parsed == 'PASS': game.pass_move() continue if parsed == 'RESIGN': winner = game.resign() print(f"{winner} победил (сдача).") return r,c = parsed ok, reason = game.play_move(r,c) if not ok: print('Невалидный ход:', reason) continue result = game.score() print('Камни:', result['stones']) print('Территория:', result['territory']) print('Итог:', result['final']) if result['final'][game.BLACK] > result['final'][game.WHITE]: print('Победили Чёрные') elif result['final'][game.WHITE] > result['final'][game.BLACK]: print('Победили Белые') else: print('Ничья')

if name == 'main': try: s = input('Размер доски (enter для 9, или 9/13/19): ').strip() size = int(s) if s else 9 except Exception: size = 9 human_play_loop(size)

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages