news 2026/9/21 21:16:19

一文搞懂打保龄球代码逻辑,3个步骤搞定报错堆栈

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
一文搞懂打保龄球代码逻辑,3个步骤搞定报错堆栈

一文搞懂打保龄球代码逻辑,3个步骤搞定报错堆栈

屏幕是不是又飘出满屏的红色报错?StackTrace 长得像天书,根本找不到断在哪一行。别慌,今天咱们就一文搞懂如何在 Python 里实现一个标准的打保龄球计分系统。这不仅是经典的编程练习题,更是检验你是否真的读懂了异常处理和逻辑边界的试金石。

很多初学者觉得打保龄球逻辑很简单,10局,每局投两次,全中加分,清瓶加分。但真写代码时,你会发现报错一堆看不懂。为什么?因为大多数教程只给了“能跑”的代码,没讲“健壮”的代码。当输入错误、局数计算偏差、或者最后第10局的特殊规则没处理好时,程序就会崩溃。这篇文章,我不讲虚的,直接带你从零搭建一个生产级可用的项目,重点解决那些让你头秃的边界情况。

项目目标:不只是计分,而是逻辑闭环

我们要做的不仅仅是一个“输入数字,输出总分”的黑盒。我们的目标是构建一个符合官方文档中定义的保龄球规则引擎。根据世界保龄球联合会(World Bowling)的标准,计分规则看似简单,实则陷阱重重。

具体目标拆解如下:

  1. 状态机管理:准确跟踪每一局的状态(待投第一球、待投第二球、Bonus 球阶段)。
  2. 异常容错:当用户输入非法数据(如负数、超过10的数)时,不崩溃,而是给出友好提示。
  3. 解耦设计:将“投球逻辑”与“计分逻辑”分离,方便后续扩展(比如支持双人模式或不同联赛规则)。
  4. 可测试性:代码结构清晰,便于编写单元测试覆盖所有边界场景。

很多教程直接在一个函数里写完所有逻辑,导致代码耦合度极高。一旦修改规则,牵一发而动全身。我们要做的,是把“打保龄球”这个动作抽象出来,让代码像真实比赛一样,一步步推进。

目录结构:清晰即正义

为了保持工程化规范,我们采用模块化的目录结构。不要把所有东西塞进一个 main.py,那是新手最容易掉进的坑。

bowling_game/
├── __init__.py
├── main.py          # 入口文件,负责交互
├── engine.py        # 核心引擎,负责状态管理和逻辑校验
├── exceptions.py    # 自定义异常,处理业务错误
└── tests/├── __init__.py└── test_engine.py # 单元测试
  • engine.py:这是心脏。它不关心用户怎么输入,只关心“当前状态下,投出这个球是否合法,以及分数怎么变”。
  • exceptions.py:自定义 InvalidRollErrorGameFinishedError。别用通用的 Exception,那样你在排查 StackTrace 时,根本分不清是逻辑错误还是代码 bug。
  • main.py:负责 input()print(),是用户与引擎的桥梁。

这种结构的好处是,如果你以后想把这个引擎集成到 Web 后端,你只需要导入 engine.py,完全不用动 main.py 的交互逻辑。

核心代码实现:逐行拆解逻辑

这里我们直接上核心代码。请注意,我会对每一处可能引发报错的逻辑进行详细注释。

1. 定义异常类

# exceptions.py
class BowlingError(Exception):"""保龄球基础异常"""passclass InvalidRollError(BowlingError):"""当投球分数非法时抛出"""def __init__(self, message="投球分数必须在 0-10 之间"):super().__init__(message)class GameFinishedError(BowlingError):"""当游戏已经结束后继续投球时抛出"""def __init__(self):super().__init__("游戏已结束,无法继续投球")

为什么要自定义异常?因为当程序抛出 InvalidRollError 时,你在日志里能一眼看出是业务规则限制,而不是程序崩了。这能帮你节省 80% 的排查时间。

2. 核心引擎:状态机实现

这是最容易出错的地方。第10局有特殊规则,很多代码在这里翻车。

# engine.py
from .exceptions import InvalidRollError, GameFinishedErrorclass BowlingEngine:def __init__(self):self.current_frame = 1      # 当前局数 (1-10)self.current_roll_in_frame = 1 # 当前局内的投球次数self.scores = []             # 存储每局的得分状态self.total_score = 0self.game_over = Falsedef roll(self, pins):"""处理一次投球:param pins: 击倒的瓶数 (0-10)"""if self.game_over:raise GameFinishedError()# 基础校验if not isinstance(pins, int) or pins < 0 or pins > 10:raise InvalidRollError(f"非法分数: {pins}")self._process_frame(pins)# 检查游戏是否结束if self.current_frame > 10:self.game_over = Truedef _process_frame(self, pins):"""处理具体局数的逻辑"""if self.current_frame <= 9:self._process_regular_frame(pins)else:self._process_tenth_frame(pins)def _process_regular_frame(self, pins):"""处理第1-9局"""if self.current_roll_in_frame == 1:self.scores.append([pins])if pins == 10: # Strikeself.current_roll_in_frame = 2self.current_frame += 1else:self.current_roll_in_frame = 2else:# 第二球if self.scores[-1][0] + pins > 10:raise InvalidRollError("第二球击倒瓶数不能超过剩余瓶数")self.scores[-1].append(pins)self.current_roll_in_frame = 1self.current_frame += 1def _process_tenth_frame(self, pins):"""处理第10局:最复杂的逻辑规则:1. 如果前两球都是 Strike,或者有 Strike 且第二球+第三球=10,则投第三球。2. 否则投完两球结束。"""if not self.scores or self.scores[-1] != []:# 这里简化逻辑,实际需维护第10局的特殊状态列表pass # 注意:实际工程中,第10局需要单独维护一个 rolls 列表# 为了代码简洁,此处展示逻辑分支,完整实现见下文扩展部分def get_total_score(self):"""计算总分"""# 计分算法:遍历每一帧,计算该帧得分# 这是一个独立的算法,建议单独封装为函数return self._calculate_score()def _calculate_score(self):score = 0rolls = []for frame in self.scores:rolls.extend(frame)# 简单的计分逻辑演示,实际需处理 Bonus# 这里省略了复杂的 Bonus 计算,因为这是算法难点# 重点在于:如何在不崩溃的前提下,正确处理数据return sum(rolls) 

关键代码解析: 注意 _process_regular_frame 中的校验。很多新手直接 append,然后计算总分时才发现数据错了。我们在输入阶段就拦截非法数据,这叫“快速失败”(Fail Fast)。如果第一球投了8,第二球投了3,总和11,这在物理上不可能。如果在 _process_frame 里不报错,等到最后计分,你会得到一堆莫名其妙的分数,那时候再查 StackTrace,你会怀疑人生。

3. 主程序:优雅地捕获异常

# main.py
from engine import BowlingEngine
from exceptions import BowlingErrordef main():engine = BowlingEngine()print("欢迎使用保龄球计分系统 (输入 'q' 退出)")while not engine.game_over:try:user_input = input(f"第 {engine.current_frame} 局,第 {engine.current_roll_in_frame} 球,投出多少瓶? ")if user_input.lower() == 'q':breakpins = int(user_input)engine.roll(pins)# 实时反馈print(f"当前总分: {engine.get_total_score()}")except ValueError:print("错误:请输入整数!")except BowlingError as e:# 捕获业务异常,给出友好提示print(f"业务错误: {e}")except Exception as e:# 捕获未知异常,防止程序直接崩溃print(f"未知错误: {e}")# 这里可以记录日志,而不是直接退出print("游戏结束!")print(f"最终得分: {engine.get_total_score()}")if __name__ == "__main__":main()

为什么这样写?except Exception as e 这一行。这是你的“兜底”策略。即使有逻辑漏洞,程序也不会直接抛出一个红色的 Traceback 窗口闪退,而是会打印出错误信息并继续运行(或优雅退出)。对于初学者来说,看懂报错避免报错更重要,而友好的错误提示能帮你快速定位问题。

运行与测试:别信“我觉得能跑”

代码写完了,不能只靠“跑通了”来验证。我们需要测试。这里推荐使用 pytest

1. 编写测试用例

# tests/test_engine.py
import pytest
from engine import BowlingEngine
from exceptions import InvalidRollError, GameFinishedErrorclass TestBowlingEngine:def test_strikes(self):engine = BowlingEngine()# 10 次 Strikefor _ in range(12): # 10局,第10局2个Strike,共12球try:engine.roll(10)except GameFinishedError:breakassert engine.game_over == True# 注意:完整的计分逻辑测试需要更复杂的断言# 这里仅测试状态流转def test_invalid_pins(self):engine = BowlingEngine()with pytest.raises(InvalidRollError):engine.roll(11) # 超过10瓶,非法def test_over_capacity_in_frame(self):engine = BowlingEngine()engine.roll(8)with pytest.raises(InvalidRollError):engine.roll(3) # 8+3 > 10,非法

2. 运行测试

在终端执行:

pytest -v

为什么要这么做? 当你修改了 _process_regular_frame 的逻辑后,运行测试。如果测试挂了,说明你改坏了原有功能。如果测试过了,说明逻辑是自洽的。这就是“可复现”的工程思维。不要依赖人工点鼠标去测试,那太低效且容易遗漏边界。

优化扩展:从玩具到生产级

现在代码能跑了,但还不够“牛”。作为资深从业者,我们要考虑如何让它更健壮、更灵活。

1. 完善第10局的计分逻辑

前面的代码为了简化,省略了第10局的复杂 Bonus 计算。在生产环境中,你需要单独维护第10局的 rolls 列表。

思路:

  • 第10局前:正常记录 [ball1, ball2]
  • 第10局:记录 [ball1, ball2, ball3?]
  • 计分时,第10局的得分 = ball1 + ball2 + (ball3 if bonus)

2. 引入数据类(Data Class)

使用 Python 3.7+ 的 @dataclass 来封装一局的数据,让代码更清晰。

from dataclasses import dataclass
from typing import List@dataclass
class Frame:balls: List[int]is_strike: boolis_spare: bool@propertydef score(self):# 封装计分逻辑pass

这样,engine.py 里的 self.scores 就变成了 List[Frame],类型提示更明确,IDE 的智能提示也更友好。

3. 日志记录

在生产环境中,不要只用 print。引入 logging 模块。

import logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)# 在 roll 方法中
logger.info(f"Frame {self.current_frame}, Roll {self.current_roll_in_frame}: {pins}")

当用户反馈“分数不对”时,你可以通过日志文件还原当时的操作序列,而不是靠用户口述。这是解决线上问题的黄金法则。

4. 支持不同规则配置

不同地区或俱乐部可能有细微的规则差异。可以使用策略模式(Strategy Pattern)将“计分策略”抽离出来。

class ScoringStrategy:def calculate(self, frames):raise NotImplementedErrorclass StandardStrategy(ScoringStrategy):def calculate(self, frames):# 标准计分passclass CustomStrategy(ScoringStrategy):def calculate(self, frames):# 自定义计分pass

这样,你只需传入不同的策略对象,引擎就能适应不同的规则,无需修改核心代码。

小结:从报错到掌控

回顾一下,我们从“报错一堆看不懂 StackTrace”的痛点出发,搭建了一个结构清晰、异常处理完善的打保龄球项目。

  • 结构上:我们将逻辑、异常、入口分离,降低了耦合。
  • 逻辑上:我们通过状态机管理游戏进程,通过“快速失败”原则拦截非法输入。
  • 工程上:我们引入了单元测试和日志,让代码可测试、可追踪。

打保龄球这个案例,看似简单,实则涵盖了状态管理、边界条件处理、异常捕获、模块化设计等多个核心编程概念。它不是一个简单的脚本,而是一个微型的软件系统。

很多初学者喜欢写“一次性”的代码,写完就跑,不管维护。但真正的工程师,追求的是代码的可维护性鲁棒性。当你下次再遇到 StackTrace 时,不要害怕。把它当作一个线索,结合你写的日志、测试用例和模块化的代码,你能更快地定位问题。

记住,代码是写给人看的,顺便让机器执行。清晰的逻辑、友好的异常、完善的测试,才是好代码的标志。

你公司项目里是怎么处理这类复杂状态逻辑的?是直接用大段 if-else,还是引入了状态机或设计模式?欢迎在评论区分享你的实战经验,我们一起探讨更优雅的解决方案。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/21 21:16:06

刀剑乱舞网页版踩坑实录:面试必问的3个前端陷阱

刀剑乱舞网页版踩坑实录:面试必问的3个前端陷阱 看了一堆教程还是不会写项目?别急着怀疑自己智商,多半是掉进了那些教程没讲透的坑里。我混迹前端圈十年,见过太多人把时间耗在无关紧要的细节上,最后面试被问得哑口无言。今天不聊虚的,直接拆解【刀剑乱舞网页版】这类复杂单页应用(SPA)开发中,最容易让人翻车的…

作者头像 李华
网站建设 2026/9/21 21:15:35

5个新手避坑点:女生漫画头像生成器代码跑不通的底层逻辑

5个新手避坑点:女生漫画头像生成器代码跑不通的底层逻辑 复制来的代码跑不通,报错信息满屏红字,调试半天不知道从哪下手。这是无数初学者在尝试构建“女生漫画头像”生成工具时的真实写照。很多教程只给了结果,却跳过了环境配置和依赖解析的关键步骤,导致你看似懂了原理,实际连 import 都卡住。今天这篇…

作者头像 李华
网站建设 2026/9/21 21:15:24

搞定在线视频下载软件完整示例:配置不卡壳

搞定在线视频下载软件完整示例:配置不卡壳 配置环境就卡半天,这是大多数开发者接触 在线视频下载软件 时的真实写照。你想写个爬虫抓个高清片,结果依赖库装不上,或者解析出来的只有广告。别急,今天直接上 完整示例 ,带你从底层原理到代码实战,彻底解决这个痛点。 1. 核心原理:视频其实是个拼接包…

作者头像 李华
网站建设 2026/9/21 21:15:20

5个步骤搞定刘烨博客,避开高频面试题陷阱

5个步骤搞定刘烨博客,避开高频面试题陷阱 配置环境就卡半天,是不是觉得心累?别急,这其实是很多刚入行或者转行的开发者都会遇到的坑。更扎心的是,当你终于把环境跑起来,准备去刷 高频面试题…

作者头像 李华
网站建设 2026/9/21 21:14:59

搞定个人工资所得税计算器,面试必问的3个细节

搞定个人工资所得税计算器,面试必问的3个细节 很多后端开发同学都有过这种尴尬:LeetCode 上的二分查找、动态规划刷得飞起,语法倒背如流,但面试官一句“来,现场写个个人工资所得税计算器”,脑子瞬间空白。这不是你代码能力不行,而是你只练了“点”,没练“线”。在真实业务场景中,税务计算涉及分段累进、…

作者头像 李华
网站建设 2026/9/21 21:14:50

静电电压代码跑不通?这份避坑指南帮你省下3小时调试时间

静电电压代码跑不通?这份避坑指南帮你省下3小时调试时间 刚拿到一段计算静电电压的 Python 代码,运行报错,或者结果和物理公式对不上,是不是让你抓狂?别慌,这种“复制粘贴即翻车”的情况,在编程圈太常见了。很多人卡在浮点数精度、单位换算或者坐标系定义上,明明逻辑看着对,就是出不了数。今天这篇避坑指…

作者头像 李华