翻转棋,以及被重复使用的代码
如果把程序比作建筑,把代码比作砖,是不合适的,因为同一块砖不能彻在建筑的两个地方,而前者可以。代码在时间上是经过压缩的,因此,理解代码与否决定是否能看清其原貌。这也是编程与砌墙相比,似乎更有意思的一个地方。
问题
有一个段代码,实现了一个翻转棋(黑白棋)的基本功能,包括人机对战、人人对战以及保存棋局, 需要在其上添加:
- 复盘功能
- 困难模式;目前人机包含简单(随机法)和普通(翻转最多子法)两种模式
图1 翻转棋局面(来源)
复盘
- 复盘包含棋局保存、读取以及局面的推进和退回
- 原代码已经实现了棋局保存,将棋局双方落子的坐标依次保存在文本文件中
- 原代码中实现了人人对战模式,双方依次点击棋盘可落子处,推动棋局发展;考虑
人人对战
与复盘的顺序推进
两个过程十分相似,只是获得落子坐标的方式不同,前者通过鼠标点击,后者从文件中读取 将复盘当作
人人对战
,将文件中的坐标作为参数,重复使用点击事件
的代码,即可实现下一步
的功能而几乎不需要添加新的代码逻辑,问题在于:人人对战
在执行落子的时候,并没有保留上一步的状态,正常实现退回上一步
的功能需要写不少代码
- 考虑到达上一步状态的方式其实有两种,一种是从当前状态退回,另一种是之前已经经历过的,即从起始状态一步一步到达上一步状态;若使用第二种方式,即可采用与实现
下一步
时类似的处理,几乎不需要添加新的代码逻辑
图2 下一步和上一步示意图
困难模式
- 困难模式的电脑落子算法,需要尝试所有可行的落子位置,并对落子后的局面进行评估,选择分数最高的落子
- 原代码中
board
对象有applyMove
方法,调用即可得到落子后的局面,但它并没有提供撤回的方法,要在调用applyMove
方法之前,保存落子前的状态,以便进行其它尝试 考虑每次尝试落子前对
board
对象进行深拷贝,然后在副本上进行演算,问题在于:- 原代码中
board
对象除了表示棋盘还负责棋盘界面的渲染,因此持有许多UI元素,如位图等,这些元素阻碍了对board
对象的深拷贝
- 原代码中
- 考虑新建一个
BoardRenderer
类,把与棋盘界面相关的代码从Board
类中提取到此类中 - 至此,即可在副本上演算,确定落子位置后再在真实的
board
对象上落子,实现困难模式而除了落子算法,几乎不需要添加其它新的代码逻辑
图3 新的BoardRenderer类
总结
世上有一个规律,即复杂的东西往往由简单的部分组合而成。问题也是如此。分解问题可行的原因在于,我们能在已经解决过的问题中发现与此问题存在某些相似的部分,并且能够理解这些部分与此问题的组成关系。