如果你已经会写基础版的猜数字游戏,这篇文章将带你把它变成一个真正的软件——有类结构、有状态管理、有持久化存储。这是从"会写代码"到"会写工程"的关键一跃。
目录
1. 回顾:基础版的局限
我们先整理一下基础版猜数字的问题:
| 问题 | 说明 | 本版改进 |
|---|---|---|
| 全局变量散放 | secret、attempts 都是全局 | 封装到类属性 |
| 无时间压力 | 玩家可以无限思考 | 加入倒计时机制 |
| 数据丢失 | 关闭程序记录就没了 | 存到 JSON 文件 |
| 代码难以扩展 | 加减功能要改多处 | 模块化设计 |
| 单一场景 | 只有一种玩法 | 多难度 + 限时模式 |
2. 面向对象重构:Game 类设计
2.1 类的职责划分
from dataclasses import dataclass, field
from typing import List, Optional
import random
import time
@dataclass
class GameRecord:
"""单局游戏记录"""
player_name: str
difficulty: str
attempts_used: int
max_attempts: int
time_used: float
won: bool
timestamp: float = field(default_factory=time.time)
def to_dict(self) -> dict:
return {
"player": self.player_name,
"difficulty": self.difficulty,
"attempts": f"{self.attempts_used}/{self.max_attempts}",
"time": f"{self.time_used:.1f}s",
"result": "🏆 胜利" if self.won else "💀 失败",
"date": time.strftime("%Y-%m-%d %H:%M", time.localtime(self.timestamp)),
}
class GuessNumberGame:
"""猜数字游戏核心类"""
# 类常量:游戏配置
CONFIGS = {
"easy": {"range": (1, 50), "attempts": 10, "time_limit": 120},
"normal": {"range": (1, 100), "attempts": 7, "time_limit": 90},
"hard": {"range": (1, 200), "attempts": 5, "time_limit": 60},
"insane": {"range": (1, 500), "attempts": 3, "time_limit": 30},
}
def __init__(self, player_name: str = "匿名玩家"):
self.player_name = player_name
self.records: List[GameRecord] = []
self._current_game: Optional[dict] = None
def start_game(self, difficulty: str = "normal") -> "GameSession":
"""开始新游戏,返回一个游戏会话"""
if difficulty not in self.CONFIGS:
raise ValueError(f"未知难度: {difficulty}。可选: {list(self.CONFIGS.keys())}")
config = self.CONFIGS[difficulty]
return GameSession(
player=self.player_name,
difficulty=difficulty,
min_val=config["range"][0],
max_val=config["range"][1],
max_attempts=config["attempts"],
time_limit=config["time_limit"],
)
def add_record(self, session: "GameSession"):
"""添加一局记录"""
record = GameRecord(
player_name=self.player_name,
difficulty=session.difficulty,
attempts_used=session.attempts_used,
max_attempts=session.max_attempts,
time_used=session.time_used,
won=session.won,
)
self.records.append(record)
def get_stats(self) -> dict:
"""获取统计数据"""
if not self.records:
return {"message": "暂无记录"}
wins = sum(1 for r in self.records if r.won)
total = len(self.records)
avg_attempts = sum(r.attempts_used for r in self.records) / total
return {
"总局数": total,
"胜利": wins,
"胜率": f"{wins/total*100:.1f}%",
"平均尝试": f"{avg_attempts:.1f} 次",
}
class GameSession:
"""单局游戏会话"""
def __init__(self, player, difficulty, min_val, max_val,
max_attempts, time_limit):
self.player = player
self.difficulty = difficulty
self.min_val = min_val
self.max_val = max_val
self.max_attempts = max_attempts
self.time_limit = time_limit # 秒
self.secret = random.randint(min_val, max_val)
self.attempts_used = 0
self.guesses: List[int] = []
self.start_time: Optional[float] = None
self.end_time: Optional[float] = None
self.won = False
# 运行时状态
self._aborted = False
@property
def time_used(self) -> float:
if self.start_time is None:
return 0.0
end = self.end_time or time.time()
return end - self.start_time
@property
def time_remaining(self) -> float:
return max(0, self.time_limit - self.time_used)
@property
def is_time_up(self) -> bool:
return self.time_remaining <= 0
@property
def is_game_over(self) -> bool:
return self.won or self.attempts_used >= self.max_attempts or self.is_time_up
def make_guess(self, guess: int) -> str:
"""进行一次猜测,返回结果提示"""
if self.start_time is None:
self.start_time = time.time()
if self.is_game_over:
return "游戏已结束"
if guess < self.min_val or guess > self.max_val:
raise OutOfRangeError(f"数字必须在 {self.min_val} 到 {self.max_val} 之间")
self.attempts_used += 1
self.guesses.append(guess)
if guess == self.secret:
self.won = True
self.end_time = time.time()
return "correct"
elif guess < self.secret:
return "too_low"
else:
return "too_high"
def get_hint(self) -> str:
"""基于历史猜测给出智能提示"""
if not self.guesses:
return f"范围: {self.min_val} ~ {self.max_val}"
# 过滤出有效范围
low_bound = self.min_val
high_bound = self.max_val
for g in self.guesses:
if g < self.secret and g > low_bound:
low_bound = g
elif g > self.secret and g < high_bound:
high_bound = g
return f"提示: {low_bound} < 答案 < {high_bound}"
def __repr__(self) -> str:
return (f"GameSession(difficulty={self.difficulty}, "
f"attempts={self.attempts_used}/{self.max_attempts}, "
f"time={self.time_used:.1f}s/{self.time_limit}s, "
f"won={self.won})")
# 自定义异常
class OutOfRangeError(ValueError):
"""猜测超出范围"""
pass
class TimeExpiredError(RuntimeError):
"""时间到"""
pass
3. 倒计时机制:time 模块与信号处理
3.1 基础倒计时实现
import time
import sys
def countdown(seconds: int):
"""简单倒计时"""
for remaining in range(seconds, 0, -1):
sys.stdout.write(f"\r⏱ 剩余时间: {remaining:2d} 秒")
sys.stdout.flush()
time.sleep(1)
print("\n⏰ 时间到!")
3.2 非阻塞倒计时(用于猜数字)
在猜数字游戏中,我们需要玩家输入和倒计时同时进行,不能阻塞:
import time
import threading
class CountdownTimer:
"""倒计时器:在游戏进行时显示剩余时间"""
def __init__(self, total_seconds: int, callback=None):
self.total = total_seconds
self.remaining = total_seconds
self._running = False
self._callback = callback
self._thread: Optional[threading.Thread] = None
def start(self):
self._running = True
self.start_time = time.time()
self._thread = threading.Thread(target=self._tick)
self._thread.start()
def _tick(self):
while self._running and self.remaining > 0:
elapsed = time.time() - self.start_time
self.remaining = max(0, self.total - int(elapsed))
if self.remaining <= 5:
print(f"\r⚠️ 剩余 {self.remaining} 秒!", end="", flush=True)
time.sleep(0.5)
if self.remaining == 0 and self._callback:
self._callback()
def stop(self):
self._running = False
if self._thread:
self._thread.join(timeout=1)
@property
def is_expired(self) -> bool:
return self.remaining <= 0
⚠️ 在终端环境中,多线程倒计时与
input()同时运行会有显示冲突。生产环境更推荐使用curses库或 Web 界面。
4. 异常处理进阶:自定义异常
# 定义游戏的异常体系
class GameError(Exception):
"""游戏基础异常"""
pass
class InvalidInputError(GameError):
"""输入格式错误"""
def __init__(self, user_input: str):
self.user_input = user_input
super().__init__(f"'{user_input}' 不是有效的数字")
class OutOfRangeError(GameError):
"""数字超出范围"""
pass
class TimeExpiredError(GameError):
"""时间耗尽"""
pass
class GameAlreadyEndedError(GameError):
"""游戏已结束,无法继续操作"""
pass
# 使用示例
def safe_get_input(session: GameSession) -> int:
"""安全获取玩家输入"""
user_input = input("\n输入数字 (或 'hint' 获取提示, 'quit' 退出): ").strip()
if user_input.lower() == "quit":
raise GameAlreadyEndedError("玩家主动退出")
if user_input.lower() == "hint":
print(f"💡 {session.get_hint()}")
raise InvalidInputError("hint") # 不算有效猜测,但不算真正错误
try:
return int(user_input)
except ValueError:
raise InvalidInputError(user_input)
5. 数据持久化:JSON 文件存储
import json
from pathlib import Path
from typing import List
class RecordManager:
"""记录管理器:处理存档文件读写"""
def __init__(self, filepath: str = None):
if filepath is None:
filepath = Path.home() / ".guess_number_records.json"
self.filepath = Path(filepath)
def save(self, records: List[GameRecord]):
"""保存记录到文件"""
data = [r.to_dict() for r in records]
self.filepath.write_text(
json.dumps(data, ensure_ascii=False, indent=2),
encoding="utf-8"
)
def load(self) -> List[dict]:
"""从文件加载记录"""
if not self.filepath.exists():
return []
try:
text = self.filepath.read_text(encoding="utf-8")
return json.loads(text)
except json.JSONDecodeError:
return []
def export_stats(self) -> str:
"""导出统计摘要"""
records = self.load()
if not records:
return "暂无记录"
wins = sum(1 for r in records if "胜利" in r.get("result", ""))
lines = [
f"总游戏数: {len(records)}",
f"胜利次数: {wins}",
f"胜率: {wins/len(records)*100:.1f}%",
]
return "\n".join(lines)
6. 完整代码实现
#!/usr/bin/env python3
"""
增强版倒计时猜数字游戏
功能:面向对象设计、倒计时机制、记录持久化、多难度模式
"""
import random
import time
import json
from pathlib import Path
from dataclasses import dataclass, field
from typing import List, Optional
# ========== 异常定义 ==========
class GameError(Exception):
"""游戏基础异常"""
pass
class InvalidInputError(GameError):
def __init__(self, user_input: str, message: str = None):
self.user_input = user_input
msg = message or f"'{user_input}' 不是有效的整数"
super().__init__(msg)
# ========== 数据模型 ==========
@dataclass
class GameRecord:
player: str
difficulty: str
attempts_used: int
max_attempts: int
time_used: float
won: bool
timestamp: float = field(default_factory=time.time)
def to_dict(self) -> dict:
return {
"player": self.player,
"difficulty": self.difficulty,
"attempts": f"{self.attempts_used}/{self.max_attempts}",
"time": f"{self.time_used:.1f}s",
"result": "🏆 胜利" if self.won else "💀 失败",
"date": time.strftime("%Y-%m-%d %H:%M", time.localtime(self.timestamp)),
}
class GameSession:
"""单局游戏会话"""
def __init__(self, player: str, difficulty: str,
min_val: int, max_val: int,
max_attempts: int, time_limit: int):
self.player = player
self.difficulty = difficulty
self.min_val = min_val
self.max_val = max_val
self.max_attempts = max_attempts
self.time_limit = time_limit
self.secret = random.randint(min_val, max_val)
self.attempts_used = 0
self.guesses: List[int] = []
self.start_time: Optional[float] = None
self.end_time: Optional[float] = None
self.won = False
@property
def time_used(self) -> float:
end = self.end_time or time.time()
start = self.start_time or end
return end - start
@property
def time_remaining(self) -> float:
return max(0, self.time_limit - self.time_used)
@property
def is_game_over(self) -> bool:
return self.won or self.attempts_used >= self.max_attempts or self.time_remaining <= 0
def make_guess(self, guess: int) -> str:
if self.start_time is None:
self.start_time = time.time()
if self.is_game_over:
return "game_over"
if not (self.min_val <= guess <= self.max_val):
return "out_of_range"
self.attempts_used += 1
self.guesses.append(guess)
if guess == self.secret:
self.won = True
self.end_time = time.time()
return "correct"
elif guess < self.secret:
return "too_low"
else:
return "too_high"
def get_hint(self) -> str:
low = max(g for g in self.guesses + [self.min_val - 1] if g < self.secret) + 1
high = min(g for g in self.guesses + [self.max_val + 1] if g > self.secret) - 1
return f"答案在 {low} 和 {high} 之间"
class RecordManager:
"""记录持久化管理"""
FILEPATH = Path.home() / ".guess_number_records.json"
@classmethod
def save(cls, records: List[GameRecord]):
data = [r.to_dict() for r in records]
cls.FILEPATH.write_text(json.dumps(data, ensure_ascii=False, indent=2),
encoding="utf-8")
@classmethod
def load(cls) -> List[dict]:
if not cls.FILEPATH.exists():
return []
try:
return json.loads(cls.FILEPATH.read_text(encoding="utf-8"))
except (json.JSONDecodeError, IOError):
return []
class GameEngine:
"""游戏引擎"""
CONFIGS = {
"easy": {"range": (1, 50), "attempts": 10, "time": 120},
"normal": {"range": (1, 100), "attempts": 7, "time": 90},
"hard": {"range": (1, 200), "attempts": 5, "time": 60},
}
def __init__(self):
self.records: List[GameRecord] = []
self._load_records()
def _load_records(self):
loaded = RecordManager.load()
# 简化:只显示,不复建 GameRecord 对象
def select_difficulty(self) -> str:
print("\n选择难度:")
for key, cfg in self.CONFIGS.items():
print(f" [{key}] {cfg['range'][0]}~{cfg['range'][1]}, "
f"{cfg['attempts']}次机会, {cfg['time']}秒")
while True:
choice = input("输入 (easy/normal/hard): ").strip().lower()
if choice in self.CONFIGS:
return choice
print("无效选择,请重试")
def play(self):
difficulty = self.select_difficulty()
cfg = self.CONFIGS[difficulty]
player = input("\n你的昵称: ").strip() or "匿名"
session = GameSession(
player=player, difficulty=difficulty,
min_val=cfg["range"][0], max_val=cfg["range"][1],
max_attempts=cfg["attempts"], time_limit=cfg["time"]
)
print(f"\n{'='*50}")
print(f"🎮 难度: {difficulty} | 范围: {cfg['range']} | "
f"时限: {cfg['time']}秒")
print(f"{'='*50}")
while not session.is_game_over:
print(f"\n第 {session.attempts_used + 1}/{session.max_attempts} 次 | "
f"剩余时间: {session.time_remaining:.0f}秒")
user_input = input("输入数字 (hint/quit): ").strip().lower()
if user_input == "quit":
print("👋 已退出")
return
if user_input == "hint":
print(f"💡 {session.get_hint()}")
continue
try:
guess = int(user_input)
except ValueError:
print("⚠️ 请输入整数!")
continue
result = session.make_guess(guess)
if result == "out_of_range":
print(f"⚠️ 超出范围!请输入 {session.min_val}~{session.max_val}")
continue
if result == "correct":
print(f"\n🎉 恭喜 {player}!猜对了!")
print(f"🎯 答案就是 {session.secret}!")
print(f"🏆 用时: {session.time_used:.1f}s | "
f"尝试: {session.attempts_used} 次")
break
elif result == "too_low":
print("📈 太小了!")
elif result == "too_high":
print("📉 太大了!")
print(f"💡 提示: {session.get_hint()}")
if not session.won:
print(f"\n💀 游戏结束!正确答案是 {session.secret}")
print(f"⏱ 用时: {session.time_used:.1f}s")
# 保存记录
record = GameRecord(
player=player, difficulty=difficulty,
attempts_used=session.attempts_used,
max_attempts=session.max_attempts,
time_used=session.time_used, won=session.won
)
self.records.append(record)
RecordManager.save(self.records)
def show_stats(self):
records = RecordManager.load()
if not records:
print("\n暂无记录")
return
print(f"\n{'='*50}")
print("📊 历史记录")
print(f"{'='*50}")
for r in records[-10:]: # 最近 10 条
print(f" {r['date']} | {r['player']} | "
f"{r['difficulty']} | {r['attempts']} | "
f"{r['time']} | {r['result']}")
wins = sum(1 for r in records if "胜利" in r.get("result", ""))
print(f"\n总结: {len(records)} 局 | 胜 {wins} 局 | "
f"胜率 {wins/len(records)*100:.1f}%")
def run(self):
print("="*50)
print("🎮 增强版倒计时猜数字")
print("="*50)
while True:
print("\n1. 开始游戏")
print("2. 历史记录")
print("3. 退出")
choice = input("\n选择: ").strip()
if choice == "1":
self.play()
elif choice == "2":
self.show_stats()
elif choice == "3":
print("👋 再见!")
break
else:
print("无效选择")
if __name__ == "__main__":
GameEngine().run()
7. 代码走读:关键设计决策
7.1 为什么选择 dataclass?
# 传统写法需要手写 __init__、__repr__ 等
# dataclass 自动生成这些,减少样板代码
@dataclass
class GameRecord:
player: str
difficulty: str
attempts_used: int
# ... 自动获得 __init__, __repr__, __eq__
7.2 为什么用 classmethod 管理文件?
class RecordManager:
FILEPATH = Path.home() / ".guess_number_records.json"
@classmethod
def save(cls, records):
# 不依赖实例,直接通过类调用
# RecordManager.save(records)
这样 RecordManager 更像一个命名空间 + 工具集,不需要实例化。
7.3 属性(@property)的妙用
@property
def time_remaining(self) -> float:
return max(0, self.time_limit - self.time_used)
# 使用时像访问属性一样读取
print(session.time_remaining) # 自动计算,不需要 ()
8. 扩展方向
- 图形界面:用
tkinter或PyQt做 GUI 版 - Web 版本:用 Flask/FastAPI 做在线对战
- AI 对手:实现自动二分查找玩家
- 多人排行榜:数据库存储 + Web 接口
- 单元测试:用 pytest 测试 GameSession 逻辑
9. 学习检查清单
完成后检查你是否掌握了:
-
@dataclass的用途和使用场景 -
@classmethodvs@staticmethod -
@property装饰器的作用 - 类的封装思想(private
_attr) - 自定义异常的继承体系
- JSON 文件读写操作
-
time.time()计时原理 -
typing类型注解的基本用法
延伸阅读
- Python 经典项目实战:从零实现猜数字游戏 —— 基础版实现
- Python 内置数据结构完全指南 —— list、dict 的深入理解
- Python 类型系统与 Pydantic V2 —— 类型注解进阶
- Python 测试与质量工程 —— 为游戏写单元测试
🎯 核心收获:通过这个项目,你体验到了"写脚本"和"写软件"的区别。类封装了状态,方法封装了行为,文件持久化让数据跨越进程生命周期——这才是工程化编程的起点。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。