- 示例工程
【免费下载链接】basic-computer-games
An updated version of the classic "Basic Computer Games" book, with well-written examples in a variety of common MEMORY SAFE, SCRIPTING programming languages. See https://coding-horror.github.io/basic-computer-games/
Poetry 是经典书籍Basic Computer Games(1978)中的第 70 号程序,它用 20 个短语按固定组序循环组合,再配合随机标点、缩进与分段规则,生成"可被宽松地视为日本俳句风格"的随机诗句。本文以 70_Poetry/README.md 为主体,结合仓库内 BASIC 原版 poetry.bas 与 Python、C#、Java、Ruby、JavaScript、Perl 六种移植实现,逐层拆解其词库组织、概率排版引擎与各语言重写思路,读完你将能完整理解该程序的运行机制,并学会如何替换短语库生成自定义风格的诗句。
程序概览:一个随机的俳句风格诗句生成器
Poetry 的设计目标非常纯粹:不依赖任何用户输入,程序启动后便持续不断地生成诗句,直到被强制中断。其核心特征在 70_Poetry/README.md 中概括为四点:
- 20 个短语,分为 4 组,每组 5 个,生成时基本按组顺序循环(第 1 组 → 第 2 组 → 第 3 组 → 第 4 组 → 段落结束);
- 随机插入逗号:约 19% 的概率;
- 随机插入缩进:约 22% 的概率;
- 随机开始新段落:约 18% 的概率,但保证至少每 20 个短语出现一次段落分隔。
README 还特别指出,程序的短语风格"略带爱伦·坡(Edgar Allen Poe)的意味"。对照 poetry.bas 中的短语表可以确认这一点——QUOTH THE RAVEN、NEVERMORE、MIDNIGHT DREARY、FIERY EYES、BIRD OR FIEND等几乎全部取材自坡的名篇《乌鸦》(The Raven)。原文档的作者信息同样值得保留:该程序原作者不详,后由 DEC 的 Jim Bailey、Peggy Ewing 与 Dave Ahl 修改和重做,1978 年收录于Basic Computer Games一书,其源码随后被 Vintage Basic 站点收录,成为本仓库移植工作的蓝本。
词库系统:4 组 × 5 个短语的完整短语表
综合 poetry.bas(行 90-135)、python/poetry.py 的line_1_options至line_4_options以及 csharp/Phrase.cs 中的_phrases静态表,可以交叉确认程序内置的完整 20 个短语:
| 组序 | 短语 1 | 短语 2 | 短语 3 | 短语 4 | 短语 5 |
|---|---|---|---|---|---|
| 第 1 组 | MIDNIGHT DREARY | FIERY EYES | BIRD OR FIEND | THING OF EVIL | PROPHET |
| 第 2 组 | BEGUILING ME | THRILLED ME | STILL SITTING.... | NEVER FLITTING | BURNED |
| 第 3 组 | AND MY SOUL | DARKNESS THERE | SHALL BE LIFTED | QUOTH THE RAVEN | SIGN OF PARTING |
| 第 4 组 | NOTHING MORE | YET AGAIN | SLOWLY CREEPING | ...EVERMORE | NEVERMORE |
每组内 5 个短语被等概率随机选中(每个 20%)。BASIC 原版通过行 215 的I=INT(INT(10*X)/2)+1把 0-9 的随机数折叠映射到 1-5,实现均匀抽取;Python 版则直接调用random.randint(0, 4)得到 0-4 的索引。
值得注意的是,短语表中埋藏着三条特殊控制规则,它们让诗句在随机之余仍能保持基本的语法连贯:
- 强制逗号连接:第 2 组的
BEGUILING ME与NEVER FLITTING会将状态U置为 2(见 poetry.bas 行 111、114),使后续短语大概率以逗号衔接,形成"XX ME, XX"的排比句式; - 跳过逗号机会:第 2 组的
STILL SITTING....直接跳转至行 212,绕过行 210 的逗号判断——因为短语以句点结尾,不能再接逗号;Python 版则在 maybe_comma 中做了更通用的保护:只要当前行以.结尾,就永不追加逗号; - 条件打印:第 3 组的
SIGN OF PARTING仅在U≠0时才输出(poetry.bas 行 125-126),U=0时直接跳过该短语,避免它在换行后突兀出现。
排版引擎:四重随机规则的概率参数
诗句的"排版"由三个独立概率控制,它们在 BASIC 原版中对应 poetry.bas 行 210-214,随机数统一由行 500 的子程序X=RND(1)提供:
| 规则 | 概率 | BASIC 行号 | 触发效果 |
|---|---|---|---|
| 插入逗号 | 19%(且要求U≠0) | 210-211 | 打印,,并将U置为 2 |
| 插入空格(同行续写) | 65% | 212-213 | 打印空格,U自增 1 |
| 换行(结束当前行) | 35% | 214 | 打印换行,U清零 |
变量U在此扮演"标点衔接状态"的角色:它记录当前行内已经累积的短语间距,U≠0表示行尚未结束、允许追加逗号。因此 19% 的逗号概率实际上是在"行未结束"的前提条件之下的概率,这与 README 中"插入逗号(随机 19%)"的描述完全吻合。
缩进逻辑位于行 230-235:当U=0(新行起点)且J为偶数时,打印 5 个空格作为缩进,概率约为 22%。分段逻辑位于行 240-270:J从 1 递增到 4 时依次进入第 1-4 组,J=5时打印换行完成一个"诗节"(stanza);计数器K累计已生成短语数,当K>20时额外打印空行、重置K,并跳到行 110 从第 2 组重新开始——这就是 README 所述"至少每 20 个短语换一次段落"的源码依据。
BASIC 原版逐段解析
poetry.bas 全文 51 行,结构非常紧凑,可划分为四个区段:
- 行 5-30(初始化与标题):行 5
Y=RND(-1)以负数参数初始化随机数种子;行 6-9 是被注释掉的调试代码(原本用于打印随机数序列并GOTO 999);行 10-30 居中打印POETRY与CREATIVE COMPUTING MORRISTOWN, NEW JERSEY标题。 - 行 90-135(四组短语):每组以
ON I GOTO ...根据当前短语序号I分派到具体PRINT语句,打印后统一汇合到行 210 的排版逻辑。 - 行 210-215(随机排版与下一短语选择):依次处理逗号、空格/换行,随后通过
GOSUB 500取新随机数并计算下一个I、递增J、K。 - 行 240-270(诗节与段落控制):
ON J GOTO 90,110,120,130,250按J值进入对应组;J=5时完成一个诗节并判断是否开启新段落;行 999END为程序终点。
主循环是一个典型的"行号跳转状态机":没有显式的FOR/WHILE,全靠GOTO与ON...GOTO在四个短语组之间轮转,这正是 1970 年代 BASIC 程序常见的控制流风格,也为后续面向过程语言的移植带来了不小的挑战。
Python 移植:数据类驱动的状态机重写
python/poetry.py 由 Dave LeCompte 移植,是理解整个算法的最佳入口。它将 BASIC 的全局变量显式封装为一个Statedataclass:
@dataclass class State: u: int = 0 # 行内标点衔接状态 i: int = 0 # 当前短语索引(0-4) j: int = 0 # 组内累计(0-4) k: int = 0 # 短语总数(决定段落) phrase: int = 1 # 当前所处短语组(1-5) line: str = "" # 正在拼装的行四个短语组被重写为process_phrase_1至process_phrase_4四个函数,每组内部用列表存储短语;特殊规则以元组副标记载入数据——例如第 2 组写作("BEGUILING ME", 2)、("STILL SITTING....", None),第 3 组的SIGN OF PARTING则用(True, "SIGN OF PARTING")标记"仅当U>0时打印"。主循环main()用一个phrase_processors字典把组号映射到处理函数,并辅以maybe_comma、pick_phrase两个辅助函数:
maybe_comma对应 BASIC 行 210-214:先做句点保护,再以0.19/0.65两个阈值决定逗号、空格还是换行;pick_phrase对应 BASIC 行 215-230:随机选短语、递增计数,并在u <= 0且j为奇数时追加 5 空格缩进。
移植版还顺带修复了原版的两个小问题:用print_centered按PAGE_WIDTH = 64计算居中空格数,并把State.phrase初始化为 1,从而将"第一句永远是 MIDNIGHT DREARY"这一行为显式化(详见下文移植注意事项)。
C# 移植:表格驱动的面向对象设计
C# 版本把短语表与状态彻底解耦,是六种移植中抽象程度最高的实现。其工程结构位于 csharp/:
- Program.cs:三行入口,
Poem.Compose(new ConsoleIO(), new RandomNumberGenerator())将输入输出与随机数两个依赖注入到组合器; - Phrase.cs:用
Phrase[][]静态二维数组存放 4×5 短语表,每个Phrase对象由"条件谓词Predicate<Context>"、"文本"与"副作用Action<Context>"三部分组成——SIGN OF PARTING注册了ctx => ctx.PhraseCount != 0条件,STILL SITTING....注册了ctx => ctx.SkipNextComma()副作用,BASIC 里的行号跳转在此变成了优雅的数据驱动声明; - Context.cs:集中管理全部可变状态,并原样保留了三条概率常量——
MaybeWriteComma使用0.19F、WriteSpaceOrNewLine使用0.65F、MaybeCompleteStanza在_lineCount > 20时结束段落;此外还通过MaybeCapitalise实现了"行首字母自动大写"的增强,弥补了 BASIC 全大写输出的生硬感; - Poem.cs:
Compose用while (true)无限循环串联WritePhrase → MaybeWriteComma → WriteSpaceOrNewLine → Update → MaybeIndent → 段落判断,与 BASIC 主循环一一对应。
其他移植:Java、Ruby、JavaScript 与 Perl
- Java(java/Poetry.java,Darren Cardenas 转换):将三个概率提炼为常量
COMMA_RATE = 0.19、SPACE_RATE = 0.65、PARAGRAPH_RATE = 20,并用Step枚举(WORD_GROUP1到RANDOMIZE_COUNTERS)构建了一个显式的有限状态机,startGame()内的switch (nextStep)每次仅推进一个状态——这是对 BASICON...GOTO最忠实的结构化重现。 - Ruby(ruby/poetry.rb):与 Python 版结构同构,同样是
State类 +process_phrase_*函数族 +maybe_comma/pick_phrase,可作为对照阅读。 - JavaScript(javascript/poetry.js,Oscar Toledo G. 转换):刻意保留了 BASIC 的行式结构,
if (j == 1) ... else if (j == 2) ...逐组展开四个短语组的 switch;它也是唯一一个主动终结程序的移植——通过++times == 10在生成 10 个段落后退出循环,并显式初始化i = 1; j = 1; k = 0; u = 0。 - Perl(perl/poetry.pl):由仓库工具 bas2perl.pl 自动转换而成,文件头注释直白地写着"Too much spaguetti code to be properly converted",全篇保留
goto Line90式的行号跳转,恰好展示了 BASIC 原版控制流"意面化"的一面。
移植注意事项(Porting Notes)
70_Poetry/README.md 末尾记录了两条原版代码的历史遗留问题,移植时必须留意:
- 未初始化变量
I:程序从行 90ON I GOTO 100,101,102,103,104开始,而I在此之前从未被赋值。从源码行为看,I=0时ON...GOTO不会匹配任何目标,执行会自然落入行 100,即第一句必然是MIDNIGHT DREARY。移植时"应当显式初始化为 0"——但这样做意味着输出永远以 "midnight dreary" 开头,随机性从第一句起就打了折扣。Python、Ruby、JavaScript 版都选择了显式初始化,因此保留了这一固定开头。 END语句不可达:行 999 的END从未被执行——主循环在行 90-270 之间无限跳转,程序会持续不断地生成诗句,直到被用户强制中断(Ctrl+C 或关闭终端)。除 JavaScript 版加入了 10 段落的退出上限外,其余移植版本均以while True忠实复现了这一"无限生成"行为。
运行方式
从源码形态可以确认各语言版本的运行入口:
- Python:
cd 70_Poetry/python && python3 poetry.py - Ruby:
cd 70_Poetry/ruby && ruby poetry.rb - Java:
cd 70_Poetry/java && javac Poetry.java && java Poetry - C#:
cd 70_Poetry/csharp && dotnet run(工程含 Poetry.csproj 与 Poetry.sln) - JavaScript:用浏览器直接打开 poetry.html,输出渲染到页面
output区域 - Perl:
cd 70_Poetry/perl && perl poetry.pl - BASIC 原版:需在兼容 Vintage BASIC 语法的解释器中加载 poetry.bas 运行;各语言子目录下的 README.md 记录了移植来源信息,仓库根目录的 HOW_TO_RUN_THE_GAMES.md 提供了整体运行指引
运行后程序会立即开始打印诗句,例如(不同随机种子下结果不同):
MIDNIGHT DREARY BEGUILING ME, AND MY SOUL NOTHING MORE NEVER FLITTING DARKNESS THERE YET AGAIN自定义短语库:让程序"写"出你的主题
Poetry 最有趣的扩展点在于短语表完全可替换。原 README 特意附上了一组自然主题的短语示例,供读者直接替换进程序:
Carpet of ferns Mighty Oaks Morning dew Grace and beauty Tang of dawn Silently singing Swaying pines Nature speaking Entrances me Untouched, unspoiled Soothing me Shades of green Rustling leaves Tranquility Radiates calm …so peacefulREADME 建议读者尝试"计算机技术、爱情浪漫、四岁儿童"等主题的短语,并寄回程序输出。从实现上看,替换方式非常直接:在 Python/Ruby 版中修改process_phrase_1至process_phrase_4里的四个列表,或在 C# 版中改写 Phrase.cs 的_phrases二维表。只要保持"每组 5 个短语"的规模,并酌情保留两条语法规则——以句点结尾的短语不应再接逗号、需要排比的短语可强制逗号衔接——即可让程序输出完全不同的诗风,而 19% 逗号、65% 空格、22% 缩进与每 20 短语一段的随机排版引擎无需任何改动。
综上,Poetry 是一个体量极小但算法层次丰富的经典程序:它用一张 4×5 短语表和三个概率常数,在"随机"与"语法连贯"之间取得了精妙的平衡;而仓库内六种语言的移植实现,则从行号跳转、数据类状态机、表格驱动对象到显式 FSM,完整展示了同一算法在不同编程范式下的表达方式,是阅读与学习经典游戏移植的最佳样本之一。
- 示例工程
【免费下载链接】basic-computer-games
An updated version of the classic "Basic Computer Games" book, with well-written examples in a variety of common MEMORY SAFE, SCRIPTING programming languages. See https://coding-horror.github.io/basic-computer-games/
相关推荐
从 1978 年 BASIC 到 10 种现代语言:Chief 数字推理游戏移植全解析(basic-computer-games)
从 1978 年 BASIC 到 10 种现代语言:Chief 数字推理游戏移植全解析(basic computer games) 导读 Chief(中文可译作
示例工程Basic Computer Games 之 Hangman:从 1978 年 BASIC 原版到 C / MiniScript 的多语言移植实战解析
Basic Computer Games 之 Hangman:从 1978 年 BASIC 原版到 C / MiniScript 的多语言移植实战解析 导读 H
示例工程Basic Computer Games 之 Diamond 菱形图案程序:从 1978 年 BASIC 到现代语言的移植解析
Basic Computer Games 之 Diamond 菱形图案程序:从 1978 年 BASIC 到现代语言的移植解析 本指南以经典书籍《Basic C
示例工程
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考