[游戏] - pygame 开发 小时候经常玩的 象棋的暗棋游戏

查看 85|回复 10
作者:pyjiujiu   
[color=]* 说明:
是用 AI 开发的,主体迭代了四五次,但修 bug 花费不少时间(非报错型的,如游戏机制理解错误)
网上随便找的素材,处理部分音频 也花了点时间,大概合起来两天时间
还是非常原始,之前没有用 pygame 写过游戏,这次主要是 想借助 AI 挑战下(望各位游戏高手海涵)
- 涉及的 AI 模型:
主要是 gemini-2.5-pro , 辅助是 gemini-2.5-flash, qwen-32B( qwen-3 今天刚发布,没来得及用)
[color=]* 游戏截图:


截图2.JPG (53.75 KB, 下载次数: 0)
下载附件
2025-4-29 11:55 上传



截图1.JPG (54.37 KB, 下载次数: 0)
下载附件
2025-4-29 11:56 上传

---分割线---
[color=]* 文件说明:
需要下载下面的素材(放在压缩包中),
- images 是棋子的图片 (有一张棋盘图(默认不用),去掉注释,设置 board.jpg 可用)
- music   bgm 加上 两个小音效


文件夹截图.JPG (6.57 KB, 下载次数: 0)
下载附件
文件夹截图
2025-4-29 11:57 上传

另外 images 中 还有个小工具(LLM 顺手写的),可以用来生成自定义字体的棋子图(windows 可用)
源码不放,截图如下:


棋子生成器.JPG (19.54 KB, 下载次数: 0)
下载附件
棋子生成
2025-4-29 12:02 上传

---分割线---
下面是源代码:
[Python] 纯文本查看 复制代码
import pygame
import sys
import random
import os
# --- Pygame Setup ---
pygame.init() # Initialize Pygame early
pygame.mixer.init() # Initialize the mixer for sound
pygame.font.init() # Initialize font system
# --- Constants ---
ROWS, COLS = 4, 8
SQUARE_SIZE = 65
BOARD_WIDTH = COLS * SQUARE_SIZE
BOARD_HEIGHT = ROWS * SQUARE_SIZE
INFO_HEIGHT = 50
# Increased height significantly for captured pieces (2 rows per color comfortably)
CAPTURED_AREA_HEIGHT = SQUARE_SIZE * 2 + 40 # Adjusted height (e.g., 2 rows + padding)
CAPTURED_PIECE_SIZE = SQUARE_SIZE * 0.5 # Smaller size for captured pieces
BUTTON_AREA_HEIGHT = 50
PADDING = 10
# Adjusted Screen Height
SCREEN_WIDTH = BOARD_WIDTH
SCREEN_HEIGHT = INFO_HEIGHT + BOARD_HEIGHT + CAPTURED_AREA_HEIGHT + BUTTON_AREA_HEIGHT + PADDING * 4
# Colors
WHITE = (255, 255, 255)
BLACK = (0, 0, 0)
GRAY = (180, 180, 180)
DARK_GRAY = (100, 100, 100)
RED = (200, 0, 0)
BLUE = (0, 0, 200)
HIGHLIGHT_COLOR = (100, 255, 100, 150)
POSSIBLE_MOVE_COLOR = (255, 255, 0, 100)
# Piece Info (棋子信息)
PIECE_LEVELS = {
    'general': 7, 'advisor': 6, 'elephant': 5,
    'chariot': 4, 'horse': 3, 'cannon': 2, 'soldier': 1
}
# Assets 资产
IMAGE_FOLDER = './images/'
SOUND_FOLDER = './music/'
DEFAULT_BACK_IMAGE_FILENAME = None
CUSTOM_BACK_IMAGE_FILENAME = "piece_back_custom.png"
BOARD_IMAGE_FILENAME = "board.jpg" #棋盘可换成自定义背景
# --- Fonts ---
try:
    INFO_FONT = pygame.font.SysFont('simhei', 30)
    BUTTON_FONT = pygame.font.SysFont('simhei', 20)
    CAPTURED_FONT = pygame.font.SysFont('simhei', 16)
except pygame.error:
    print("Warning: 'simhei' font not found. Using default font.")
    INFO_FONT = pygame.font.Font(None, 40)
    BUTTON_FONT = pygame.font.Font(None, 25)
    CAPTURED_FONT = pygame.font.Font(None, 20)
screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT))
pygame.display.set_caption("暗棋 (Anqi / Dark Chess)") #游戏标题
# --- Load Images ---
piece_images = {}
try:
    for color in ['red', 'black']:
        piece_images[color] = {}
        for piece_type in PIECE_LEVELS.keys():
            filename = f"{color}_{piece_type}.png"
            path = os.path.join(IMAGE_FOLDER, filename)
            img = pygame.image.load(path).convert_alpha()
            piece_images[color][piece_type] = pygame.transform.scale(img, (SQUARE_SIZE, SQUARE_SIZE))
except pygame.error as e:
    print(f"Error loading piece image: {path}. {e}")
    print(f"Please ensure all piece images are in the '{IMAGE_FOLDER}' folder.")
    sys.exit()
except FileNotFoundError:
     print(f"Error: Image file not found at {path}.")
     print(f"Please ensure all piece images are in the '{IMAGE_FOLDER}' folder.")
     sys.exit()
# Load piece back image
piece_back_image = None
back_image_path_custom = os.path.join(IMAGE_FOLDER, CUSTOM_BACK_IMAGE_FILENAME) if CUSTOM_BACK_IMAGE_FILENAME else None
back_image_path_default = os.path.join(IMAGE_FOLDER, DEFAULT_BACK_IMAGE_FILENAME) if DEFAULT_BACK_IMAGE_FILENAME else None
if back_image_path_custom and os.path.exists(back_image_path_custom):
    try:
        img = pygame.image.load(back_image_path_custom).convert_alpha()
        piece_back_image = pygame.transform.scale(img, (SQUARE_SIZE, SQUARE_SIZE))
        print(f"Loaded custom back image: {CUSTOM_BACK_IMAGE_FILENAME}")
    except pygame.error as e:
        print(f"Warning: Error loading custom back image: {back_image_path_custom}. {e}")
        piece_back_image = None
elif back_image_path_default and os.path.exists(back_image_path_default):
     try:
        img = pygame.image.load(back_image_path_default).convert_alpha()
        piece_back_image = pygame.transform.scale(img, (SQUARE_SIZE, SQUARE_SIZE))
        print(f"Loaded default back image: {DEFAULT_BACK_IMAGE_FILENAME}")
     except pygame.error as e:
        print(f"Warning: Error loading default back image: {back_image_path_default}. {e}")
        piece_back_image = None
if piece_back_image is None:
    print("Using default blue color for piece back.")
    piece_back_image = pygame.Surface((SQUARE_SIZE, SQUARE_SIZE))
    piece_back_image.fill(BLUE)
    pygame.draw.rect(piece_back_image, WHITE, piece_back_image.get_rect(), 3)
# Load board image
board_image = None
board_image_path = os.path.join(IMAGE_FOLDER, BOARD_IMAGE_FILENAME)
if os.path.exists(board_image_path):
    try:
        img = pygame.image.load(board_image_path).convert()
        board_image = pygame.transform.scale(img, (BOARD_WIDTH, BOARD_HEIGHT))
        print(f"Loaded board image: {BOARD_IMAGE_FILENAME}")
    except pygame.error as e:
        print(f"Error loading board image: {board_image_path}. {e}")
        board_image = None
else:
    print(f"Warning: Board image '{BOARD_IMAGE_FILENAME}' not found in '{IMAGE_FOLDER}'. Will draw colored squares.")
# --- Load Sounds ---
try:
    click_sound = pygame.mixer.Sound(os.path.join(SOUND_FOLDER, "choose.mp3"))  #选择 或 翻起
    move_sound = pygame.mixer.Sound(os.path.join(SOUND_FOLDER, "move_short_1.mp3"))   #吃子
    # Load BGM
    bgm_path = os.path.join(SOUND_FOLDER, "bgm.mp3")
    pygame.mixer.music.load(bgm_path)
    pygame.mixer.music.set_volume(0.3)
    pygame.mixer.music.play(-1)
    print("Loaded sound effects and BGM.")
except pygame.error as e:
    print(f"Warning: Could not load sound file(s). {e}")
    print("Make sure 'click.wav', 'move.wav', and 'bgm.mp3' are in the correct folder.")
    click_sound = None
    move_sound = None
except FileNotFoundError as e:
    print(f"Warning: Sound file not found. {e}")
    print("Make sure 'click.wav', 'move.wav', and 'bgm.mp3' are in the correct folder.")
    click_sound = None
    move_sound = None
# Game States
STATE_PLAYING = 2
STATE_GAME_OVER = 3
class Piece:
    def __init__(self, color, piece_type, row, col):
        self.color = color
        self.piece_type = piece_type
        self.level = PIECE_LEVELS[piece_type]
        self.image = piece_images[color][piece_type]
        self.back_image = piece_back_image
        self.is_face_up = False
        self.row = row
        self.col = col
        self.rect = pygame.Rect(col * SQUARE_SIZE, INFO_HEIGHT + PADDING + row * SQUARE_SIZE, SQUARE_SIZE, SQUARE_SIZE)
    def draw(self, surface):
        if self.is_face_up:
            surface.blit(self.image, self.rect.topleft)
        else:
            surface.blit(self.back_image, self.rect.topleft)
    def move(self, row, col):
        self.row = row
        self.col = col
        self.rect.topleft = (col * SQUARE_SIZE, INFO_HEIGHT + PADDING + row * SQUARE_SIZE)
    def __repr__(self):
        return f"{self.color} {self.piece_type} ({self.row},{self.col}) {'Up' if self.is_face_up else 'Down'}"
class Button:
    def __init__(self, x, y, width, height, text, action):
        self.rect = pygame.Rect(x, y, width, height)
        self.text = text
        self.action = action
        self.color = GRAY
        self.hover_color = DARK_GRAY
        self.is_hovered = False
    def draw(self, surface):
        color = self.hover_color if self.is_hovered else self.color
        pygame.draw.rect(surface, color, self.rect)
        pygame.draw.rect(surface, BLACK, self.rect, 2)
        text_surf = BUTTON_FONT.render(self.text, True, BLACK)
        text_rect = text_surf.get_rect(center=self.rect.center)
        surface.blit(text_surf, text_rect)
    def handle_event(self, event):
        if event.type == pygame.MOUSEMOTION:
            self.is_hovered = self.rect.collidepoint(event.pos)
        if event.type == pygame.MOUSEBUTTONDOWN and event.button == 1:
            if self.is_hovered:
                self.play_sound(click_sound)
                return self.action
        return None
    def play_sound(self, sound):
        if sound:
            sound.play()
class Game:
    def __init__(self):
        self.board = [[None for _ in range(COLS)] for _ in range(ROWS)]
        self.all_pieces = [] # Keep track of all pieces currently on the board
        self.captured_pieces = {'red': [], 'black': []}
        self.turn = None
        self.player_color = {'player1': None, 'ai': None}
        self.selected_piece = None
        self.possible_moves = []
        self.game_state = STATE_PLAYING
        self.winner = None
        self.game_over_reason = "" # Store why the game ended
        self.vs_ai = False
        self.ai_thinking = False
        self.ai_delay = 500 # ms
        self.buttons = []
        self.setup_buttons()
        self.setup_board()
    def setup_buttons(self):
        self.buttons = []
        button_y = INFO_HEIGHT + PADDING + BOARD_HEIGHT + PADDING + CAPTURED_AREA_HEIGHT + PADDING
        button_width = 120
        button_height = BUTTON_AREA_HEIGHT - PADDING
        spacing = 20
        restart_button = Button(SCREEN_WIDTH / 2 - button_width - spacing / 2, button_y, button_width, button_height, "重新开始", "restart")
        ai_button_text = "电脑对战 (AI)" if not self.vs_ai else "双人对战 (2P)"
        ai_button = Button(SCREEN_WIDTH / 2 + spacing / 2, button_y, button_width + 20, button_height, ai_button_text, "toggle_ai")
        self.buttons.append(restart_button)
        self.buttons.append(ai_button)
    def setup_board(self):
        self.board = [[None for _ in range(COLS)] for _ in range(ROWS)]
        self.all_pieces = []
        self.captured_pieces = {'red': [], 'black': []}
        self.turn = None
        self.player_color = {'player1': None, 'ai': None}
        self.selected_piece = None
        self.possible_moves = []
        self.game_state = STATE_PLAYING
        self.winner = None
        self.game_over_reason = ""
        self.ai_thinking = False
        pieces_to_place = []
        piece_counts = {'general': 1, 'advisor': 2, 'elephant': 2, 'horse': 2, 'chariot': 2, 'cannon': 2, 'soldier': 5}
        for color in ['red', 'black']:
            for piece_type, count in piece_counts.items():
                for _ in range(count):
                    pieces_to_place.append((color, piece_type))
        random.shuffle(pieces_to_place)
        idx = 0
        for r in range(ROWS):
            for c in range(COLS):
                if idx  bool:
        if not attacker or not defender or not attacker.is_face_up or not defender.is_face_up:
             return False
        if attacker.color == defender.color:
             return False
        if attacker.piece_type == 'cannon':
            return False
        if attacker.piece_type == 'soldier' and defender.piece_type == 'general':
            return True
        if attacker.piece_type == 'general' and defender.piece_type == 'soldier':
            return False
        return attacker.level >= defender.level
    def get_valid_moves(self, piece: Piece) -> list:
        """Gets valid move/capture coordinates for a face-up piece."""
        if not piece or not piece.is_face_up:
            return []
        moves = []
        r, c = piece.row, piece.col
        directions = [(0, 1), (0, -1), (1, 0), (-1, 0)]
        if piece.piece_type != 'cannon':
            for dr, dc in directions:
                nr, nc = r + dr, c + dc
                if 0  bool:
        """Checks if the player of the given color has any possible action (move, capture, or flip)."""
        for piece in self.all_pieces:
            if piece.color == color:
                if not piece.is_face_up:
                    return True
                elif self.get_valid_moves(piece):
                    return True
        return False
    def handle_click(self, pos):
        for button in self.buttons:
             event = pygame.event.Event(pygame.MOUSEBUTTONDOWN, {'button': 1, 'pos': pos})
             action = button.handle_event(event)
             if action:
                 if action == "restart":
                     print("Restarting game...")
                     self.setup_board()
                     self.setup_buttons()
                     return
                 elif action == "toggle_ai":
                     self.vs_ai = not self.vs_ai
                     print(f"AI Mode: {'On' if self.vs_ai else 'Off'}")
                     self.setup_board()
                     self.setup_buttons()
                     return
        if self.game_state == STATE_GAME_OVER:
            return
        if self.vs_ai and self.turn == self.player_color['ai'] and self.ai_thinking:
            print("AI is thinking, please wait.")
            return
        board_y_start = INFO_HEIGHT + PADDING
        if not (0  bool:
        """Checks if the game has ended based on global state. Sets winner and state."""
        if self.game_state == STATE_GAME_OVER:
            return True
        red_pieces = [p for p in self.all_pieces if p.color == 'red']
        black_pieces = [p for p in self.all_pieces if p.color == 'black']
        if not red_pieces:
            self.winner = 'black'
            self.game_state = STATE_GAME_OVER
            self.game_over_reason = "红方无子 (Red has no pieces)"
            print(f"Game Over: {self.game_over_reason}. Winner: Black")
            return True
        if not black_pieces:
            self.winner = 'red'
            self.game_state = STATE_GAME_OVER
            self.game_over_reason = "黑方无子 (Black has no pieces)"
            print(f"Game Over: {self.game_over_reason}. Winner: Red")
            return True
        if self.turn:
            current_player_can_move = self.has_any_valid_action(self.turn)
            if not current_player_can_move:
                self.winner = 'black' if self.turn == 'red' else 'red'
                self.game_state = STATE_GAME_OVER
                opponent_color_cn = "黑方" if self.winner == 'black' else "红方"
                current_color_cn = "红方" if self.turn == 'red' else "黑方"
                self.game_over_reason = f"{current_color_cn})"
                print(f"Game Over: {self.game_over_reason}. Winner: {self.winner}")
                return True
        return False
   
    def ai_move(self):
        if self.game_state != STATE_PLAYING or self.turn != self.player_color['ai'] or self.ai_thinking:
            return
        self.ai_thinking = True
        print(f"AI ({self.player_color['ai']}) is thinking...")
        self.draw(screen) # Update screen to show "thinking" message
        pygame.display.flip()
        pygame.time.wait(self.ai_delay // 2) # Short wait before calculation
        # --- AI Logic V2: Evaluate all actions together ---
        ai_color = self.player_color['ai']
        possible_actions = [] # List of tuples: (score, action_type, piece, target_coords/piece_to_flip)
        # 1. Evaluate all possible actions
        for piece in self.all_pieces:
            # A. Evaluate Flips  
            if not piece.is_face_up:  
                 # Score for flipping: relatively low, maybe higher if few pieces left?
                 flip_score = 5
                 # Add potential bonus if few pieces are face up? (根据场上翻开的棋子数量加分)
                 face_up_count = sum(1 for p in self.all_pieces if p.is_face_up)
                 if face_up_count  {target} (Score: {score})")
        # --- Execute Action ---
        pygame.time.wait(self.ai_delay // 2) # Remainder of delay
        if action_type == 'capture':
            target_r, target_c = target
            defender = self.board[target_r][target_c]
            if defender:
                self.capture_piece(piece, defender)
                self.move_piece(piece, target_r, target_c)
                self.play_sound(move_sound)
            else:
                 print("AI Error: Capture target square is empty?")
                 self.move_piece(piece, target_r, target_c) # Move anyway
                 self.play_sound(move_sound)
        elif action_type == 'move':
            target_r, target_c = target
            self.move_piece(piece, target_r, target_c)
            self.play_sound(move_sound)
        elif action_type == 'flip':
            # Target is the piece to flip
            piece_to_flip = target
            if piece_to_flip and not piece_to_flip.is_face_up:
                piece_to_flip.is_face_up = True
                print(f"AI flipped {piece_to_flip.color} {piece_to_flip.piece_type}")
                self.play_sound(click_sound) # Use click sound for flip
            else:
                print(f"AI Error: Tried to flip an invalid piece? {piece_to_flip}")
        # Action finished
        self.ai_thinking = False
        self.selected_piece = None # Clear human selection state
        self.possible_moves = []
        
        if self.check_game_over():
             print(f"Game Over detected after AI move by {self.winner}.")
        self.switch_turn() #change self.turn
def main():
    running = True
    clock = pygame.time.Clock()
    game = Game()
    while running:
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                running = False
            if event.type == pygame.MOUSEBUTTONDOWN and event.button == 1:
                if game.game_state != STATE_GAME_OVER:
                    if not (game.vs_ai and game.turn == game.player_color['ai'] and game.ai_thinking):
                        game.handle_click(event.pos)
                    else:
                        print("Ignoring click during AI thinking.")
                elif game.game_state == STATE_GAME_OVER:
                    game.handle_click(event.pos)
            if event.type == pygame.MOUSEMOTION:
                 for button in game.buttons:
                     button.handle_event(event)
        if game.game_state == STATE_PLAYING and game.vs_ai and game.turn is not None and game.turn == game.player_color['ai'] and not game.ai_thinking:
             if not game.check_game_over():
                 game.ai_move()
             else:
                  print("Skipping AI move because game is already over.")
        game.draw(screen)
        pygame.display.flip()
        clock.tick(30)
    pygame.mixer.music.stop()
    pygame.quit()
    sys.exit()
if __name__ == '__main__':
    if not os.path.isdir(IMAGE_FOLDER):
        print(f"Error: Image folder '{IMAGE_FOLDER}' not found.")
        print("Please create the folder and place piece and board images inside.")
        sys.exit()
    if not os.path.isdir(SOUND_FOLDER):
         print(f"Warning: Sound folder '{SOUND_FOLDER}' not found. Sounds may not play.")
    main()
---分割---
最后:
- 玩法就不作介绍,相信大家都玩过,主要是适合双人对战
- 里面的 AI 电脑对战,是非常“愚蠢型”的,不过可以让玩家熟悉玩法 (其实 也稍微探索下 有搜索深度的,不过遇到了重复行动的问题,非认真设计不可)
代码层面来说,pygame 是第一次写,写过一次后,才发现这个库比较底层,很多东西需要自己写框架,比游戏引擎难很多(还好 暗棋本身不是很复杂)
用AI 开发游戏的体会:
1 要自己先写好游戏规则(在迭代过程中 就遇到自己规则没加上,规则之间有矛盾的情况,很浪费时间),
2 对比一般软件,游戏里面 状态管理是非常复杂,往往嵌套在一起,容易会晕,建议遇到这种情况,不要蛮横(脑子会过热),可设计个 prompt,让LLM梳理,或者画状态树
游戏素材:

发布--.7z
(902.18 KB, 下载次数: 49)
2025-4-29 12:15 上传
点击文件名下载附件
游戏素材
下载积分: 吾爱币 -1 CB

棋子, 游戏

pyjiujiu
OP
  


yxf515321 发表于 2025-4-29 22:26
感觉象棋的规则写起来好麻烦呐

说的没错,这个规则确实不容易写,细节比较多,描述反复
不过 ,现在用 AI 大模型,也是很方便,直接让它起草一个,然后自己修改 即可
(因为 AI 对象棋非常之了解,不需要什么 prompt技巧,简单问就行 )
如果你要设计的游戏比较偏僻,或者完全自己新发明的,那就要想点办法,给大模型 喂点材料(图片,视频等都可)
我LLM 直接生成的,类似下面这样:
"

jiedao747   

以前很喜欢玩。入坑作品,我记得。那个里面有更多象棋走法在里面的。比如士可以斜着走。马可以日子走法。当时第一次遇到的时候很惊喜。后来才发现这规则在暗棋里面相当另类。
luodeman   

玩玩试试,感谢分享,辛苦了
wanibo   

围观学习,感谢楼主分享。
cagu   

一直玩军旗的暗棋,看看这个怎么样,多谢分享了
jackal   

玩玩试试,感谢分享
yxf515321   

感觉象棋的规则写起来好麻烦呐
shenwq   

玩一下试试,非常感谢分享,楼主辛苦了。
wangMaple   

感谢楼主分享,辛苦了,正好在学python,学习一下
您需要登录后才可以回帖 登录 | 立即注册

返回顶部