是用 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