目录
前言
最近在github上看到一个用bash shell实现2048小游戏的脚本感觉挺有意思的,就自己仿照他的样子稍作修改再实现了一遍我的实现,并记录下实现的详细过程。
脚本的运行如图:
重要变量
- board 是一个整型数组,用来存储所有格子中的当前数值,它的下标即从0到总格子数减1,与界面中的格子从左到右,再从上到下,一一对应。数组的所有元素,在全新的游戏开始时用0初始化,而载入游戏时,用存档目录中的文件初始化。
- pieces 用来储存当前界面中所有非零的格子数。
- score 用来储存当前的得分总数。
- flag_skip 用来防止一个格子在同一回合中被操作两次。
- moves 用来测试游戏还能不能被操作,是否已经输掉游戏。
- ESC 作为Escape屏幕控制码,在输出颜色格式的时候使用,\e或者\033等都可以。
- header 也就是界面的标题,介绍性质的,可以自己定义
- start_time 记录程序开始的时间
这四个变量作为默认值,可以通过选项改变
- board_size 即游戏界面的每边的大小,限制在3至9之间
- target 即游戏成功的目标,可以自己设定,但需要是2的幂
- reload_flag 即是否载入存档
- config_dir 即存档文件的目标目录
- last_added 即最新生成的块的位置
- first_round 即第一个生成的块的位置。因为游戏最开始必须要生成两个块,而我定义用黄色表现这两个块以及用黄色表示以后每次最新生成的块。
- index_max 因为数组的下标从0开始,所以定义这个变量为游戏界面的每边的大小减一
- fields_total 即游戏界面中格子的总数
定义一个数组用来储存各个数字的颜色表现,搭配前面定义的$ESC构成Escape屏幕控制码,64及以上添加了高亮和白底,这些可以自行修改。
功能函数
退出处理
这里调用end_game函数处理INT信号,亦即使用ctrl+c
发出退出请求,由end_game进行退出流程。
print_board
83 function print_board(){
84 clear
85 printf "**************************************************************\n"
86 printf "***********************$header*************************\n"
87 printf "*$ESC[1;5;33mpieces=%-11d target=%-11d score=%-12d$ESC[0m*\n" $pieces $target $score
88 printf "**************************************************************\n"
89 echo
90 printf "/------"
91 for((row=1;row<=index_max;row++))
92 do
93 printf "+------"
94 done
95 printf '\\\n'
96 for((row=0;row<=index_max;row++))
97 do
98 printf "|"
99 for((line=0;line<=index_max;line++))
100 do
101 if let ${board[$row*$board_size+$line]}
102 then
103 if let '(last_added==(row*board_size+line))|(first_round==(row*board_size+line))'
104 then
105 printf "$ESC[1;33m %4d $ESC[0m|" ${board[$row*$board_size+$line]}
106 else
107 printf "$ESC[${colors[${board[$row*$board_size+$line]}]}m %4d $ESC[0m|" ${board[$row*$board_size+$line]}
108 fi
109 else
110 printf " |"
111 fi
112 done
113 if ((row!=index_max))
114 then
115 printf "\n|------"
116 for((r=1;r<=index_max;r++))
117 do
118 printf "+------"
119 done
120 printf "|\n"
121 fi
122 done
123 printf '\n\\------'
124 for((row=1;row<=index_max;row++))
125 do
126 printf "+------"
127 done
128 printf "/\n"
129 }
print_board函数用来打印游戏界面
-
86至88行打印台头
-
90至95行及123至128行分别打印游戏界面的
和
-
96至122行用一个双重循环遍历整个界面,外循环row即行,内循环line即列。
- 其中101行用来判断当前位置的格子内的值是否为0,若非零则输出。103行是在格子内的值非零的情况下,判断是否为最新生成的块或第一回合的块,如果是的话,用特定的黄色输出,如果不是的话,则按前面定义好的数值对应的颜色输出。
- 113至121行用来输出界面每两行中间的
generate_piece
function generate_piece(){
137 while true
138 do
139 ((pos=RANDOM%fields_total))
140 let ${board[$pos]} ||{ let value=RANDOM%10?2:4;board[$pos]=$value;last_added=$pos;break;}
141 done
142 ((pieces++))
143 }
generate_piece函数用来生成块
- 139行随机生成块位置
- 140行先判断生成的块的位置是否有非零值,如果没有的话就生成2或4作为该块的值,并跳出循环。
- 142行将当前界面中所有非零格子数加一
push_pieces
155 function push_pieces(){
156 case $4 in
157 "up")
158 let "first=$2*$board_size+$1"
159 let "second=($2+$3)*$board_size+$1"
160 ;;
161 "down")
162 let "first=($index_max-$2)*$board_size+$1"
163 let "second=($index_max-$2-$3)*$board_size+$1"
164 ;;
165 "left")
166 let "first=$1*$board_size+$2"
167 let "second=$1*$board_size+$2+$3"
168 ;;
169 "right")
170 let "first=$1*$board_size+($index_max-$2)"
171 let "second=($1*$board_size)+($index_max-$2-$3)"
172 ;;
173 esac
174 if ((board[$first]))
175 then
176 if ((board[$second]))
177 then
178 let flag_skip=1
179 fi
180 if ((board[$first]==board[$second]))
181 then
182 if [ -z $5 ]
183 then
184 let board[$first]*=2
185 if ((board[$first]==target))
186 then
187 end_game 1
188 fi
189 let board[$second]=0
190 let pieces-=1
191 let change=1
192 let score+=${board[$first]}
193 else
194 let moves++
195 fi
196 fi
197 else
198 if ((board[$second]))
199 then
200 if [ -z $5 ]
201 then
202 let board[$first]=${board[$second]}
203 let board[$second]=0
204 let change=1
205 else
206 let moves++
207 fi
208 fi
209 fi
210 }
push_pieces函数处理游戏操作,即接受到上下左右的操作信号时,处理格子内的数值。
它接受五个参数,参数1、2、3用来确定first及second的位置,参数4用来确定上下左右的操作信号,参数5用来确认是否只是测试而不改变游戏状态。
它的基本逻辑是,first格子作为容器存放处理后的数值,second格子则提供值等待处理。
-
174行判断first内是否存在非零值,如果存在非零值的话:
- 176至179行判断如果second存在非零值的话,就只处理这一次,防止重复处理。
- 180至196行判断,如果first和second内的值相同的话,就相加,并把结果存在first内。
-
如果first内不存在非零值的话:
- 189至209行判断,如果second内存在非零值,则将其移到first内。
-
182及200行,配合check_moves函数测试是否还有可以进行的操作,不改变当前游戏状态。
apply_push
212 function apply_push(){
213 for((i=0;i<=index_max;i++))
214 do
215 for((j=0;j<=index_max;j++))
216 do
217 let flag_skip=0
218 let increment_max=index_max-j
219 for((k=1;k<=increment_max;k++))
220 do
221 if ((flag_skip))
222 then
223 break
224 fi
225 push_pieces $i $j $k $1 $2
226 done
227 done
228 done
229 }
apply_push函数遍历整个界面并调用push_pieces函数处理游戏操作。
- 上下操作时,i代表列,j代表行。向上时,它的逻辑是从上到下,从左到右。向下时,它的逻辑是从下到上,从左到右。
- 左右操作时,i代表行,j代表列。向左时,它的逻辑是从左到右,从上到下。向右时,它的逻辑是从右到左,从上到下。
check_moves
230 function check_moves(){
231 let moves=0
232 apply_push "up" fake
233 apply_push "down" fake
234 apply_push "left" fake
235 apply_push "right" fake
}
check_moves函数用来测试是否还有可以进行的操作,如果没有的话,游戏失败。
key_react
237 function key_react(){
238 let change=0
239 read -d '' -sn 1
240 if [ "$REPLY" = "$ESC" ]
241 then
242 read -d '' -sn 1
243 if [ "$REPLY" = "[" ]
244 then
245 read -d '' -sn 1
246 case $REPLY in
247 A)
248 apply_push up;;
249 B)
250 apply_push down;;
251 C)
252 apply_push right;;
253 D)
254 apply_push left;;
255 esac
256 fi
257 else
258 case $REPLY in
259 k)
260 apply_push up;;
261 j)
262 apply_push down;;
263 h)
264 apply_push left;;
265 l)
266 apply_push right;;
267
268 w)
269 apply_push up;;
270 s)
271 apply_push down;;
272 a)
273 apply_push left;;
274 d)
275 apply_push right;;
276 esac
277 fi
278 }
key_react函数用来读取键盘操作,并调用apply_push进行相应处理。
例如↑键,它的控制码是\e[A,239、242、245行分别读取\e
[
A
,并在接下来进行判断。同时258至276行表示,也接受kjhl
和wsad
作为上下左右。
save_game和reload_game
280 function save_game(){
281 rm -rf "$config_dir"
282 mkdir -p "$config_dir"
283 echo "${board[*]}">"$config_dir/board"
284 echo "$board_size">"$config_dir/board_size"
285 echo "$pieces">"$config_dir/pieces"
286 echo "$target">"$config_dir/target"
287 echo "$score">"$config_dir/score"
288 echo "$first_round">"$config_dir/first_round"
289 }
290
291 function reload_game(){
292 if [ ! -d "$config_dir" ]
293 then
294 return
295 else
296 board=(`cat "$config_dir/board"`)
297 board_size=`cat "$config_dir/board_size"`
298 pieces=`cat "$config_dir/pieces"`
299 target=`cat "$config_dir/target"`
300 score=`cat "$config_dir/score"`
301 first_round=`cat "$config_dir/first_round"`
302 let fields_total=board_size**2
303 let index_max=board_size-1
304 fi
305 }
这两个函数分别利用数据流重定向生成存储文件,和从文件中读取数据以载入上次游戏,其他没什么好说的了。
end_game
312 function end_game(){
313 stty echo
314 end_time=`date +%s`
315 let total_time=end_time-start_time
316 duration=`date -u -d @${total_time} +%T`
317 print_board
318 printf "Your score: $score\n"
319 printf "Your game lasted $duration.\n"
320 if (($1))
321 then
322 printf "Congratulations you have achieved $target!\n"
323 exit 0
324 fi
325 if [ ! -z $2 ]
326 then
327 read -n1 -p "Do you want to overwrite saved game?[Y|N]: "
328 if [ "$REPLY" = "Y" ]||[ "$REPLY" = "y" ]
329 then
330 save_game
331 printf "\nGame saved! Use -r option next to load this game.\n"
332 exit 0
333 else
334 printf "\nGame not saved!\n"
335 exit 0
336 fi
337 fi
338 printf "\nYou have lost, better luck next time.\n"
339 exit 0
340 }
end_game函数处理游戏的结束。
- 320至324行,它接受一个非零的参数1来表示游戏达成目标。
- 325至337行,即表示在使用
ctrl+c
主动结束游戏的情况下,询问是否存储。 - 338、339行,表示游戏失败。
344 while getopts ":b:t:l:rhv" opt
345 do
346 case $opt in
347 b)
348 let board_size="$OPTARG"
349 let '(board_size>=3)&(board_size<=9)'||{ printf "Invalid board size, please choose size between 3 and 9\n";exit 1;}
350 ;;
351 t)
352 let target="$OPTARG"
353 printf "obase=2;$target\n"|bc|grep -e '^1[^1]*$'
354 let $? && { printf "Invalid target, have to be power of two\n";exit 1;}
355 ;;
356 l)
357 echo "This function have not be implement."
358 exit 0
359 ;;
360 r)
361 let reload_flag=1
362 ;;
363 h)
364 help $0
365 exit 0
366 ;;
367 v)
368 version
369 exit 0
370 ;;
371 \?)
372 printf "Invalid option -$opt, please $0 -h\n">&2
373 exit 1
374 ;;
375 :)
376 printf "Option -$opt requires an argument, please $0 -h\n">&2
377 exit 1
378 ;;
379 esac
380 done
334至380行,即利用getopts处理相关选项,-l我并没有实现,主要是懒的。至于help和version函数没什么好说的。353行,利用grep的返回值测试-t选项所指定的target是否符合2的幂。
382 let index_max=board_size-1
383 let fields_total=board_size**2
384
385 for((index=0;index<fields_total;index++))
386 do
387 let board[$index]=0
388 done
389 generate_piece
390 let first_round=$last_added
391 generate_piece
392 if ((reload_flag))
393 then
394 reload_game
395 fi
396
397 while true
398 do
399 print_board
400 key_react
401 let change&&generate_piece
402 let first_round=-1
403 if ((pieces==fields_total))
404 then
405 check_moves
406 if ((moves==0))
407 then
408 end_game 0
409 fi
410 fi
411 done
- 385至388行初始化board数组。
- 389至391行,产生第一回合的两个块。
- 392至395行,判断是否载入上次存储的游戏。
- 403至410行,判断游戏是否失败。
完整脚本
1 #!/bin/bash
2
3
4 #help information
5 function help(){
6 cat <<EOF
7 --------------------------------------------------------------------------------------------------
8 Usage: $1 [-b INTEGER] [-t INTEGER] [-l FILE] [-r] [-h] [-v]
9
10 -b INTEGER -- specify game board size (sizes 3-9 allowed)
11 -t INTEGER -- specify target score to win (needs to be power of 2)
12 -l FILE -- logged debug information to specified file
13 -r -- reload the previous game
14 -h -- help information
15 -v -- version information
16 ---------------------------------------------------------------------------------------------------
17 EOF
18 }
19
20 #version information
21 function version(){
22 cat <<EOF
23 ----------------------------------------------------------------------------------------------------
24 Name: bash2048
25 Version: 1.00
26 Author: goddog312
27 ----------------------------------------------------------------------------------------------------
28 EOF
29 }
30 ###########################
31 #some important variables##
32 ###########################
33 declare -ia board #this array keep all values for each piece on the board
34 declare -i pieces=0 #number of pieces present on board
35 declare -i score=0 #store the current score
36 declare -i flag_skip #flag that prevents doing more than one operation on single field in one step
37 declare -i moves #store number of possible moves to determine are you lost the game or not
38 declare ESC=$'\e' #escape byte
39 declare header="Bash 2048 v1.0" #print on the top of screen
40
41 #start time of the program
42 declare -i start_time=$(date +%s)
43
44
45 #############################################
46 #default config, some can modify by options##
47 #############################################
48 declare -i board_size=4
49 declare -i target=2048
50 declare -i reload_flag=0
51 declare config_dir="$HOME/.bash2048"
52
53
54 ################################
55 ##temp variables for once game##
56 ################################
57 declare last_added #the piece latest generated
58 declare first_round #the piece that generate in the first round
59 declare -i index_max=$[$board_size-1]
60 declare -i fields_total=$[$board_size*$board_size]
61
62 ########################
63 #for colorizing number##
64 ########################
65 declare -a colors
66 colors[2]=32 #green text
67 colors[4]=34 #blue text
68 colors[8]=33 #yellow text
69 colors[16]=36 #cyan text
70 colors[32]=35 #purple text
71
72 colors[64]="1;47;32" #white background green text
73 colors[128]="1;47;34" #white background bule text
74 colors[256]="1;47;33" #white background yellow text
75 colors[512]="1;47;36" #white background cyan text
76 colors[1024]="1;47;35" #white background purple text
77 colors[2048]="1;41;32" #red background green text
78
79
80 trap "end_game 0 1" INT #handle INT signal
81
82 #print current status of the game, the last added piece are red text
83 function print_board(){
84 clear
85 printf "**************************************************************\n"
86 printf "***********************$header*************************\n"
87 printf "*$ESC[1;5;33mpieces=%-11d target=%-11d score=%-12d$ESC[0m*\n" $pieces $target $score
88 printf "**************************************************************\n"
89 echo
90 printf "/------"
91 for((row=1;row<=index_max;row++))
92 do
93 printf "+------"
94 done
95 printf '\\\n'
96 for((row=0;row<=index_max;row++))
97 do
98 printf "|"
99 for((line=0;line<=index_max;line++))
100 do
101 if let ${board[$row*$board_size+$line]}
102 then
103 if let '(last_added==(row*board_size+line))|(first_round==(row*board_size+line))'
104 then
105 printf "$ESC[1;33m %4d $ESC[0m|" ${board[$row*$board_size+$line]}
106 else
107 printf "$ESC[${colors[${board[$row*$board_size+$line]}]}m %4d $ESC[0m|" ${board[$row*$board_size+$line]}
108 fi
109 else
110 printf " |"
111 fi
112 done
113 if ((row!=index_max))
114 then
115 printf "\n|------"
116 for((r=1;r<=index_max;r++))
117 do
118 printf "+------"
119 done
120 printf "|\n"
121 fi
122 done
123 printf '\n\\------'
124 for((row=1;row<=index_max;row++))
125 do
126 printf "+------"
127 done
128 printf "/\n"
129 }
130
131 #generate new piece on board
132 #generate a pos
133 #generate a value in board[pos]
134 #update last_added
135 #update pieces
136 function generate_piece(){
137 while true
138 do
139 ((pos=RANDOM%fields_total))
140 let ${board[$pos]} ||{ let value=RANDOM%10?2:4;board[$pos]=$value;last_added=$pos;break;}
141 done
142 ((pieces++))
143 }
144
145 #perform push operation between two pieces
146 #variables:
147 # $1:push position, for horizontal push is column,for vertical is row
148 # $2:recipient piece, this will store result if moving or join
149 # $3:originator piece, after moving or join this piece will left empty
150 # $4:direction of push, can be either "up" , "donw" , "left" or "right"
151 # $5:if anything was passed, do not perform the push, but only update number of valid moves. Used for function check_moves
152 # $board:the status of the game board
153 # $change:indicates if the board was changed this round
154 # $flag_skip:indicates the recipient piece cannot be modified further
155 function push_pieces(){
156 case $4 in
157 "up")
158 let "first=$2*$board_size+$1"
159 let "second=($2+$3)*$board_size+$1"
160 ;;
161 "down")
162 let "first=($index_max-$2)*$board_size+$1"
163 let "second=($index_max-$2-$3)*$board_size+$1"
164 ;;
165 "left")
166 let "first=$1*$board_size+$2"
167 let "second=$1*$board_size+$2+$3"
168 ;;
169 "right")
170 let "first=$1*$board_size+($index_max-$2)"
171 let "second=($1*$board_size)+($index_max-$2-$3)"
172 ;;
173 esac
174 if ((board[$first]))
175 then
176 if ((board[$second]))
177 then
178 let flag_skip=1
179 fi
180 if ((board[$first]==board[$second]))
181 then
182 if [ -z $5 ]
183 then
184 let board[$first]*=2
185 if ((board[$first]==target))
186 then
187 end_game 1
188 fi
189 let board[$second]=0
190 let pieces-=1
191 let change=1
192 let score+=${board[$first]}
193 else
194 let moves++
195 fi
196 fi
197 else
198 if ((board[$second]))
199 then
200 if [ -z $5 ]
201 then
202 let board[$first]=${board[$second]}
203 let board[$second]=0
204 let change=1
205 else
206 let moves++
207 fi
208 fi
209 fi
210 }
211
212 function apply_push(){
213 for((i=0;i<=index_max;i++))
214 do
215 for((j=0;j<=index_max;j++))
216 do
217 let flag_skip=0
218 let increment_max=index_max-j
219 for((k=1;k<=increment_max;k++))
220 do
221 if ((flag_skip))
222 then
223 break
224 fi
225 push_pieces $i $j $k $1 $2
226 done
227 done
228 done
229 }
230 function check_moves(){
231 let moves=0
232 apply_push "up" fake
233 apply_push "down" fake
234 apply_push "left" fake
235 apply_push "right" fake
236 }
237 function key_react(){
238 let change=0
239 read -d '' -sn 1
240 if [ "$REPLY" = "$ESC" ]
241 then
242 read -d '' -sn 1
243 if [ "$REPLY" = "[" ]
244 then
245 read -d '' -sn 1
246 case $REPLY in
247 A)
248 apply_push up;;
249 B)
250 apply_push down;;
251 C)
252 apply_push right;;
253 D)
254 apply_push left;;
255 esac
256 fi
257 else
258 case $REPLY in
259 k)
260 apply_push up;;
261 j)
262 apply_push down;;
263 h)
264 apply_push left;;
265 l)
266 apply_push right;;
267
268 w)
269 apply_push up;;
270 s)
271 apply_push down;;
272 a)
273 apply_push left;;
274 d)
275 apply_push right;;
276 esac
277 fi
278 }
279
280 function save_game(){
281 rm -rf "$config_dir"
282 mkdir -p "$config_dir"
283 echo "${board[*]}">"$config_dir/board"
284 echo "$board_size">"$config_dir/board_size"
285 echo "$pieces">"$config_dir/pieces"
286 echo "$target">"$config_dir/target"
287 echo "$score">"$config_dir/score"
288 echo "$first_round">"$config_dir/first_round"
289 }
290
291 function reload_game(){
292 if [ ! -d "$config_dir" ]
293 then
294 return
295 else
296 board=(`cat "$config_dir/board"`)
297 board_size=`cat "$config_dir/board_size"`
298 pieces=`cat "$config_dir/pieces"`
299 target=`cat "$config_dir/target"`
300 score=`cat "$config_dir/score"`
301 first_round=`cat "$config_dir/first_round"`
302 let fields_total=board_size**2
303 let index_max=board_size-1
304 fi
305 }
306
307
308 #print game duration
309 #print total score
310 #print end or achieve information
311 #choose save game or not
312 function end_game(){
313 stty echo
314 end_time=`date +%s`
315 let total_time=end_time-start_time
316 duration=`date -u -d @${total_time} +%T`
317 print_board
318 printf "Your score: $score\n"
319 printf "Your game lasted $duration.\n"
320 if (($1))
321 then
322 printf "Congratulations you have achieved $target!\n"
323 exit 0
324 fi
325 if [ ! -z $2 ]
326 then
327 read -n1 -p "Do you want to overwrite saved game?[Y|N]: "
328 if [ "$REPLY" = "Y" ]||[ "$REPLY" = "y" ]
329 then
330 save_game
331 printf "\nGame saved! Use -r option next to load this game.\n"
332 exit 0
333 else
334 printf "\nGame not saved!\n"
335 exit 0
336 fi
337 fi
338 printf "\nYou have lost, better luck next time.\n"
339 exit 0
340 }
341
342
343 #parse command line options
344 while getopts ":b:t:l:rhv" opt
345 do
346 case $opt in
347 b)
348 let board_size="$OPTARG"
349 let '(board_size>=3)&(board_size<=9)'||{ printf "Invalid board size, please choose size between 3 and 9\n";exit 1;}
350 ;;
351 t)
352 let target="$OPTARG"
353 printf "obase=2;$target\n"|bc|grep -e '^1[^1]*$'
354 let $? && { printf "Invalid target, have to be power of two\n";exit 1;}
355 ;;
356 l)
357 echo "This function have not be implement."
358 exit 0
359 ;;
360 r)
361 let reload_flag=1
362 ;;
363 h)
364 help $0
365 exit 0
366 ;;
367 v)
368 version
369 exit 0
370 ;;
371 \?)
372 printf "Invalid option -$opt, please $0 -h\n">&2
373 exit 1
374 ;;
375 :)
376 printf "Option -$opt requires an argument, please $0 -h\n">&2
377 exit 1
378 ;;
379 esac
380 done
381
382 let index_max=board_size-1
383 let fields_total=board_size**2
384
385 for((index=0;index<fields_total;index++))
386 do
387 let board[$index]=0
388 done
389 generate_piece
390 let first_round=$last_added
391 generate_piece
392 if ((reload_flag))
393 then
394 reload_game
395 fi
396
397 while true
398 do
399 print_board
400 key_react
401 let change&&generate_piece
402 let first_round=-1
403 if ((pieces==fields_total))
404 then
405 check_moves
406 if ((moves==0))
407 then
408 end_game 0
409 fi
410 fi
411 done