diff --git a/chess_open_world.py b/chess_open_world.py new file mode 100644 index 0000000..d81b32e --- /dev/null +++ b/chess_open_world.py @@ -0,0 +1,648 @@ +import pygame +import sys +import random + +pygame.init() + +# --- ПАРАМЕТРЫ ПОЛЯ --- +CELL_SIZE = 32 +GRID_WIDTH = 30 +GRID_HEIGHT = 30 +WINDOW_WIDTH = CELL_SIZE * GRID_WIDTH +WINDOW_HEIGHT = CELL_SIZE * GRID_HEIGHT + +# --- ЦВЕТА --- +COLOR_BG = (30, 30, 30) +COLOR_GRID = (50, 50, 50) +COLOR_FOG = (20, 20, 20) +COLOR_SELECTED = (255, 0, 0) +COLOR_MOVE = (0, 255, 0) + +COLOR_WHITE_PIECE = (255, 255, 255) # Заливка белых фигур +COLOR_BLACK_PIECE = (0, 0, 0) # Заливка чёрных фигур + +# Новые цвета для бонусов +COLOR_REGEN = (0, 255, 0) # Зелёный +COLOR_HP_UPGRADE = (0, 128, 255) # Синий +COLOR_DAMAGE = (255, 0, 0) # Красный +COLOR_KING_HP_UPGRADE = (255, 215, 0) # Золотой +COLOR_ADD_PIECE = (255, 255, 255) # Белый (с плюсом) +COLOR_ADD_PIECE_PINK = (255, 192, 203) # Розовый + +# Цвет текущего хода +COLOR_TURN_TEXT = (255, 255, 255) # Белый + +# Новый цвет для пометки клеток, которые скоро уйдут в туман войны +COLOR_PRE_FOG = (169, 169, 169) # Светло-серый + +screen = pygame.display.set_mode((WINDOW_WIDTH, WINDOW_HEIGHT)) +pygame.display.set_caption("Шахматы с открытым миром 1.0") + +# Шрифты +font = pygame.font.SysFont(None, CELL_SIZE // 2) +victory_font = pygame.font.SysFont(None, 60) +turn_font = pygame.font.SysFont(None, 30) + +# --- Глобальное множество открытых клеток (туман войны) --- +global_revealed = set() + +# --- Словарь символов --- +piece_symbols = { + 'rook': 'Л', + 'knight': 'К', + 'bishop': 'С', + 'queen': 'Ф', + 'king': 'К', # Король (можно заменить на 'K' или другой символ) + 'pawn': 'П' +} + +# === МЕХАНИКА БОНУСОВ === +# Храним в глобальном словаре bonus_cells: {(x,y): {'type': ...}} +bonus_cells = {} + +# Вероятности появления различных типов бонусов +bonus_probabilities = { + 'regen': 0.42, # 42% + 'hp_upgrade': 0.32, # 32% + 'damage': 0.2, # 20% + 'king_hp_upgrade': 0.05, # 5% + 'add_piece': 0.01 # 1% +} + +# Функция для выбора бонуса на основе вероятностей +def choose_bonus_type(): + rand = random.random() + cumulative = 0 + for bonus_type, prob in bonus_probabilities.items(): + cumulative += prob + if rand < cumulative: + return bonus_type + return 'regen' # По умолчанию + +def initialize_bonus_cells(pieces, num_each=10): + """ + Создаём случайно бонусных клеток. + Включает три типа изначально: 'hp_upgrade', 'regen', 'king_hp_upgrade'. + Бонусы не размещаются под стартовыми позициями фигур. + """ + global bonus_cells + all_positions = [(x, y) for x in range(GRID_WIDTH) for y in range(GRID_HEIGHT)] + random.shuffle(all_positions) + i_pos = 0 + types = ['hp_upgrade', 'regen', 'king_hp_upgrade'] + for t in types: + for _ in range(num_each): + while i_pos < len(all_positions): + (xx, yy) = all_positions[i_pos] + i_pos += 1 + # Проверка, чтобы клетка не занята фигурой + if not any(p.x == xx and p.y == yy for p in pieces): + bonus_cells[(xx, yy)] = {'type': t} + break + +def apply_bonus(piece, bonus_type, pieces): + """Применяем эффект бонуса к фигуре piece в зависимости от типа.""" + if bonus_type == 'regen': + # Регенерация (1..2) для НЕ короля + if piece.name != 'king': + amt = random.randint(1, 2) + piece.hp = min(piece.hp + amt, piece.max_hp) + + elif bonus_type == 'hp_upgrade': + # Повышение max_hp только НЕ королю без восстановления HP + if piece.name != 'king': + piece.max_hp += 1 + # piece.hp остается неизменным + + elif bonus_type == 'king_hp_upgrade': + # Только для короля, бонус активируется другим игроком + if piece.name != 'king': + # Найти короля той же команды + king = next((p for p in pieces if p.name == 'king' and p.color == piece.color), None) + if king: + if king.hp == king.max_hp: + # Если король на полном здоровье, + # то увеличиваем max_hp на 1 + king.max_hp += 1 + king.hp = king.max_hp + else: + # Если король не на полном, то + # просто восстанавливаем HP до max + king.hp = min(king.hp + 1, king.max_hp) + + elif bonus_type == 'damage': + # Нанесение урона (0..5) + damage = random.randint(0, 5) + piece.hp = max(piece.hp - damage, 0) + if piece.hp == 0: + # Удаляем фигуру, если HP равен 0 + pieces.remove(piece) + if piece.name == 'king': + global winner, game_over + winner = 'black' if piece.color == 'white' else 'white' + game_over = True + + elif bonus_type == 'add_piece': + # Добавление новой фигуры рядом, если возможно и не король + if piece.name != 'king': + # Определяем возможные соседние клетки + directions = [(-1,0),(1,0),(0,-1),(0,1), (-1,-1),(1,-1),(1,1),(-1,1)] + random.shuffle(directions) + for dx, dy in directions: + nx = piece.x + dx + ny = piece.y + dy + if 0 <= nx < GRID_WIDTH and 0 <= ny < GRID_HEIGHT: + # Проверяем, что клетка пуста + if not any(p.x == nx and p.y == ny for p in pieces) and (nx, ny) not in bonus_cells: + # Создаём новую фигуру того же типа + new_piece = Piece(piece.name, piece.color, nx, ny) + pieces.append(new_piece) + break + # Если не удалось разместить, бонус не активируется + +# === МЕХАНИКА БОЯ === +def resolve_combat(attacker, defender): + """ + Атакующая фигура (A) идёт на клетку защитника (B). + HP(A)=a, HP(B)=b. + - Если a < b: B теряет a (b := b-a), A погибает. + - Если a > b: A теряет b (a := a-b), B погибает. + - Если a == b: A побеждает, убивает B, а у A остаётся 1 HP. + Возвращаем кортеж: (attacker_alive, defender_alive) + при этом изменяем HP в самих объектах. + """ + a = attacker.hp + b = defender.hp + + if a < b: + # Защитник теряет a HP + defender.hp = b - a + # Атакующая погибает + attacker.hp = 0 + return (False, True) + + elif a > b: + # Атакующая теряет b HP + attacker.hp = a - b + # Защитник погибает + defender.hp = 0 + return (True, False) + + else: + # a == b + # Атакующая побеждает, остаётся с 1 HP + attacker.hp = 1 + defender.hp = 0 + return (True, False) + +# === КЛАСС ФИГУРЫ === +class Piece: + def __init__(self, name, color, x, y): + self.name = name + self.color = color # 'white' или 'black' + self.x = x + self.y = y + self.selected = False + + # Инициализация HP + if self.name == 'king': + self.max_hp = 5 + self.hp = 5 + elif self.name in ['rook', 'knight', 'bishop', 'queen']: + self.max_hp = 3 + self.hp = 3 + elif self.name == 'pawn': + self.max_hp = 1 + self.hp = 1 + + def get_possible_moves(self, pieces): + moves = [] + if self.name == 'pawn': + directions = [ + (-1,-1), (0,-1), (1,-1), + (-1, 0), (1, 0), + (-1, 1), (0, 1), (1, 1) + ] + for dx, dy in directions: + nx = self.x + dx + ny = self.y + dy + if 0 <= nx < GRID_WIDTH and 0 <= ny < GRID_HEIGHT: + blocking = next((p for p in pieces if p.x == nx and p.y == ny), None) + # Можем ходить, если клетка пуста или там враг + if not blocking or blocking.color != self.color: + moves.append((nx, ny)) + + elif self.name in ['rook', 'bishop', 'queen']: + # Ограничиваем движение до 5 клеток + if self.name == 'rook': + directions = [(-1,0),(1,0),(0,-1),(0,1)] + elif self.name == 'bishop': + directions = [(-1,-1),(1,-1),(1,1),(-1,1)] + elif self.name == 'queen': + directions = [ + (-1,0),(1,0),(0,-1),(0,1), + (-1,-1),(1,-1),(1,1),(-1,1) + ] + for dx, dy in directions: + for step in range(1, 6): # До 5 клеток + nx = self.x + dx * step + ny = self.y + dy * step + if 0 <= nx < GRID_WIDTH and 0 <= ny < GRID_HEIGHT: + blocking = next((p for p in pieces if p.x == nx and p.y == ny), None) + if blocking: + if blocking.color != self.color: + moves.append((nx, ny)) + break + moves.append((nx, ny)) + else: + break + + elif self.name == 'knight': + deltas = [(-2,-1),(-1,-2),(1,-2),(2,-1), + (2,1),(1,2),(-1,2),(-2,1)] + for dx, dy in deltas: + nx, ny = self.x+dx, self.y+dy + if 0 <= nx < GRID_WIDTH and 0 <= ny < GRID_HEIGHT: + blocking = next((p for p in pieces if p.x == nx and p.y == ny), None) + if not blocking or blocking.color != self.color: + moves.append((nx, ny)) + + elif self.name == 'king': + directions = [ + (-1,0),(1,0),(0,-1),(0,1), + (-1,-1),(1,-1),(1,1),(-1,1) + ] + for dx, dy in directions: + nx, ny = self.x+dx, self.y+dy + if 0 <= nx < GRID_WIDTH and 0 <= ny < GRID_HEIGHT: + blocking = next((p for p in pieces if p.x == nx and p.y == ny), None) + if not blocking or blocking.color != self.color: + moves.append((nx, ny)) + + return moves + +# --- Инициализация фигур --- +def initialize_pieces(): + """Чёрные: y=2(back), y=3(pawns). Белые: y=27(back), y=26(pawns).""" + pieces = [] + cx = GRID_WIDTH//2 + + # ЧЁРНЫЕ + black_back = ['rook','knight','bishop','king','queen','bishop','knight','rook'] + for i,pname in enumerate(black_back): + x = cx - 3 + i + y = 2 + pieces.append(Piece(pname,'black',x,y)) + for i in range(8): + x = cx-3 + i + y = 3 + pieces.append(Piece('pawn','black',x,y)) + + # БЕЛЫЕ + white_back = ['rook','knight','bishop','king','queen','bishop','knight','rook'] + for i,pname in enumerate(white_back): + x = cx - 3 + i + y = 27 + pieces.append(Piece(pname,'white',x,y)) + for i in range(8): + x = cx-3 + i + y = 26 + pieces.append(Piece('pawn','white',x,y)) + + return pieces + +# Инициализация фигур до инициализации бонусов +pieces = initialize_pieces() +initialize_bonus_cells(pieces, num_each=10) + +selected_piece = None +possible_moves = [] +game_over = False +winner = None + +# --- Туман войны --- +cell_counters = {} # {(x,y): turns_since_last_revealed} +BONUS_GENERATION_CHANCE = 0.1 # 10% шанс появления бонуса при повторном открытии + +# --- Система ходов --- +current_turn = 'white' # Начинаем с белых + +def update_fog(): + """ + Обновляет туман войны: + - Открытые клетки остаются открытыми. + - Закрытые клетки, которые не находятся рядом с любыми фигурами на расстоянии 2 клеток, + становятся закрытыми через 5 ходов. + - При повторном открытии клетки шанс на появление нового бонуса. + """ + global global_revealed, bonus_cells + + # Сохраняем предыдущее состояние открытых клеток + previous_revealed = global_revealed.copy() + + # Определяем все открытые клетки на основе позиций фигур + new_revealed = set() + for p in pieces: + for dx in range(-2, 3): + for dy in range(-2, 3): + nx = p.x + dx + ny = p.y + dy + if 0 <= nx < GRID_WIDTH and 0 <= ny < GRID_HEIGHT: + new_revealed.add((nx, ny)) + + # Определяем только новые открытые клетки + newly_revealed = new_revealed - previous_revealed + + # Обрабатываем все клетки + for y in range(GRID_HEIGHT): + for x in range(GRID_WIDTH): + pos = (x, y) + if pos in new_revealed: + # Если клетка видна, сбрасываем счетчик + cell_counters[pos] = 0 + global_revealed.add(pos) + else: + # Если клетка не видна, увеличиваем счетчик + if pos in cell_counters: + cell_counters[pos] += 1 + else: + cell_counters[pos] = 1 + + # Если счетчик достигает 5, закрываем клетку + if cell_counters[pos] >= 5: + if pos in global_revealed: + global_revealed.remove(pos) + # Удаляем бонус, если клетка закрывается и бонус не является 'damage' + if pos in bonus_cells and bonus_cells[pos]['type'] != 'damage': + del bonus_cells[pos] + + # Генерируем бонусы только на новых открытых клетках + for pos in newly_revealed: + if pos not in bonus_cells and random.random() < BONUS_GENERATION_CHANCE: + bonus_type = choose_bonus_type() + # Проверяем, чтобы бонус не был под фигурой + if not any(p.x == pos[0] and p.y == pos[1] for p in pieces): + bonus_cells[pos] = {'type': bonus_type} + +def draw_grid(): + for x in range(0, WINDOW_WIDTH, CELL_SIZE): + pygame.draw.line(screen, COLOR_GRID, (x, 0), (x, WINDOW_HEIGHT)) + for y in range(0, WINDOW_HEIGHT, CELL_SIZE): + pygame.draw.line(screen, COLOR_GRID, (0, y), (WINDOW_WIDTH, y)) + +def draw_fog(): + for y in range(GRID_HEIGHT): + for x in range(GRID_WIDTH): + pos = (x, y) + if pos not in global_revealed: + rect = pygame.Rect(x*CELL_SIZE, y*CELL_SIZE, CELL_SIZE, CELL_SIZE) + pygame.draw.rect(screen, COLOR_FOG, rect) + else: + # Проверяем, будет ли клетка закрыта в следующем ходу + if cell_counters.get(pos, 0) == 4: + # Помечаем клетку светло-серым + rect = pygame.Rect(x*CELL_SIZE, y*CELL_SIZE, CELL_SIZE, CELL_SIZE) + pygame.draw.rect(screen, COLOR_PRE_FOG, rect) + +def draw_bonus_cells(): + """ + Отрисовка бонусов (если клетка открыта или бонус типа 'damage'). + Цвета: + - 'regen': (0,255,0) зелёный + - 'hp_upgrade': (0,128,255) синий + - 'damage': (255,0,0) красный + - 'king_hp_upgrade': (255,215,0) золотой + - 'add_piece': (255,255,255) белый с плюсом или розовый + """ + for (bx, by), info in bonus_cells.items(): + # Для 'damage' бонусов отображаем всегда, иначе только если клетка видна + if info['type'] != 'damage' and (bx, by) not in global_revealed: + continue + cx = bx*CELL_SIZE + CELL_SIZE//2 + cy = by*CELL_SIZE + CELL_SIZE//2 + bonus_type = info['type'] + if bonus_type == 'regen': + color = COLOR_REGEN + pygame.draw.circle(screen, color, (cx, cy), CELL_SIZE//4) + elif bonus_type == 'hp_upgrade': + color = COLOR_HP_UPGRADE + pygame.draw.circle(screen, color, (cx, cy), CELL_SIZE//4) + elif bonus_type == 'damage': + color = COLOR_DAMAGE + pygame.draw.circle(screen, color, (cx, cy), CELL_SIZE//4) + elif bonus_type == 'king_hp_upgrade': + color = COLOR_KING_HP_UPGRADE + pygame.draw.circle(screen, color, (cx, cy), CELL_SIZE//4) + elif bonus_type == 'add_piece': + # Проверяем, возможно ли добавить фигуру рядом + possible = False + for dx, dy in [(-1,0),(1,0),(0,-1),(0,1), (-1,-1),(1,-1),(1,1),(-1,1)]: + nx = bx + dx + ny = by + dy + if 0 <= nx < GRID_WIDTH and 0 <= ny < GRID_HEIGHT: + if not any(p.x == nx and p.y == ny for p in pieces): + possible = True + break + color = COLOR_ADD_PIECE if possible else COLOR_ADD_PIECE_PINK + pygame.draw.circle(screen, color, (cx, cy), CELL_SIZE//4) + # Рисуем плюс + pygame.draw.line(screen, (0,0,0), (cx - CELL_SIZE//8, cy), (cx + CELL_SIZE//8, cy), 2) + pygame.draw.line(screen, (0,0,0), (cx, cy - CELL_SIZE//8), (cx, cy + CELL_SIZE//8), 2) + +def draw_pieces(pieces): + """Рисуем фигуры, их HP и подсвечиваем ходы.""" + for p in pieces: + rect = pygame.Rect(p.x*CELL_SIZE, p.y*CELL_SIZE, CELL_SIZE, CELL_SIZE) + + if p.color == 'white': + pygame.draw.rect(screen, COLOR_WHITE_PIECE, rect) + symbol = piece_symbols.get(p.name,'?') + text = font.render(symbol, True, (255,0,0)) + text_rect = text.get_rect(center=rect.center) + screen.blit(text, text_rect) + # HP чёрным + hp_str = f"{p.hp}/{p.max_hp}" + hp_text = font.render(hp_str, True, (0,0,0)) + screen.blit(hp_text, (rect.x+2, rect.y+2)) + else: + pygame.draw.rect(screen, COLOR_BLACK_PIECE, rect) + symbol = piece_symbols.get(p.name,'?') + text = font.render(symbol, True, (255,0,0)) + text_rect = text.get_rect(center=rect.center) + screen.blit(text, text_rect) + # HP белым + hp_str = f"{p.hp}/{p.max_hp}" + hp_text = font.render(hp_str, True, (255,255,255)) + screen.blit(hp_text, (rect.x+2, rect.y+2)) + + if p.selected: + pygame.draw.rect(screen, COLOR_SELECTED, rect, 2) + + for (mx, my) in possible_moves: + r = pygame.Rect(mx*CELL_SIZE, my*CELL_SIZE, CELL_SIZE, CELL_SIZE) + pygame.draw.rect(screen, COLOR_MOVE, r, 2) + +def display_victory(winner): + text = victory_font.render(f"{winner.capitalize()} победил!", True, (255, 255, 255)) + text_rect = text.get_rect(center=(WINDOW_WIDTH//2, WINDOW_HEIGHT//2)) + screen.blit(text, text_rect) + +def display_turn(current_turn): + text = turn_font.render(f"Текущий ход: {current_turn.capitalize()}", True, COLOR_TURN_TEXT) + screen.blit(text, (10, WINDOW_HEIGHT - 30)) + +running = True +clock = pygame.time.Clock() + +# Инициализируем туман войны в начале игры +update_fog() + +while running: + clock.tick(30) + for event in pygame.event.get(): + if event.type == pygame.QUIT: + running = False + + elif event.type == pygame.MOUSEBUTTONDOWN and not game_over: + mx, my = pygame.mouse.get_pos() + gx = mx // CELL_SIZE + gy = my // CELL_SIZE + + clicked_piece = next((p for p in pieces if p.x == gx and p.y == gy), None) + + if selected_piece: + if (gx, gy) in possible_moves: + # Проверяем бонус + if (gx, gy) in bonus_cells: + bonus_type = bonus_cells[(gx, gy)]['type'] + apply_bonus(selected_piece, bonus_type, pieces) + del bonus_cells[(gx, gy)] + + # Проверяем, есть ли там вражеская фигура + defender = next((p for p in pieces if p.x == gx and p.y == gy), None) + if defender: + # Проводим бой + attacker = selected_piece + attacker_alive, defender_alive = resolve_combat(attacker, defender) + if not defender_alive: + # Защитник погиб + if defender.name == 'king': + winner = attacker.color + game_over = True + pieces.remove(defender) + if not attacker_alive: + # Атакующая погибла + pieces.remove(attacker) + # Снимаем выделение + selected_piece = None + possible_moves.clear() + # После изменения позиций фигур обновляем туман войны + update_fog() + # Смена хода + current_turn = 'black' if current_turn == 'white' else 'white' + continue + else: + # Атакующая жива, перемещаем её + # Проверка прохождения через клетки (для длинных ходов) + path = [] + if attacker.name in ['rook', 'bishop', 'queen']: + dx = gx - attacker.x + dy = gy - attacker.y + if dx != 0: + dx = dx // abs(dx) + if dy != 0: + dy = dy // abs(dy) + for step in range(1, max(abs(gx - attacker.x), abs(gy - attacker.y))): + path_x = attacker.x + dx * step + path_y = attacker.y + dy * step + path.append((path_x, path_y)) + # Применяем урон при прохождении через клетки и удаляем 'damage' клетки + survived = True + for pos in path: + if pos in bonus_cells and bonus_cells[pos]['type'] == 'damage': + damage = random.randint(0,5) + attacker.hp = max(attacker.hp - damage, 0) + # Удаляем 'damage' клетку как использованную + del bonus_cells[pos] + if attacker.hp == 0: + pieces.remove(attacker) + selected_piece = None + possible_moves.clear() + survived = False + break + if survived: + attacker.x = gx + attacker.y = gy + attacker.selected = False + selected_piece = None + possible_moves.clear() + # Смена хода + current_turn = 'black' if current_turn == 'white' else 'white' + + else: + # Пустая клетка — просто ходим + # Проверка прохождения через клетки (для длинных ходов) + path = [] + if selected_piece.name in ['rook', 'bishop', 'queen']: + dx = gx - selected_piece.x + dy = gy - selected_piece.y + if dx != 0: + dx = dx // abs(dx) + if dy != 0: + dy = dy // abs(dy) + for step in range(1, max(abs(gx - selected_piece.x), abs(gy - selected_piece.y))): + path_x = selected_piece.x + dx * step + path_y = selected_piece.y + dy * step + path.append((path_x, path_y)) + # Применяем урон при прохождении через клетки и удаляем 'damage' клетки + survived = True + for pos in path: + if pos in bonus_cells and bonus_cells[pos]['type'] == 'damage': + damage = random.randint(0,5) + selected_piece.hp = max(selected_piece.hp - damage, 0) + # Удаляем 'damage' клетку как использованную + del bonus_cells[pos] + if selected_piece.hp == 0: + pieces.remove(selected_piece) + selected_piece = None + possible_moves.clear() + survived = False + break + if survived: + selected_piece.x = gx + selected_piece.y = gy + selected_piece.selected = False + selected_piece = None + possible_moves.clear() + # Смена хода + current_turn = 'black' if current_turn == 'white' else 'white' + + # После успешного хода обновляем туман войны + update_fog() + else: + # Клик вне возможных ходов + selected_piece.selected = False + selected_piece = None + possible_moves.clear() + + else: + # Выбираем фигуру только если клетка видима и соответствует текущему ходу + if clicked_piece and (gx, gy) in global_revealed and clicked_piece.color == current_turn: + selected_piece = clicked_piece + selected_piece.selected = True + possible_moves = selected_piece.get_possible_moves(pieces) + + # Отрисовка + screen.fill(COLOR_BG) + draw_grid() + draw_fog() + draw_bonus_cells() + draw_pieces(pieces) + if game_over and winner: + display_victory(winner) + else: + display_turn(current_turn) + pygame.display.flip() + +pygame.quit() +sys.exit()