Python 实现:从数学模型到完整控制台版《2048》游戏
Python 实现:从数学模型到完整控制台版《2048》游戏
本文将带你从数学建模角度完整解析《2048》游戏的设计逻辑,结合 Python 实现代码,逐步还原游戏从状态表示、移动合并、胜负判断到控制循环的全过程。
导入库:
import random
import os
import msvcrt # Windows下的键盘输入处理
1. 模块 1:状态表示与初始化
1.1 模块 1 - 数学模型
1.1.1 状态矩阵定义
定义游戏在时刻 ttt 的状态矩阵为:
St=[s11(t)s12(t)…s1n(t)s21(t)s22(t)…s2n(t)⋮⋮⋱⋮sn1(t)sn2(t)…snn(t)]S_t = \begin{bmatrix} s_{11}(t) & s_{12}(t) & \dots & s_{1n}(t) \\ s_{21}(t) & s_{22}(t) & \dots & s_{2n}(t) \\ \vdots & \vdots & \ddots & \vdots \\ s_{n1}(t) & s_{n2}(t) & \dots & s_{nn}(t) \end{bmatrix} St=s11(t)s21(t)⋮sn1(t)s12(t)s22(t)⋮sn2(t)……⋱…s1n(t)s2n(t)⋮snn(t)
其中
sij(t)∈{0,2,4,8,…},i,j∈{1,2,3,4}s_{ij}(t) \in \{0, 2, 4, 8, \dots\}, \quad i, j \in \{1,2,3,4\} sij(t)∈{0,2,4,8,…},i,j∈{1,2,3,4}
表示第 iii 行第 jjj 列方格中的数值。若 sij(t)=0s_{ij}(t) = 0sij(t)=0,则该格为空。
1.1.2 游戏全局状态
定义整体游戏状态为:
Stgame=(St,scoret,best_scoret,wont,game_overt)S_t^{\text{game}} = (S_t, \text{score}_t, \text{best\_score}_t, \text{won}_t, \text{game\_over}_t) Stgame=(St,scoret,best_scoret,wont,game_overt)
初始状态为:
S0game=(S0′,0,0,False,False),S0′=S0+P1+P2S_0^{\text{game}} = (S_0', 0, 0, \text{False}, \text{False}), \quad S_0' = S_0 + P_1 + P_2 S0game=(S0′,0,0,False,False),S0′=S0+P1+P2
其中 P1,P2P_1, P_2P1,P2 为随机生成的初始方块矩阵。
1.1.3 随机生成新方块
空格集合定义为:
Et={(i,j)∣si,j(t)=0}E_t = \{(i,j) \mid s_{i,j}(t) = 0\} Et={(i,j)∣si,j(t)=0}
在空格集合中随机选择位置:
(i∗,j∗)=随机选择(Et)(i^*, j^*) = \text{随机选择}(E_t) (i∗,j∗)=随机选择(Et)
生成新方块值:
si∗,j∗(t+1)={2,概率 0.94,概率 0.1s_{i^*,j^*}(t+1) = \begin{cases} 2, & \text{概率 } 0.9 \\ 4, & \text{概率 } 0.1 \end{cases} si∗,j∗(t+1)={2,4,概率 0.9概率 0.1
1.2 模块 1 - 代码实现
下面展示该部分的 Python 实现,对应公式的每一个步骤:
def __init__(self):"""初始化游戏"""self.grid_size = 4self.grid = [[0 for _ in range(self.grid_size)] for _ in range(self.grid_size)]self.score = 0self.best_score = 0self.game_over = Falseself.won = False# 初始化游戏,添加两个方块for _ in range(2):self.add_new_tile()def add_new_tile(self):"""在随机空位置添加新方块(2或4)"""# 找出所有空白格坐标empty_cells = [(i, j) for i in range(self.grid_size)for j in range(self.grid_size) if self.grid[i][j] == 0]if not empty_cells:return False# 随机选取空格i, j = random.choice(empty_cells)# 以90%概率生成2,10%概率生成4self.grid[i][j] = 2 if random.random() < 0.9 else 4return True
1.3 模块 1 - 逻辑说明
初始化阶段:
- 创建一个 4×44\times44×4 的零矩阵;
- 随机生成两个初始方块;
- 初始化分数、最高分及游戏状态。
数学意义:
- 游戏从空棋盘(零矩阵)开始,随机性使每局初始状态不同,形成状态空间的多样性。
1.4 初始状态可视化示例
初始状态可能如下(用矩阵形式):
S0′=[0200000000040000]S_0' = \begin{bmatrix} 0 & 2 & 0 & 0 \\ 0 & 0 & 0 & 0 \\ 0 & 0 & 0 & 4 \\ 0 & 0 & 0 & 0 \end{bmatrix} S0′=0000200000000040
2. 模块 2:方块移动与合并
2.1 模块 2 - 数学模型
2.1.1 单行压缩与合并
对一行向量 L=[s1,s2,s3,s4]L = [s_1, s_2, s_3, s_4]L=[s1,s2,s3,s4] 先去除零元素:
L′=[si∣si≠0]L' = [s_i \mid s_i \neq 0] L′=[si∣si=0]
合并规则:
sj′={2sj,sj=sj+1sj,否则s_j' = \begin{cases} 2 s_j, & s_j = s_{j+1} \\ s_j, & \text{否则} \end{cases} sj′={2sj,sj,sj=sj+1否则
合并后删除重复元素,并在右端填充零:
L′′=补零(L′)L'' = \text{补零}(L') L′′=补零(L′)
分数更新为:
scoret+1=scoret+∑合并的方块sj\text{score}_{t+1} = \text{score}_t + \sum_{\text{合并的方块}} s_j scoret+1=scoret+合并的方块∑sj
2.1.2 四个方向的移动矩阵
-
向左(Left):
St′=[_merge_line(Ri)∣Ri∈St]S_t' = [\_merge\_line(R_i) \mid R_i \in S_t] St′=[_merge_line(Ri)∣Ri∈St] -
向右(Right):
St′=[反转(_merge_line(反转(Ri)))∣Ri∈St]S_t' = [\text{反转}(\_merge\_line(\text{反转}(R_i))) \mid R_i \in S_t] St′=[反转(_merge_line(反转(Ri)))∣Ri∈St] -
向上(Up):
St′=转置([_merge_line(Ri)∣Ri∈转置(St)])S_t' = \text{转置}([\_merge\_line(R_i) \mid R_i \in \text{转置}(S_t)]) St′=转置([_merge_line(Ri)∣Ri∈转置(St)]) -
向下(Down):
St′=转置([反转(_merge_line(反转(Ri)))∣Ri∈转置(St)])S_t' = \text{转置}([\text{反转}(\_merge\_line(\text{反转}(R_i))) \mid R_i \in \text{转置}(S_t)]) St′=转置([反转(_merge_line(反转(Ri)))∣Ri∈转置(St)])
2.2 模块 2 - 代码实现
# ========= 移动与合并逻辑 =========
def _merge_line(self, line):"""合并单行(核心算法)返回新行和是否发生移动的标志"""original = list(line)# 1. 压缩非零元素new_line = [num for num in line if num != 0]# 2. 合并相邻相同数字i = 0while i < len(new_line) - 1:if new_line[i] == new_line[i + 1]:new_line[i] *= 2self.score += new_line[i]new_line.pop(i + 1)new_line.append(0) # 补零i += 1i += 1# 3. 补零到原始长度new_line += [0] * (self.grid_size - len(new_line))# 4. 返回新行与是否移动return new_line, new_line != originaldef _move(self, direction):"""通用移动逻辑direction: 'up', 'down', 'left', 'right'"""moved = False# 上/下移动需要先转置if direction in ('up', 'down'):self.grid = [list(row) for row in zip(*self.grid)]for i in range(self.grid_size):row = self.grid[i][::-1] if direction in ('right', 'down') else self.grid[i]new_row, has_moved = self._merge_line(row)if direction in ('right', 'down'):new_row.reverse()self.grid[i] = new_rowmoved = moved or has_moved# 上/下移动还原if direction in ('up', 'down'):self.grid = [list(row) for row in zip(*self.grid)]return moved
2.3 模块 2 - 逻辑说明
- 单行处理核心:
_merge_line统一完成压缩、合并、补零和得分更新。 - 方向抽象化:
_move根据方向决定是否需要行翻转或矩阵转置,从而只用一套逻辑处理四个方向。 - 移动标志: 每次移动会返回
moved=True或False,用于判断是否生成新方块。
2.4 示意矩阵变化
假设某行初始状态:
R=[2,0,2,4]R = [2, 0, 2, 4] R=[2,0,2,4]
-
压缩非零元素:
[2,2,4][2, 2, 4] [2,2,4] -
合并:
[4,4,0][4, 4, 0] [4,4,0] -
得分增加 4(2+2)
-
补零完成最终行:
[4,4,0,0][4, 4, 0, 0] [4,4,0,0]
3. 模块 3:胜负判断与游戏结束检测
3.1 模块 3 - 数学模型
3.1.1 胜利条件
若存在某格值为 204820482048,则胜利:
wont={True,∃i,j:si,j(t)=2048False,否则\text{won}_t = \begin{cases} \text{True}, & \exists i,j: s_{i,j}(t) = 2048 \\ \text{False}, & \text{否则} \end{cases} wont={True,False,∃i,j:si,j(t)=2048否则
3.1.2 游戏结束条件
若无法移动,则游戏结束。具体条件为:
-
棋盘无空格:
∀i,j:si,j(t)≠0\forall i,j: s_{i,j}(t) \neq 0 ∀i,j:si,j(t)=0 -
水平方向和垂直方向不存在相邻相同方块:
∀i,j<4:si,j≠si,j+1且 sj,i≠sj+1,i\forall i,j<4: s_{i,j} \neq s_{i,j+1} \text{ 且 } s_{j,i} \neq s_{j+1,i} ∀i,j<4:si,j=si,j+1 且 sj,i=sj+1,i
若满足以上条件,则:
game_overt=True\text{game\_over}_t = \text{True} game_overt=True
3.1.3 重置游戏
更新最高分:
best_scoret+1=max(best_scoret,scoret),scoret+1=0\text{best\_score}_{t+1} = \max(\text{best\_score}_t, \text{score}_t), \quad \text{score}_{t+1} = 0 best_scoret+1=max(best_scoret,scoret),scoret+1=0
清空棋盘并重置状态:
St+1=O4×4,wont+1=game_overt+1=FalseS_{t+1} = O_{4\times4}, \quad \text{won}_{t+1} = \text{game\_over}_{t+1} = \text{False} St+1=O4×4,wont+1=game_overt+1=False
随机生成两个初始方块:
S0′=S0+P1+P2S_0' = S_0 + P_1 + P_2 S0′=S0+P1+P2
3.2 模块 3 - 代码实现
# ========= 状态检测 =========
def check_win(self):"""检查是否有 2048 方块"""if any(2048 in row for row in self.grid):self.won = Truedef check_game_over(self):"""检查是否无法移动"""# 1. 检查是否有空格子if any(0 in row for row in self.grid):return False# 2. 检查水平和垂直方向是否有可合并方块for i in range(self.grid_size):for j in range(self.grid_size - 1):if self.grid[i][j] == self.grid[i][j + 1]:return Falseif self.grid[j][i] == self.grid[j + 1][i]:return False# 3. 无法移动,则游戏结束self.game_over = Truereturn Truedef reset_game(self):"""重置游戏"""# 更新最高分self.best_score = max(self.best_score, self.score)self.score = 0self.game_over = self.won = False# 清空棋盘self.grid = [[0] * self.grid_size for _ in range(self.grid_size)]# 初始化两个方块for _ in range(2):self.add_new_tile()
3.3 模块 3 - 逻辑说明
- 胜利检测
- 使用
any()判断棋盘中是否存在 2048 方块。 - 一旦出现,设置
self.won = True,但游戏可继续进行以追求更高分数。
- 使用
- 游戏结束检测
- 首先检查是否存在空格子,有空格子则游戏未结束。
- 然后检查所有相邻行列是否有相同方块,有则可合并,游戏未结束。
- 如果没有空格子且无法合并,则游戏结束。
- 重置游戏
- 保存最高分。
- 清空棋盘和分数,重置状态标志。
- 随机生成两个新方块,重新开始游戏。
4. 模块 4:控制台显示与用户界面
4.1 模块 4 - 数学模型
4.1.1 棋盘显示矩阵
显示(Gt)=[g1,1g1,2g1,3g1,4g2,1g2,2g2,3g2,4g3,1g3,2g3,3g3,4g4,1g4,2g4,3g4,4],gi,j={si,j(t),si,j(t)≠0空格,si,j(t)=0\text{显示}(G_t) = \begin{bmatrix} g_{1,1} & g_{1,2} & g_{1,3} & g_{1,4} \\ g_{2,1} & g_{2,2} & g_{2,3} & g_{2,4} \\ g_{3,1} & g_{3,2} & g_{3,3} & g_{3,4} \\ g_{4,1} & g_{4,2} & g_{4,3} & g_{4,4} \end{bmatrix}, \quad g_{i,j} = \begin{cases} s_{i,j}(t), & s_{i,j}(t) \neq 0 \\ 空格, & s_{i,j}(t) = 0 \end{cases} 显示(Gt)=g1,1g2,1g3,1g4,1g1,2g2,2g3,2g4,2g1,3g2,3g3,3g4,3g1,4g2,4g3,4g4,4,gi,j={si,j(t),空格,si,j(t)=0si,j(t)=0
4.1.2 操作与状态提示
-
清屏:
清屏()⟹删除上一帧显示\text{清屏}() \implies \text{删除上一帧显示} 清屏()⟹删除上一帧显示 -
分数显示:
scoret→显示当前分数,best_scoret→显示历史最高分\text{score}_t \to \text{显示当前分数}, \quad \text{best\_score}_t \to \text{显示历史最高分} scoret→显示当前分数,best_scoret→显示历史最高分 -
棋盘行输出:
输出行(Ri)=∣di,1∣di,2∣di,3∣di,4∣\text{输出行}(R_i) = | d_{i,1} | d_{i,2} | d_{i,3} | d_{i,4} | 输出行(Ri)=∣di,1∣di,2∣di,3∣di,4∣ -
状态输出:
输出状态(wont,game_overt)\text{输出状态}(\text{won}_t, \text{game\_over}_t) 输出状态(wont,game_overt)
该函数满足:
- 若 wont=True\text{won}_t = \text{True}wont=True,显示“恭喜你赢了!”
- 若 game_overt=True\text{game\_over}_t = \text{True}game_overt=True,显示“游戏结束!”
- 否则显示当前棋盘与分数信息。
4.2 模块 4 - 代码实现
def display_grid(self):"""在控制台显示游戏网格和状态"""# 清屏os.system('cls' if os.name == 'nt' else 'clear')# 显示标题和分数print("=" * 30)print(" " * 10 + "2048 游戏" + " " * 10)print("=" * 30)print(f"分数: {self.score:<8}最高分: {self.best_score}")print("-" * 30)# 显示棋盘网格for row in self.grid:row_str = "|".join(f"{val:^6}" if val != 0 else " " * 6 for val in row)print(f"|{row_str}|")print("-" * 30)# 显示操作提示print("使用方向键或 WASD 移动方块,'Q' 退出游戏")# 显示游戏状态if self.won:print("恭喜你赢了!继续挑战更高分数!")elif self.game_over:print("游戏结束!按任意键重新开始")
4.3 模块 4 - 逻辑说明
- 清屏
使用系统命令cls(Windows) 或clear(Linux/macOS) 清除之前输出。 - 显示标题与分数
格式化输出当前分数和最高分,便于玩家查看。 - 显示棋盘网格
- 遍历棋盘矩阵的每一行。
- 使用固定宽度格式化每个方块值,使网格整齐。
- 空方块显示为空格。
- 显示操作提示
指示玩家使用方向键或 WASD 移动方块。 - 显示游戏状态提示
根据won和game_over标志显示不同信息。
4.4 界面设计
==============================2048 游戏
==============================
分数: 0 最高分: 0
------------------------------
| | 2 | | |
------------------------------
| | | | |
------------------------------
| | | | 4 |
------------------------------
| | | | |
------------------------------
使用方向键或 WASD 移动方块,'Q' 退出游戏
5. 模块 5:主循环与用户交互
5.1 游戏主循环
在每一帧交互中,定义用户输入函数:
Ut=get_key()U_t = \text{get\_key}() Ut=get_key()
输入空间:
Ut∈{up,down,left,right,quit}U_t \in \{\text{up}, \text{down}, \text{left}, \text{right}, \text{quit}\} Ut∈{up,down,left,right,quit}
状态转移函数为:
St+1=f(St,Ut)S_{t+1} = f(S_t, U_t) St+1=f(St,Ut)
其中:
- 若 Ut=方向U_t = \text{方向}Ut=方向,则调用
_move(U_t); - 若棋盘状态变化,则调用
add_new_tile(); - 若 Ut=quitU_t = \text{quit}Ut=quit,则退出循环。
5.2 模块 5 - 代码实现
def run(self):"""运行游戏主循环"""while True:# 显示网格self.display_grid() # 显示当前状态self.check_win() # 检查是否赢了# 检查是否游戏结束if self.check_game_over(): # 如果游戏结束self.display_grid() # 显示最终状态input("按回车键重新开始...")self.reset_game() # 重置游戏continue# 获取用户输入key = self.get_key() if key == 'quit': # 如果用户选择退出print("感谢游玩!再见!")breakif key in ('up', 'down', 'left', 'right'): # 处理用户移动if self._move(key): # 如果移动成功self.add_new_tile() # 添加新方块
5.3 模块 5 - 逻辑说明
- 事件循环结构: 游戏逻辑以无限循环运行,每帧更新状态。
- 按键捕获机制:
msvcrt.getch()可监听方向键或 WASD 键,实现即时响应。 - 输入映射层: 将低级字节码输入(如
b'H')映射为方向命令,便于逻辑统一。 - 状态控制: 若本轮操作导致棋盘变化,则随机生成新方块并进入下一轮循环。
6. 模块 6:程序入口与启动
6.1 模块 6 - 代码实现
import random
import os
import msvcrt # Windows下的键盘输入处理class Console2048:def __init__(self, grid_size=4):"""初始化游戏"""def add_new_tile(self):"""在随机空位置添加新方块(2或4)"""def display_grid(self):"""在控制台显示游戏网格"""# ========= 移动与合并逻辑 =========def _merge_line(self, line):"""合并单行(核心算法),返回新行与是否移动标志"""def _move(self, direction):"""通用移动逻辑(方向: up/down/left/right)"""# ========= 状态检测 =========def check_win(self):"""检查是否有 2048 方块"""def check_game_over(self):"""检查是否无法移动"""def reset_game(self):"""重置游戏"""# ========= 输入与控制 =========def get_key(self):"""获取键盘输入"""# ========= 主循环 =========def run(self):"""运行游戏主循环"""if __name__ == "__main__":print("2048 控制台版游戏")print("使用方向键或 WASD 键移动方块,按 'Q' 退出游戏")input("按回车键开始游戏...")Console2048().run()
6.2 模块 6 - 逻辑说明
- 程序入口判断: 使用 Python 标准写法
if __name__ == "__main__"保证独立运行。 - 初始化提示信息: 以交互方式提示玩家游戏说明。
- 实例化与运行: 调用类
Console2048的run()方法进入主循环,实现完整的控制台游戏体验。
7. 总结
本文以数学建模视角完整剖析了 2048 控制台版游戏 的程序逻辑。
从状态矩阵定义到用户交互主循环,构建了一个由五个核心方程驱动的有限状态系统:
{St+1=f(St,Ut)scoret+1=scoret+∑合并值wont=[∃sij=2048]game_overt=[∀sij≠0∧无相邻相等]Et={(i,j)∣sij=0}\begin{cases} S_{t+1} = f(S_t, U_t) \\ \text{score}_{t+1} = \text{score}_t + \sum \text{合并值} \\ \text{won}_t = [\exists s_{ij}=2048] \\ \text{game\_over}_t = [\forall s_{ij}\neq 0 \land 无相邻相等] \\ E_t = \{(i,j)\mid s_{ij}=0\} \end{cases} ⎩⎨⎧St+1=f(St,Ut)scoret+1=scoret+∑合并值wont=[∃sij=2048]game_overt=[∀sij=0∧无相邻相等]Et={(i,j)∣sij=0}
这一结构展示了如何用数学模型统一游戏逻辑与程序实现:
- 状态矩阵映射游戏局势;
- 转移函数刻画玩家动作;
- 概率分布控制随机生成;
- 判定函数定义系统边界。
