写在前面
今天找时间把LC-3的Nim游戏(实验四、五)给做了,浅浅地分享一下吧。
一、游戏背景和规则
背景:Nim是一个简单的双人游戏,可能起源于中国。游戏中使用的计数器类型有很多种类,如石头、火柴、苹果等。游戏界面被划分为很多行,每行中有数量不等的计数器,如表所示:
行号 | 计数器数量 |
---|---|
1 | ○○○ |
2 | ○○○○○○ |
··· | ··· |
n | ○○○○○○○○○ |
本次实验对Nim游戏做了一些小的改变,具体如下:
游戏界面由三行组成,计数器类型为石头,其中A行包含3个石头,B行包含5个石头,C行包含8个石头。
规则如下:
⑴ 每个玩家轮流从某一行中移除一个或多个石头。
⑵ 一个玩家不能在一个回合中从多个行中移除石头。
⑶ 当某个玩家从游戏界面上移除最后剩余的石头时,此时游戏结束,该玩家获胜。
二、实验要求
⑴ 在游戏开始时,你应该显示游戏界面的初始化状态。具体包括:在每行石头的前面,你应该先输出行的名称,例如“ROW A”。你应该使用ASCII字符小写字母“o”(ASCII码 x006F)来表示石头。游戏界面的初始化状态应该如下:
ROW A: ooo
ROW B: ooooo
ROW C: oooooooo
⑵ 游戏总是从玩家1先开始,之后玩家1和玩家2轮流进行。在每一个回合开始时,你应该输出轮到哪一个玩家开始,并提示玩家进行操作。例如,对于玩家1,应该有如下显示:
Player 1,choose a row and number of rocks:
⑶ 为了指定要移除哪一行中的多少石头,玩家应该输入一个字母后跟一个数字(输入结束后不需要按Enter键),其中字母(A,B或C)指定行,数字(从1到所选行中石头的数量)指定要移除的石头的数量。你的程序必须要确保玩家从有效的行中移除有效数量的石头,如果玩家输入无效,你应该输出错误提示信息并提示该玩家再次进行输入。例如,如果轮到玩家1:
Player 1, choose a row and number of rocks: D4
Invalid move. Try again.
Player 1, choose a row and number of rocks: A9
Invalid move. Try again.
Player 1, choose a row and number of rocks: A*
Invalid move. Try again.
Player 1, choose a row and number of rocks: &4
Invalid move. Try again.
Player 1, choose a row and number of rocks:
你的程序应保持提示玩家,直到玩家选择有效的输入为止。确保你的程序能够回显玩家的输入到屏幕上,当回显玩家的输入后,此时应该输出一个换行符(ASCII码x000A)使光标指向下一行。
⑷ 玩家选择有效的输入后,你应该检查获胜者。如果有一个玩家获胜,你应该显示相应的输出来表明该玩家获胜。如果没有胜利者,你的程序应该更新游戏界面中每行石头的数量,重新显示更新的游戏界面,并轮到下一个玩家继续。
⑸ 当某个玩家从游戏界面上移除最后的石头时,游戏结束。此时,你的程序应该显示获胜者然后停止。例如,如果玩家2移除了最后的石头,你的程序应该输出一下内容:
Player 2 Wins.
三、样例输入/输出
据说要和样例输出一模一样
ROW A: ooo
ROW B: ooooo
ROW C: oooooooo
Player 1, choose a row and number of rocks: B2
ROW A: ooo
ROW B: ooo
ROW C: oooooooo
Player 2, choose a row and number of rocks: A1
ROW A: oo
ROW B: ooo
ROW C: oooooooo
Player 1, choose a row and number of rocks: C6
ROW A: oo
ROW B: ooo
ROW C: oo
Player 2, choose a row and number of rocks: G1
Invalid move. Try again.
Player 2, choose a row and number of rocks: B3
ROW A: oo
ROW B:
ROW C: oo
Player 1, choose a row and number of rocks: A3
Invalid move. Try again.
Player 1, choose a row and number of rocks: C2
ROW A: oo
ROW B:
ROW C:
Player 2, choose a row and number of rocks: A1
ROW A: o
ROW B:
ROW C:
Player 1, choose a row and number of rocks: A*
Invalid move. Try again.
Player 1, choose a row and number of rocks: &4
Invalid move. Try again.
Player 1, choose a row and number of rocks: A1
Player 1 Wins.
----- Halting the processor -----
四、代码编写
数据存放
1、程序从x3000开始存放
2、每行石头的数量从x3200开始存放,且以-1结尾作为结束标志,以支持非题目要求的D~Z行(属于是自找麻烦了)
编写代码
注意:
以下代码片段并非代码实际顺序。
并且子程序没有严格地保存和恢复寄存器状态,这是不对滴。
1、石头状态输出子程序:
;石头状态输出
;R1-石头数量地址,R2-数量
FUNC_DRAW ST R7,SAVER7 ;保存R7的值,因为PUTS和OUT会对其进行修改
LD R1,PTR_STONES ;石头堆首地址
LD R3,CHAR_ROWBASE ;'A'的ASCII码
;遍历石头堆
LOOP_1 LDR R2,R1,#0 ;读取当前行石头数量
BRn END_DRAW ;若数字为-1,结束输出
LEA R0,STR_ROW
PUTS
AND R0,R0,#0
ADD R0,R3,#0
OUT
LEA R0,STR_ROW2
PUTS
LD R0,CHAR_SYMBOL ;
ADD R2,R2,#0 ;将CC恢复为R2
;循环输出o
LOOP_2 BRz OUT_2 ;若数字R2降到0,结束当前循环
OUT
ADD R2,R2,#-1
BRnzp LOOP_2
OUT_2 LD R0,CHAR_LF
OUT
ADD R1,R1,#1
ADD R3,R3,#1
BRnzp LOOP_1
END_DRAW LD R7,SAVER7
RET
2、检查石头是否被取完:
;检查石头是否被取完了
;Output(R6):没取完(0),取完了(1)
FUN_CHECK LD R1, PTR_STONES ;R1指向石头
AND R6, R6, #0
ADD R6, R6, #1 ;初始化R6为1
SUB_CHECK_LOOP LDR R2, R1, #0 ;读取当前行石头数
BRp SUB_CHECK_FALSE ;任意行非零则返回0
BRn SUB_CHECK_END ;遇到-1,说明石头数全为0,返回1
ADD R1, R1, #1
BRnzp SUB_CHECK_LOOP
SUB_CHECK_FALSE ADD R6, R6, #-1 ;R6置为0
SUB_CHECK_END RET
3、判断当前输入的合法性并取走石头:
;判断当前的输入是否还有剩余,若无剩余输出错误信息
;Input(PTR_ROW,PTR_CNT) Output(R6):非法输入(0),取走前一切正常(n)
FUN_HASLEFT ST R7, SAVER7 ;保存R7,因为PUTS
LD R1, PTR_STONES ;R1指向首堆石头
LD R2, PTR_ROW
LD R3, NCHAR_A
ADD R2, R2, R3 ;行距离A的偏移
BRn SUB_HAS_ERROR ;行号非法
ADD R1, R1, R2 ;指向输入指定的行
AND R6, R6, #0
LDR R2, R1, #0
BRnz SUB_HAS_ERROR ;剩下的石头<=0,可同时判断行号非法和当前行为0
ADD R6, R6, R2 ;R6 = R2
SUB_HAS_SUB LD R2, PTR_CNT ;读取输入的数字
LD R3, NCHAR_0
ADD R2, R2, R3 ;ASCII -> num
BRnz SUB_HAS_ERROR ;数字非法
NOT R2, R2
ADD R2, R2, #1 ;R2 = -R2
ADD R4, R6, R2 ;R4 = 该行剩余 - 取走数量
BRn SUB_HAS_ERROR ;该行剩下的石头不够
STR R4, R1, #0 ;更新石头数量
BRnzp SUB_HAS_END
SUB_HAS_ERROR AND R6, R6, #0 ;需要返回的0
LD R0, CHAR_LF
OUT
LEA R0, STR_INVALID
PUTS
LD R0, CHAR_LF
OUT
SUB_HAS_END LD R7, SAVER7 ;载入R7
RET
4、行号及数量输入:
;输入行号(ABCDEFGHIJKLMNOPQRSTUVWXYZ)
INPUT_ROW ST R7, SAVER7
GETC
OUT
ST R0, PTR_ROW
LD R7, SAVER7
RET
;输入个数(123456789)
INPUT_CNT ST R7, SAVER7
GETC
OUT
ST R0, PTR_CNT
LD R7, SAVER7
RET
;此处用PTR命名是因为原来是打算用LDI和STI的,后来改用LD和ST懒得改了哈哈哈
PTR_ROW .BLKW 1 ;储存输入的行号(ASCII码A~Z)
PTR_CNT .BLKW 1 ;储存输入的数字(ASCII码0~9)
PTR_PLAYER .FILL x0000 ;储存当前玩家,0为P1,1为P2
5、主程序及玩家控制:
;主程序
;FUNC_MAIN
LOOP_PLAY JSR FUNC_DRAW ;开局先输出一次石堆
LD R6, PTR_PLAYER
BRz FUNC_P1 ;为0就是玩家1
BRp FUNC_P2 ;为1就是玩家2
PLAY_AFTER LD R0, CHAR_LF
OUT ;输出换行
JSR FUNC_CHANGE ;切换玩家
BRnzp LOOP_PLAY ;循环就是了,跳出在玩家控制中
;玩家1控制
FUNC_P1 LEA R0, STR_PLAYER1 ;输出游戏提示
PUTS
JSR INPUT_ROW ;输入行号
JSR INPUT_CNT ;输入数量
JSR FUN_HASLEFT ;进行一个的判断和取石头
ADD R6, R6, #0 ;刷新CC
BRnz FUNC_P1 ;非法输入,再来
LD R0, CHAR_LF
OUT ;为了让输出和样例一模一样
JSR FUN_CHECK
ADD R6, R6, #0 ;刷新CC
BRnz PLAY_AFTER ;R6=0,还没赢
LD R0, CHAR_LF ;R6=1,赢了
OUT
LEA R0, STR_WIN_1
PUTS
BRnzp _END
;玩家2控制(结构同玩家1)
FUNC_P2 LEA R0, STR_PLAYER2
PUTS
JSR INPUT_ROW ;输入行号
JSR INPUT_CNT ;输入数量
JSR FUN_HASLEFT ;进行一个的判断和取石头
ADD R6, R6, #0 ;刷新CC
BRnz FUNC_P2 ;非法输入,再来
LD R0, CHAR_LF
OUT ;为了让输出和样例一模一样
JSR FUN_CHECK
ADD R6, R6, #0 ;刷新CC
BRnz PLAY_AFTER ;R6=0,还没赢
LD R0, CHAR_LF ;R6=1,赢了
OUT
LEA R0, STR_WIN_2
PUTS
BRnzp _END
;玩家切换子程序(没有用到TRAP故)
FUNC_CHANGE LD R6, PTR_PLAYER
BRz SUB_ZERO
ADD R6, R6, #-1 ;如果是1就切0
ST R6, PTR_PLAYER
BRnzp SUB_CHANGE_RET
SUB_ZERO ADD R6, R6, #1 ;如果是0就切1
ST R6, PTR_PLAYER
SUB_CHANGE_RET RET
完整代码如下(仅供参考,建议独立思考,所以我删掉了完整代码中的很多注释哈哈哈哈):
.ORIG x3000
;主程序
;FUNC_MAIN
LOOP_PLAY JSR FUNC_DRAW
LD R6, PTR_PLAYER
BRz FUNC_P1
BRp FUNC_P2
PLAY_AFTER LD R0, CHAR_LF
OUT ;LF
JSR FUNC_CHANGE
BRnzp LOOP_PLAY
;玩家1
FUNC_P1 LEA R0, STR_PLAYER1
PUTS
JSR INPUT_ROW
JSR INPUT_CNT
JSR FUN_HASLEFT
ADD R6, R6, #0
BRnz FUNC_P1
LD R0, CHAR_LF
OUT
JSR FUN_CHECK
ADD R6, R6, #0
BRnz PLAY_AFTER
LD R0, CHAR_LF
OUT
LEA R0, STR_WIN_1
PUTS
BRnzp _END
;玩家2
FUNC_P2 LEA R0, STR_PLAYER2
PUTS
JSR INPUT_ROW
JSR INPUT_CNT
JSR FUN_HASLEFT
ADD R6, R6, #0
BRnz FUNC_P2
LD R0, CHAR_LF
OUT
JSR FUN_CHECK
ADD R6, R6, #0
BRnz PLAY_AFTER
LD R0, CHAR_LF
OUT
LEA R0, STR_WIN_2
PUTS
BRnzp _END
;玩家切换子程序
FUNC_CHANGE LD R6, PTR_PLAYER
BRz SUB_ZERO
ADD R6, R6, #-1
ST R6, PTR_PLAYER
BRnzp SUB_CHANGE_RET
SUB_ZERO ADD R6, R6, #1
ST R6, PTR_PLAYER
SUB_CHANGE_RET RET
;输入行号(ABCDEFGHIJKLMNOPQRSTUVWXYZ)
INPUT_ROW ST R7, SAVER7
GETC
OUT
ST R0, PTR_ROW
LD R7, SAVER7
RET
;输入个数
INPUT_CNT ST R7, SAVER7
GETC
OUT
ST R0, PTR_CNT
LD R7, SAVER7
RET
PTR_ROW .BLKW 1
PTR_CNT .BLKW 1
PTR_PLAYER .FILL x0000
;石头状态输出
;R1-石头数量地址,R2-数量
FUNC_DRAW ST R7,SAVER7
LD R1,PTR_STONES
LD R3,CHAR_ROWBASE
;遍历石头堆
LOOP_1 LDR R2,R1,#0
BRn END_DRAW
LEA R0,STR_ROW
PUTS
AND R0,R0,#0
ADD R0,R3,#0
OUT
LEA R0,STR_ROW2
PUTS
LD R0,CHAR_SYMBOL
ADD R2,R2,#0
;循环输出o
LOOP_2 BRz OUT_2
OUT
ADD R2,R2,#-1
BRnzp LOOP_2
OUT_2 LD R0,CHAR_LF
OUT
ADD R1,R1,#1
ADD R3,R3,#1
BRnzp LOOP_1
END_DRAW LD R7,SAVER7
RET
;检查石头是否完了
;Output(R6):false(0),true(1)
FUN_CHECK LD R1, PTR_STONES
AND R6, R6, #0
ADD R6, R6, #1
SUB_CHECK_LOOP LDR R2, R1, #0
BRp SUB_CHECK_FALSE
BRn SUB_CHECK_END
ADD R1, R1, #1
BRnzp SUB_CHECK_LOOP
SUB_CHECK_FALSE ADD R6, R6, #-1
SUB_CHECK_END RET
;判断当前的输入是否还有剩余,若无剩余输出错误信息
;Input(x5001) Output(R6):false(0),p-left(n)
FUN_HASLEFT ST R7, SAVER7
LD R1, PTR_STONES
LD R2, PTR_ROW
LD R3, NCHAR_A
ADD R2, R2, R3
BRn SUB_HAS_ERROR
ADD R1, R1, R2
AND R6, R6, #0
LDR R2, R1, #0
BRnz SUB_HAS_ERROR
ADD R6, R6, R2
SUB_HAS_SUB LD R2, PTR_CNT
LD R3, NCHAR_0
ADD R2, R2, R3
BRnz SUB_HAS_ERROR
NOT R2, R2
ADD R2, R2, #1
ADD R4, R6, R2
BRn SUB_HAS_ERROR
STR R4, R1, #0
BRnzp SUB_HAS_END
SUB_HAS_ERROR AND R6, R6, #0
LD R0, CHAR_LF
OUT
LEA R0, STR_INVALID
PUTS
LD R0, CHAR_LF
OUT
SUB_HAS_END LD R7, SAVER7
RET
_END HALT
CHAR_ROWBASE .FILL x0041 ;'A'的ASCII码
NCHAR_A .FILL xFFBF ;'A'的负值
NCHAR_0 .FILL xFFD0 ;'0'的负值
CHAR_SYMBOL .FILL x006f ;'o'的ASCII码
CHAR_LF .FILL x000A ;换行符的ASCII
NCHAR_LF .FILL xFFF6 ;LF的负值
PTR_STONES .FILL x3200 ;石头数量信息储存起点
STR_ROW .STRINGZ "ROW "
STR_ROW2 .STRINGZ ": "
STR_PLAYER1 .STRINGZ "Player 1, choose a row and number of rocks:"
STR_PLAYER2 .STRINGZ "Player 2, choose a row and number of rocks:"
STR_WIN_1 .STRINGZ "Player 1 Wins."
STR_WIN_2 .STRINGZ "Player 2 Wins."
STR_INVALID .STRINGZ "Invalid move. Try again."
SAVER7 .BLKW 1 ;保存R7
.END
五、小小总结
这次的实验长度(难度)肯定是比上次要翻倍的,不过有了子程序思想的加持,以及对LC-3汇编指令理解的加深,感觉也不是特别的难。
以上代码可能仍存在一些问题,但毕竟只是个实验嘛,凑合着做做(逃)~