引言:当 Shell 遇上棋类游戏
在大多数人的印象中,Bash(Bourne Again Shell)常用于系统管理、自动化脚本编写,很少与游戏开发关联。然而,借助关联数组、循环逻辑和终端交互能力,Bash 完全可以实现轻量级的交互式游戏。本文将以五子棋为案例,拆解 Bash 游戏开发的核心技术,并探讨从基础实现到工程优化的完整路径。
一、核心实现:Bash 五子棋的技术架构
1. 数据结构设计
五子棋的棋盘本质是二维网格,Bash 中通过关联数组模拟二维数组:
declare -A board # 键为坐标i,j,值为棋子状态
BOARD_SIZE=10 # 10×10棋盘(可调整为15×15标准规格)
- 空位用
.
表示,玩家棋子分别用X
和O
标识。 - 坐标映射:
board[$i,$j]
对应棋盘第 i 行第 j 列的位置。
2. 核心功能模块
(1)棋盘初始化与渲染
init_board() {
for ((i=0; i<BOARD_SIZE; i++)); do
for ((j=0; j<BOARD_SIZE; j++)); do
board[$i,$j]='.' # 所有位置初始为空
done
done
}
print_board() {
clear # 清屏优化显示
echo -n " "
for ((i=0; i<BOARD_SIZE; i++)); do printf "%2d " $i; done # 列坐标
echo
for ((i=0; i<BOARD_SIZE; i++)); do
printf "%2d " $i # 行坐标
for ((j=0; j<BOARD_SIZE; j++)); do
printf " %s " "${board[$i,$j]}"
done
echo
done
}
- 输出格式示例:
0 1 2 3 4 5 6 7 8 9 0 . . . . . . . . . . 1 . . . . . . . . . . ...
(2)胜负判定算法
五子棋的胜利条件是某方向连续 5 子同色,需检查 4 个方向:
check_win() {
local x=$1 y=$2 player=$3
local directions=("1 0" "0 1" "1 1" "1 -1") # 横、竖、主对角、副对角
for dir in "${directions[@]}"; do
local dx=$(echo $dir | cut -d' ' -f1)
local dy=$(echo $dir | cut -d' ' -f2)
local count=1 # 当前落子本身计数1
# 正方向检查(如横向向右)
local nx=$((x+dx)) ny=$((y+dy))
while is_in_bounds "$nx" "$ny" && [[ ${board[$nx,$ny]} == $player ]]; do
((count++)); nx=$((nx+dx)); ny=$((ny+dy))
done
# 反方向检查(如横向向左)
nx=$((x-dx)) ny=$((y-dy))
while is_in_bounds "$nx" "$ny" && [[ ${board[$nx,$ny]} == $player ]]; do
((count++)); nx=$((nx-dx)); ny=$((ny-dy))
done
if ((count >= 5)); then return 0; fi # 满足5子连珠
done
return 1
}
is_in_bounds() { # 新增边界检查函数
local x=$1 y=$2
((x >= 0 && x < BOARD_SIZE && y >= 0 && y < BOARD_SIZE))
}
- 算法核心:对每个方向,分别向正负方向遍历,统计连续同色棋子数。
二、从可用到好用:脚本优化实践
1. 原脚本的三大隐患
- 边界溢出风险:原
check_win
未验证坐标范围,当棋子靠近边界时可能访问board[-1,5]
等非法位置。 - 变量作用域混乱:未用
local
声明的nx
、ny
可能在循环中被意外修改。 - 交互体验缺失:无清屏功能、错误提示模糊、游戏结束直接退出。
2. 优化后的关键改进
# 主游戏循环优化对比(部分)
while ((moves < max_moves)); do
print_board
echo "Player $current, enter your move (row col):"
read -r row col
# 输入验证增强
if [[ ! $row =~ ^[0-9]+$ || ! $col =~ ^[0-9]+$ || $row -ge $BOARD_SIZE || $col -ge $BOARD_SIZE ]]; then
echo "❌ 输入非法!请输入0-$((BOARD_SIZE-1))范围内的数字"
sleep 1 # 延迟显示错误,便于用户查看
continue
fi
# 落子逻辑
board[$row,$col]=$current
((moves++))
if check_win "$row" "$col" "$current"; then
print_board
echo "🎉 Player $current wins!"
read -p "按Enter键退出..." # 等待用户确认
return
fi
current=$([[ $current == "X" ]] && echo "O" || echo "X")
done
- 优化点总结:
- 新增
is_in_bounds
函数,彻底避免数组越界; - 错误提示明确化,加入颜色符号(需终端支持);
- 游戏结束后增加用户确认环节,提升交互友好性。
- 新增
三、进阶扩展:从单机到可演进的游戏框架
1. AI 对手实现思路
Bash 实现简单 AI 可基于以下策略:
- 防御优先:检查对手是否有四连珠(如
X X X X .
),优先阻断; - 进攻其次:寻找自身三连珠(如
X X . X
),扩展成四连珠; - 棋盘权重:中心区域权重高于边缘(如 (4,4) 位置优先级最高)。
# AI落子伪代码(简化逻辑)
ai_move() {
# 1. 检查对手四连珠
check_enemy_four_in_a_row && block_move
# 2. 检查自身三连珠
check_self_three_in_a_row && extend_move
# 3. 落子棋盘中心区域
place_at_center()
}
2. 界面升级:引入颜色与图形元素
借助 ANSI 转义序列实现棋子颜色区分:
# 定义颜色变量
RED='\033[0;31m' # X为红色
GREEN='\033[0;32m' # O为绿色
NC='\033[0m' # 重置颜色
# 打印时应用颜色
printf " ${RED}X${NC} " # 红色X
printf " ${GREEN}O${NC} " # 绿色O
- 效果示例:红色
X
与绿色O
在棋盘上形成视觉区分,提升游戏体验。
四、实践价值与技术延伸
1. Bash 游戏开发的局限与意义
- 局限:性能有限(纯文本界面、无图形加速),不适合复杂游戏;
- 意义:
- 深入理解 Shell 编程的边界与可能性;
- 为 Linux 系统下轻量级交互工具提供开发思路(如终端版小游戏、教育演示工具)。
2. 技术延伸方向
- 跨平台适配:通过
readline
库增强输入体验,或用expect
脚本实现自动化测试; - 混合开发:Bash 作为前端交互层,后端用 Python/Go 实现复杂逻辑(如 AI 算法);
- 实战案例:可扩展为井字棋、扫雷等更多终端游戏,形成 Shell 游戏开发系列。
总结
从一个简单的 Bash 五子棋脚本,我们看到了 Shell 编程的灵活性 —— 即使不依赖复杂框架,也能通过基础语法构建交互式应用。优化过程中涉及的边界检查、用户体验设计和算法逻辑,本质上与其他编程语言的开发思想相通。对于想深入 Shell 编程或探索小众开发场景的开发者,这类项目是极佳的实践入口。
五、脚本
#!/bin/bash
# 初始化变量
BOARD_SIZE=10
declare -A board
# 初始化棋盘
init_board() {
for ((i=0; i<BOARD_SIZE; i++)); do
for ((j=0; j<BOARD_SIZE; j++)); do
board[$i,$j]='.'
done
done
}
# 打印棋盘
print_board() {
echo -n " "
for ((i=0; i<BOARD_SIZE; i++)); do
printf "%2d " $i
done
echo
for ((i=0; i<BOARD_SIZE; i++)); do
printf "%2d " $i
for ((j=0; j<BOARD_SIZE; j++)); do
printf " %s " "${board[$i,$j]}"
done
echo
done
}
# 判断是否胜利
check_win() {
local x=$1
local y=$2
local player=$3
directions=(
"1 0" # 横
"0 1" # 竖
"1 1" # 主对角
"1 -1" # 副对角
)
for dir in "${directions[@]}"; do
dx=$(echo $dir | cut -d' ' -f1)
dy=$(echo $dir | cut -d' ' -f2)
count=1
# 正方向
nx=$((x + dx))
ny=$((y + dy))
while [[ ${board[$nx,$ny]} == $player ]]; do
((count++))
nx=$((nx + dx))
ny=$((ny + dy))
done
# 反方向
nx=$((x - dx))
ny=$((y - dy))
while [[ ${board[$nx,$ny]} == $player ]]; do
((count++))
nx=$((nx - dx))
ny=$((ny - dy))
done
if ((count >= 5)); then
return 0
fi
done
return 1
}
# 主游戏循环
play_game() {
init_board
current="X"
moves=0
max_moves=$((BOARD_SIZE * BOARD_SIZE))
while ((moves < max_moves)); do
print_board
echo "Player $current, enter your move (row col):"
read -r row col
if [[ ! $row =~ ^[0-9]+$ || ! $col =~ ^[0-9]+$ || $row -ge $BOARD_SIZE || $col -ge $BOARD_SIZE ]]; then
echo "Invalid input. Try again."
continue
fi
if [[ ${board[$row,$col]} != '.' ]]; then
echo "Cell already taken. Try again."
continue
fi
board[$row,$col]=$current
((moves++))
if check_win "$row" "$col" "$current"; then
print_board
echo "Player $current wins!"
return
fi
current=$([[ $current == "X" ]] && echo "O" || echo "X")
done
echo "It's a draw!"
}
play_game