IA32汇编语言 —— 贪吃蛇游戏

  • 这里分享一下我的汇编语言课程设计,贪吃蛇游戏
  • 程序使用的资源不超过8086,可以用nasm编译成.com文件,运行在DOSBox环境中
  • 效果演示

一、简介

1. 游戏规则

  • wasd控制转向
  • r重启游戏
  • esc退出游戏
  • p暂停游戏
  • F1~F10控制蛇的十级变速

注:wasd下文统称方向键;其他键统称控制键

2. 段寄存器安排

  • 整体上不分段,ssdsescs处于同一个段
  • 设置一个128字节的全局变量作为堆栈区,ss指向这个区域(用于push/pop指令)
  • dses都初始化为cs的值,使它们指向同一个段
  • ds主要以超越前缀来访问屏幕显示缓冲区,也可以缺省前缀形式访问全局变量
  • es主要以超越前缀来访问全局变量,并且以超越前缀形式访问中断向量表

3. 流程图

  1. 主程序流程,主要包括以下几部分:

    1. 初始化:初始化各寄存器、全局变量、显示初始游戏界面、修改中断向量表等
    2. 控制键处理循环:从按键缓冲队列取值,判断控制键、设置全局标志和变量等
    3. 死亡界面循环:显示死亡界面,判断r和esc键
    4. 收尾:还原中断向量表、返回操作系统
      在这里插入图片描述
  2. 按键中断
    在这里插入图片描述

  3. 定时器中断
    在这里插入图片描述

3. 关键技术

  • 屏幕显示相关
  • 键盘控制相关
  • 食物随机生成相关
  • 蛇移动刷新相关

二、原理说明

1. 屏幕显示

  • 使用直接写屏的方式来做界面显示。在内存中有一个显示缓存区,可以用访问普通内存的方法访问它。这是一片4000字节的连续区域,存储着25行80列显示区域中每个点的显示数据,每个点数据长两个字节,低字节是显示字符的ASCII码;高字节是底色和字的颜色,格式如下
    在这里插入图片描述
  • 显卡会不断扫描这个区域,并把写入其中的数据立刻显示到屏幕上。注意,在这个区域写入的数据,在刷新到屏幕上后不会消失,也就是说:任意时刻,只要读取屏幕显示区,就能得知屏幕上任意一点显示的字符,这对我们后续游戏逻辑有着重要作用
  • 显示区域的起始地是0B800H,第m行的初始位置的偏移地址为80*m*2,低m行n列位置对应的存储单元偏移是(80*m+n)*2,示意图如下:
    在这里插入图片描述
  • 为了统一和方便起见,在程序中读写屏上点(x,y)时,统一把ds指向x行起始,用bx指示y对应的偏移,用byte [ds:bx] 和byte [ds:bx+1]取得数据位置

2. 显示刷新

(1)定时器中断

  • 贪吃蛇是不断在运动的,如何让屏幕显示的画面动起来呢?我的方法是利用系统定时器来刷新显示。在系统加电初始化期间,系统定时器被设定为每55毫秒发出一次中断请求,cpu的int 8中断响应它后,会调用一个“接口中断”1ch。这个中断每秒被触发18.2次,但预置的中断服务函数没有做任何事,换句话说,1ch中断就是一个给开发者预留的接口,方面开发者实现某些周期性工作,而我们正好可以利用它。
  • 在中断服务函数中做一个计数器,每次进中断都减计数,在计数器位为0时进行计数值重装载和功能函数调用,这样我们就得到了一个可以自由控制周期长度的周期性触发函数。如果要暂停也很简单,设置一个暂停标志跳过减计数即可。这也是单片机定时器中断使用过程中很常用的方法了,可以把一个周期信号变成多个。我们可以在这个函数中进行蛇显示位置的刷新,这样就能通过控制计数器装在值来控制蛇的前进速度

(2)蛇刷新逻辑

  • 先说明一下游戏显示方式:蛇头用亮紫色 *表示,蛇身用浅紫色 *表示,食物用浅蓝闪烁 #表示,游戏边框用*表示,如下图所示

    在这里插入图片描述

  • 蛇每一帧的移动,在视觉上起始只有蛇头和蛇尾在移动,我们只要存一下当前时刻蛇头的位置&前进方向 + 蛇尾的位置&前进方向就行了。每轮刷新时,只刷新显示新的蛇头,并把旧尾消除,蛇身其他部分不动。先确定头尾的前进方向,用此结合当前的位置得到更新的位置,判断更新的位置会不会死,不死就显示新的蛇头出来尾巴也差不多,先在原位置显示一个空格把旧尾刷掉,再修改蛇尾位置数据即可如果蛇头吃到食物的话,这一轮头动尾不动就行了。蛇头蛇尾存储结构示意如下:

%define getXY(x,y) (x<<8) | y		;宏函数,屏幕上坐标(x,y)拼成一个字,xy必须是常数


snake_head dw getXY(7,8), 'd'		;存储蛇头位置和下一格移动方向(hdir),缺省为(7 8'd')
snake_tail dw getXY(5,8), 'd'		;存储蛇尾位置和下一格移动方向(tdir),缺省为(5 8'd'
  • 因为先移动蛇头,再移动蛇尾,要注意一下蛇头和蛇尾恰好擦过的情况。 如果蛇头撞到了非#空格的点,而且现在蛇头和蛇尾不重合,那要么是撞到蛇身,要么是撞墙,这样就可以做死亡判断
  • 还有一个比较麻烦的地方在于:显示刷新频率方向键操作频率(wasd)、控制键操作频率(r p esc…)不同,如果在两次刷新过程中进行了多次方向操作,这些操作不应被忽略,而是应在每次刷新显示时读一个出来。但是控制键(调速、重启、暂停等)要求立刻有反应,所以他们不能和方向键一起存。还有,方向键是直接操作蛇头方向,间接操作蛇尾的方向的,蛇尾方向变化频率比蛇头更慢,要一直走到拐点才能变,所以蛇尾的转向数据也要单独存,为了得到转弯的位置,蛇尾转向数据还要结合转向点坐标一起存才可以。
  • 我借鉴了按键缓冲区(下文提到)的思路,对于蛇头蛇尾的转向数据开了两个循环队列,控制键数据存在按键缓冲队列、方向键数据存入蛇头方向队列,在刷新到蛇头转向点的时候,转向方向结合转向位置一起存入蛇尾方向队列,这样就解决了问题。这是整个游戏里最麻烦的也最核心的部分了。循环队列结构如下:
hdir_buff times 20 dw 0		;蛇头方向(hdir)按键缓冲区(循环队列),每个字数据:高字节是scan code,低字节是ascII(仅wasd四种)
hdir_head dw hdir_buff 	
hdir_tail dw hdir_buff

tdir_buff times 80 dw 0		;蛇尾方向(tdir)按键缓冲区(循环队列),每个双字数据:高字是转向位置,低节是ascII(仅wasd四种),因为蛇尾总是更晚进行转向,这个要开长一点
tdir_head dw tdir_buff 	
tdir_tail dw tdir_buff
  • 三个循环队列整理如下
循环队列名队列元素入队时机出队时机说明
按键缓冲队列每个元素长一个字,高字节是键的扫描码,低字节是ascII码(键包括:r / p / esc / F1~F10)int 9中断服务函数主函数中的while循环改写了bios的int 9中断服务函数,但缓冲区仍是bios中原有的
蛇头方向循环队列同上(键包括:w / a / s / d)int 9中断服务函数蛇刷新中断服务函数中调用的蛇头刷新函数完全模仿按键缓冲队列实现,在数据段自定义缓冲区
蛇尾方向循环队列每个元素长一个双字,高字是蛇头转向位置(高字节x,低字节y);低字是蛇头转向的方向键(高字节空,低字节ascII码)(键包括:w / a / s / d)蛇刷新中断服务函数中调用的蛇头刷新函数蛇刷新中断服务函数中调用的蛇尾刷新函数在数据段自定义缓冲区
  • 蛇头刷新函数说明
    在这里插入图片描述
  • 蛇尾刷新函数说明
    在这里插入图片描述

3. 键盘控制

  • 键盘控制涉及到两个中断,一是int 9h外部中断,这是一个可屏蔽的外部中断,只要键盘有输入,中断信号就通过8259中断控制芯片传入cpu,如果中断允许标志IF置1,就会响应中断信号,执行bios中预置的中断服务函数。在中断函数中,触发键盘的按键扫描码被读入al寄存器中,并被存储到键盘缓冲区键盘缓冲区是一个循环队列,由一个长16个字的buff和两个字指针组成。这个数据结构位于bios数据区,buff地址为0040:001AH~0040:001CH,头指针地址0040:001Ah,尾指针地址0040:001Ch。由于循环队判空的方法是尾指针在头指针前一个位置,所以这个缓冲最多存15个键数据。每个键数据占一个字,高字节是键的扫描码,低字节是ascII码(扫描码和按键对应关系请看:按键扫描码
  • 另一个相关中断是int 16h软中断,这是一个不可屏蔽的指令中断,由用户调用触发。它有三个功能,常用的是0号功能,即从按键缓冲区出队一个键数据到ax中,如果队列为空,就会阻塞等待;我的程序中也用到了1号功能,可以用来查询键盘缓冲区,对键盘扫描但不等待,并设置ZF标志。(关于16h中断详见:键盘I/O中断调用(INT 16H))下图显示了int 9h、int 16h和按键缓冲的关系
    在这里插入图片描述
  • 预置的int9服务函数会处理键盘上所有的键,把他们一视同仁地放入缓冲队列,但这不方便我们对按键操作进行灵活处理,而且我们也不需要检查那么多键,所以我重新自定义了9号中断的服务函数。本质上就是修改了中断向量表,把9号中断服务函数的入口改成我自定义的服务函数,从而实现了自定义。
  • 按照上面的分析,我在自定义的中断服务函数中把设置键存入按键缓冲队列,把方向控制键存入蛇头队列
  • 主函数循环中从按键队列取数,在蛇刷新中断中从蛇头(蛇尾)队列取数。(写到这里,突然感觉设置键也可以不存,而是在int9读取时直接处理,这样还能简化一点)

4. 食物随机生成

  • 每吃到一个食物,都要随机生成一个新食物。这是一个随机过程,需要随机数。但是汇编中没有自带的随机函数,所有我们必须要自己来生成伪随机数。因为有一定的单片机开发经验,我自然地想到了单片机中随机数的生成方法,最简单的是利用自然的噪声,比如做模数转换时,取几个超过精度范围的位作为随机数;另一种方法是利用系统时钟,尤其是小数点后好几位的那种,只要采样周期不是特别一致,基本都挺随机的。
  • 受硬件限制,没法做模数转换,而在食物随机生成这个情境中,显然周期是很不一定的,所以我考虑使用实时钟来实现这个功能。经查资料,我发现的实时钟最精确采样能到百分之一秒,用int 21h中断的2ch功能,返回的CH:CL=时:分DH:DL=秒:1/100秒,基本满足要求。
  • 只有一个百分之一秒还是不太随机,需要结合秒的数据一起处理,我的方法是:设百分之一秒值为y,秒的个位值x,如果x是奇数,伪随机数=(x+1)*y;如果是偶数,伪随机数=(10-x)*y,结果看来是挺随机的
  • 接下来对伪随机数取两个余数就能得到随机食物的位置了,这里还要专门判断一下随机食物的位置是不是随机到蛇身上了,我本来的想法是如果随机到蛇身体就重新取随机数的,但是操作了几次发现,如果取得太快,好像返回的实时钟值就不变了,所以改成了类似哈希的线性散列法:一直往右找直到找到合法位置,当然这样要注意下换行问题。这个方法的好处在于效率比重新随机高,尤其是在蛇很长的时候更高,但坏处在于食物相对容易出现在靠墙的位置。

三、相关代码

1. 全局变量

;--------------------------------------数据区---------------------------------------------------------------
stack times 128 db 0 		;常规操作所用的堆栈
data times 4 dw 0 			;(int 9中断例程ip)(int 9中断例程cs)(int 1ch中断例程ip)(int 1ch中断例程cs)

frame_color db 00001000B	;边框颜色
info_color db 0				;提示信息颜色(由边框颜色确定)
food_color db 10001011b		;食物颜色(闪烁亮蓝色)	

score dw 0   				;得分

speed db 25,20,16,14,10,8,5,3,2,1	;速度对应的计数器周期
spd db '6'					;速度选择

recount db 5				;定时器重装载值
count db 1					;定时器计数值

;hdir看成结构体
hdir_buff times 20 dw 0		;蛇头方向(hdir)按键缓冲区(循环队列),每个字数据:高字节是scan code,低字节是ascII(仅wasd四种)
hdir_head dw hdir_buff 	
hdir_tail dw hdir_buff

;tdir看成结构体
tdir_buff times 80 dw 0		;蛇尾方向(hdir)按键缓冲区(循环队列),每个双字数据:高节是转向位置,低节是ascII(仅wasd四种),因为蛇尾总是更晚进行转向,这个要开长一点
tdir_head dw tdir_buff 	
tdir_tail dw tdir_buff

;snake_head 和snake_tail 看成结构体
snake_head dw getXY(7,8), 'd'		;存储蛇头位置和下一格移动方向(hdir),缺省为(5 8'd')
snake_tail dw getXY(5,8), 'd'		;存储蛇尾位置和下一格移动方向(tdir),缺省为(5 6'd')

get_food_flag db 0					;吃到食物标志
die_flag db 0						;死亡标志
pause_flag db 0						;暂停标志

score_str_pos dw getXY(25,10)		;信息位置
replay_str_pos dw getXY(27,11)		;信息位置
exit_str_pos dw getXY(27,12)				;信息位置
score_str db "game over! your score: ", '$' ;游戏结束显示信息
replay_str db "press R to replay!",'$' 		;游戏结束显示信息
exit_str db "press ESC to exit!",'$' 		;游戏结束显示信息

2. 宏&常数

;----------------------------------------------------------------&常数--------------------------------------------------------------------------
%define esc 1bh					;esc按键的ascii
snake_color EQU  00000101B		;蛇身颜色	
snake_head_color EQU  00001101B	;蛇头颜色	

displayBase EQU  0b800h		;显示区基地址	
PORT_KEY_DAT EQU 0x60		;键盘数据接口
PORT_KEY_STA EQU 0x64		;键盘控制接口

KEYBUFF_DS  	EQU 0040H	;键盘缓冲段地址
KEYBUFF_HEAD 	EQU 001AH	;键盘缓冲头指针偏移地址(0040:001AH)
KEYBUFF_TAIL 	EQU 001CH	;键盘缓冲尾指针偏移地址(0040:001CH)
KEYBUFF_FIRST 	EQU 001EH	;键盘缓冲首地址	(0040:001EH,左闭)
KEYBUFF_LAST 	EQU 003EH	;键盘缓冲尾地址	(0040:003EH,右闭)

%define HDIRBUFF_DS  	es				;hdir缓冲段地址
%define HDIRBUFF_HEAD 	hdir_head		;hdir缓冲头指针偏移地址
%define HDIRBUFF_TAIL 	hdir_tail		;hdir缓冲尾指针偏移地址
%define HDIRBUFF_FIRST 	hdir_buff		;hdir缓冲首地址	
%define HDIRBUFF_LAST  	hdir_buff+2*20	;hdir缓冲尾地址	

%define TDIRBUFF_DS  	es				;tdir缓冲段地址
%define TDIRBUFF_HEAD 	tdir_head		;tdir缓冲头指针偏移地址
%define TDIRBUFF_TAIL 	tdir_tail		;tdir缓冲尾指针偏移地址
%define TDIRBUFF_FIRST 	tdir_buff		;tdir缓冲首地址	
%define TDIRBUFF_LAST  	tdir_buff+2*80	;tdir缓冲尾地址

%define incW(x)   	inc word [es:x]		;宏函数,字x加1
%define clearW(x)  	mov  [es:x],word 0	;宏函数,字x清0
%define setW(x,y)  	mov  [es:x],word y	;宏函数,字x设为y
%define incB(x)   	inc byte [es:x]		;宏函数,字节x加1
%define decB(x)   	dec byte [es:x]		;宏函数,字节x加1
%define clearB(x)  	mov  [es:x],byte 0	;宏函数,字节x清0
%define setB(x,y)  	mov  [es:x],byte y	;宏函数,字节x设为y
%define notB(x) 	NOTB x				;宏函数,bool变量x取反(x只能是01%macro NOTB 1	;宏函数,bool变量取反
	cmp byte [es:%1],0
	jnz ISONE
	setB(%1,1)
	jmp NOTDONE
ISONE:
	clearB(%1)
NOTDONE:
%endmacro

%define setDri(x,y) mov [es:x+2],word y	;宏函数,设置x方向为y(x必须是snake_head或snake_tail)
%define getDir(x)	word [es:x+2]		;宏函数,获取x方向(x必须是snake_head或snake_tail)

%define lineAddr(x) displayBase+x*0ah	;宏函数,获取屏幕x行首地址,x必须是常数
%define getXY(x,y) (x<<8) | y			;宏函数,屏幕上坐标(x,y)拼成一个字,xy必须是常数

%macro showFrames 1		;宏函数,按参数颜色刷新边框
	mov ax,%1	
	push ax	
	call show_all_frames
	add sp,2	;平衡堆栈
%endmacro

%macro setDS 1			;宏函数,设置ds寄存器为参数值
	push ax
	mov ax,%1
	mov ds,ax
	pop ax	
%endmacro

%macro reCount 0 		;宏函数,计数器重装载
	push ax
	mov al,[es:recount]	
	mov [es:count],al
	pop ax	
%endmacro

%macro setAddrHT 1  	;宏函数,设置ds和bx,使[ds:bx]指向某点,参数必须是snake_head 或 snake_tail 或 某字存储空间
	push ax
	push dx
	
	mov bx,[es:%1] 	;bh是列号,bl是行号		
	xor ax,ax		;行号乘0ah找到行首地址偏移
	mov al,bl
	mov dl,0ah
	mul dl

	add ax,displayBase	;加上显示区基地址得到行地址,给ds
	mov ds,ax

	mov bl,bh
	xor bh,bh		;清bh,bx存列号
	shl bx,1		;列号乘2得到点在列中的偏移地址
	
	pop dx
	pop ax
%endmacro

%macro printStr 2  		;宏函数,在%2位置打印字符串%1(参数必须是score_str,score_str_pos 或 replay_str,replay_str_pos)
	push si				;此函数需要手动保存和恢复ds:bx !

	mov si,%1
	setAddrHT %2
	call show_str

	pop si
%endmacro

%macro printDieInfo 0  	;宏函数,打印死亡提示信息
	push ds
	push bx
	printStr score_str, score_str_pos
	mov ax,[es:score]
	call print_num
	printStr replay_str, replay_str_pos
	printStr exit_str, exit_str_pos
	pop bx
	pop ds	
%endmacro

3. 主函数

  • 对应主程序流程图
;----------------------------------------------------------------主函数--------------------------------------------------------------------------
segment code 
    org 100H	
	 
;=================设置段寄存器=========================
	mov ax,cs			;cs,ds一样,不分段
	mov ds, ax		
		
	mov ax,0			;es指向0,准备更改9号中断向量表指向自定义的处理函数(如果es值不对,效果上没啥变化,会调用默认的9号中断服务程序)
	mov es,ax

;=================准备中断向量表=========================
	push word [es:9*4]		;将原来的int 9中断例程的入口地址保存在data段
	pop word  [data]
	push word [es:9*4+2]
	pop word  [data+2]

	push word [es:1ch*4]	;将原来的int 1ch中断例程的入口地址保存在data段
	pop word  [data+4]
	push word [es:1ch*4+2]
	pop word  [data+6]

	CLI
	mov word [es:9*4], int09h_handler	;在中断向量表中设置新的int 9中断例程的入口地址(设置过程关闭中断,避免被打断)
	mov [es:9*4+2], cs
 	mov word [es:1ch*4], int1ch_handler	;在中断向量表中设置新的int 1ch中断例程的入口地址(设置过程关闭中断,避免被打断)
	mov [es:1ch*4+2], cs
	STI	
	
	mov ax,cs		
	mov es,ax			;程序运行过程中用es来寻址全局变量	

	mov ax,[es:stack]
	mov ss,ax			;ss指向数据区的stack
	
;================游戏初始化===============================
game_init:
	;初始化全局变量
	clearW(score)			;得分
	;setB(spd,'6')			;速度选择(注释这些,在重启游戏时速度会保持原设定)
	;setB(recount,5)		;定时器重装载值	
	;setB(count,1)			;定时器计数值
	setW(snake_head,getXY(7,8))	;蛇头信息
	setW(snake_head+2,'d')
	setW(snake_tail,getXY(5,8))	;蛇尾信息
	setW(snake_tail+2,'d')	
	clearB(die_flag)		;复位各标志
	clearB(get_food_flag)
	clearB(pause_flag)	

	;每次重启游戏,改变边框和提示信息的颜色
	incB(frame_color)			;边框frame_color前景色+100001001~00001111循环)
	cmp byte[es:frame_color],00001111b
	jbe frame_color_seted
	setB(frame_color,00001001b)
frame_color_seted:				;提示信息info_color(00001111~00001001循环)	
	mov al,24			
	sub al,[es:frame_color]	
	mov [es:info_color],al

	;初始化窗口	
	call clear_window

	;用frame_color颜色显示所有边框 
	showFrames [es:frame_color]

	;初始化蛇
	call snake_init
	
	;初始化食物
	call draw_food	

	;初始化循环队列
	call Queue_clear


;=============主循环中进行设置键和死亡检测========================
key_and_death:	
	cmp byte [es:die_flag],0	;检查死亡情况
	jnz DIE			;死了,转移

	mov ah, 01h		;查询按键缓冲是否为空
	int 16h
	jnz KEYCHEAK		;不空,检查按键
	jmp key_and_death		;空,循环检查死亡情况
	
KEYCHEAK:
	mov ah,0			;取一个键
	int 16h
R:					;r键,用来重启游戏
	cmp al,'r'
	jnz P
	jmp game_init
P:
	cmp al,'p'
	jnz SPD
	
	notB(pause_flag)
	jmp key_and_death
SPD:				;F1~F10,速度设置
	cmp al,'0'
	jb ESC
	cmp al,'9'
	ja ESC
	
	;刷新显示速度
	setB(spd,al)
	call show_score_and_spd		

	;更新recount值
	sub al,'0'
	xor bx,bx
	mov bl,al	
	mov al,[es:speed+bx]
	setB(recount,al)

	jmp key_and_death
ESC:
	cmp al,esc 		;esc键,用来退出游戏
	jz program_end
	;showFrames 02h

	jmp key_and_death

;=========================死亡界面=========================
DIE:	
	call clear_window		;刷新窗口
	showFrames [es:frame_color]

	printDieInfo			;打印提示信息

restart_or_exit:
	mov ah,0			;取一个键
	int 16h
restart_cheak:			;r键,用来重启游戏
	cmp al,'r'
	jnz exit_cheak
	jmp game_init
exit_cheak:
	cmp al,esc 			;esc键,用来退出户游戏
	jz program_end
	jmp restart_cheak
	
;=========================程序出口=========================
program_end:
	;恢复int 9int 1ch中断例程
	mov ax,cs
	mov ds,ax
	mov ax,0
	mov es,ax
	push word [data]
	pop word [es:9*4]
	push word [data+2]
	pop word [es:9*4+2]
	
	push word [data+4]
	pop word [es:1ch*4]
	push word [data+6]
	pop word [es:1ch*4+2]

	;返回dos
	mov ax,4c00h
	int 21h

4. 定时器相关

  • 对应定时器中断流程图
;----------------------------------------------------------------时钟相关-------------------------------------------------------------------------
;功能:自定义的1ch号定时器中断
;参数:;返回:无
int1ch_handler:
	cmp byte [es:die_flag],0	;死了,不要刷新蛇(不能把暂停放在前面,否则暂停的时候就不能退出或者重启了)
	jnz RETURN

	cmp byte [es:pause_flag],0	;暂停状态,不刷新
	jnz RETURN

	dec byte [es:count]
	JZ FLUSH				;计数减到0时刷新蛇位置
	jmp RETURN
FLUSH:
	reCount					;重装载计数器
	call snake_head_flush	;刷新蛇头
	
	cmp byte[es:get_food_flag],1	;吃到食物,这一轮蛇尾不要动
	jz RETURN
	call snake_tail_flush			;没吃到,刷新蛇尾
RETURN:	
	mov al,20h			;通知中断控制器8259A
	out 20h,al			;当前中断处理已经结束
	iret				;中断返回

5. 蛇显示相关

;----------------------------------------------------------------蛇显示相关--------------------------------------------------------------------------
;功能:初始化显示蛇
;参数:;返回:无
snake_init:
	push ax
	push bx	
	push dx
	push cx
	
	setAddrHT snake_head	;使[ds:bx]指向蛇头点

	mov ah,snake_head_color	;画蛇头
	mov al,2ah		;*号
	mov [ds:bx],ax		
	sub bx,2			

	mov ah,snake_color		;从蛇头开始向左画2格,代表初始蛇
	mov cx,2			
PRINTSNAKE:	
	mov [ds:bx],ax	
	sub bx,2
	loop PRINTSNAKE
	
	pop cx
	pop dx
	pop bx
	pop ax
	ret



;功能:刷新显示蛇头(根据HDIRBUFF移动蛇头,画*;参数:无
;返回:设置全局变量get_food_flag和die_flag
snake_head_flush:
	push ax
	push bx
	push ds

	mov si,[es:HDIRBUFF_HEAD]	;如果hdir缓冲为空,不用改snake_head
	cmp si,[es:HDIRBUFF_TAIL]	
	jz DIRSETTED_H		
				
	mov si,[es:HDIRBUFF_HEAD]	;如果hdir缓冲非空,取队首的键数据
	mov ax,[es:si]		;这个es前缀千万别忘了(bp/sp缺省ss,其他缺省ds)
	
;==========判断当前操作是否合法(不能掉头)重设snake_head=============
GET_W:	
	cmp al,'w'
	jnz GET_A
	cmp getDir(snake_head),'s'	;不能直接调头
	jz POP_hdir
	jmp RESET_HEAD
GET_A:
	cmp al,'a'
	jnz GET_S
	cmp getDir(snake_head),'d'	;不能直接调头
	jz POP_hdir
	jmp RESET_HEAD
GET_S:
	cmp al,'s'
	jnz GET_D
	cmp getDir(snake_head),'w'	;不能直接调头
	jz POP_hdir
	jmp RESET_HEAD
GET_D:
	cmp getDir(snake_head),'a'	;不能直接调头
	jz POP_hdir	

RESET_HEAD:
	xor ah,ah
	setDri(snake_head,ax)	;重设蛇头方向

	mov dx,[es:snake_head]	;此刻snake_head高字存储转向点,ax存储将要转向的方向,在此对TDIRBUFF进行入队
	call Enqueue_tdir

POP_hdir:
	call Dequeue_hdir		;队首元素出队

;=======至此snake_head中方向数据已经更新或保持,下面找到新的蛇头位置=================

DIRSETTED_H:
	mov dx,[es:snake_head]	;取得蛇头数据中“点位置”部分数据,根据前进方向进行修改
MOVE_W:
	cmp getDir(snake_head),'w'
	jnz MOVE_A
	dec dl
	jmp MOVE_DONE
MOVE_A:
	cmp getDir(snake_head),'a'
	jnz MOVE_S
	dec dh
	jmp MOVE_DONE
MOVE_S:
	cmp getDir(snake_head),'s'
	jnz MOVE_D
	inc dl
	jmp MOVE_DONE
MOVE_D:
	inc dh	

;=======至此dx已经存储了新蛇头位置,更新snake_head画新蛇头,并进行新食物生成=================
MOVE_DONE:
	clearB(get_food_flag)	;复位get_food_flag	
	
	setAddrHT snake_head	;修改原蛇头颜色为蛇身颜色
	mov ah,snake_color		;颜色
	mov [ds:bx+1],ah			
	
	mov [es:snake_head],dx	;更新snake_head
	setAddrHT snake_head	;设置ds:bx为蛇头点地址
	
	cmp byte[ds:bx],'#'		;如果蛇头撞到了非#或空格的点,而且现在蛇头和蛇尾不重合,那要么是撞到蛇身,要么是撞墙,设die标志
	jz DIECHEAKED
	cmp byte	[ds:bx],' '	
	jz DIECHEAKED
	mov ax,[es:snake_head]
	cmp ax,[es:snake_tail]
	jz DIECHEAKED
	setB(die_flag,1)

DIECHEAKED:
	cmp byte [ds:bx],'#'		;比较蛇头位置和当前食物位置
	jnz DRAWHEAD		;如果没吃到,转DRAWHEAD

	setB(get_food_flag,1)	;吃到了,设标志get_food_flag
	call draw_food		;画新食物	
	add word[es:score],10	;得分增加
	call show_score_and_spd	;刷新得分显示
DRAWHEAD:
	mov ah,snake_head_color	;颜色
	mov al,2ah		;*号
	mov [ds:bx],ax		;画新蛇头	
	
	pop ds
	pop bx
	pop ax	
	ret	


;功能:刷新显示蛇尾(先画空格刷掉蛇身,再根据TDIRBUFF移动蛇尾)
;参数:无
;返回:无
snake_tail_flush:
	push ax
	push bx
	
	push ds			;清原蛇尾点
	setAddrHT snake_tail	;设置ds:bx为蛇尾点地址
	mov ah,1
	mov al,' '			;显示一个空格
	mov [ds:bx],ax		
	pop ds

	mov si,[es:TDIRBUFF_HEAD]	;如果tdir缓冲为空,不用改snake_tail
	cmp si,[es:TDIRBUFF_TAIL]	
	jz NOTURN		
				
	mov si,[es:TDIRBUFF_HEAD]	;如果tdir缓冲非空,取队首的键数据到BX:AX
	mov bx,[es:si]		;这个es前缀千万别忘了(bp/sp缺省ss,其他缺省ds)
	mov ax,[es:si+2]	

	cmp bx,[es:snake_tail]	;判断snake_tail中位置(高字),如果蛇尾到达转向点了,就修改snake_tail中的方向参数(低字)
	jnz NOTURN
	
	setDri(snake_tail,ax)		;修改蛇尾	
	call Dequeue_tdir		;出队

NOTURN:
	mov bx,[es:snake_tail]	;取得蛇尾数据中“点位置”部分数据,根据前进方向进行修改
MOVE_W_T:
	cmp getDir(snake_tail),'w'
	jnz MOVE_A_T
	dec bl
	jmp MOVE_DONE_T
MOVE_A_T:
	cmp getDir(snake_tail),'a'
	jnz MOVE_S_T
	dec bh
	jmp MOVE_DONE_T
MOVE_S_T:
	cmp getDir(snake_tail),'s'
	jnz MOVE_D_T
	inc bl
	jmp MOVE_DONE_T
MOVE_D_T:
	inc bh	
MOVE_DONE_T:
	mov [es:snake_tail],bx	;更新snake_tail

	pop bx
	pop ax
	ret

6. 键盘输入相关

;----------------------------------------------------------------键盘输入相关--------------------------------------------------------------------------
;功能:自定义的9号键盘中断
;参数:;返回:无
int09h_handler:
	pusha				;保护通用reg

	mov al,0adh
	out PORT_KEY_STA,al	;禁止键盘发送数据到接口(准备接受键盘发送到接口的数据)
	
	in al, PORT_KEY_DAT	;读取键盘发来的按键扫描码
	
	sti			;开中断
	call int09h_fun		;完成相关功能
	cli			;关中断

	mov al,0aeh			;允许发送数据到接口
	out PORT_KEY_STA,al	

	mov al,20h			;通知中断控制器8259A
	out 20h,al			;当前中断处理已经结束

	popa				;恢复通用reg
	iret				;中断返回



;功能:自定义的9号键盘中断功能函数
;参数:;返回:无
int09h_fun:

CHEAK_ESC:	
	cmp al, 81h		;esc松开
	jnz CHEAK_R
	
	mov ah,al
	mov al,esc 	
	jmp Save2keyBuff	
	
CHEAK_R:
	cmp al,13h		;r按下
	jnz CHEAK_P
	
	mov ah,al
	mov al,'r' 	
	jmp Save2keyBuff

CHEAK_P:
	;如果是死亡状态,只检测 r/esc(避免入队多余的键)
	cmp byte [es:die_flag],0
	jnz int9_DONE

	cmp al,19h		;p按下
	jnz CHEAK_W

	mov ah,[es:pause_flag]	;注意这里,确保每一次按下P后存入keyBuff的ax都不同,否则不能入队(这样处理可以避免按住p时重复入队)
	mov al,'p' 	
	jmp Save2keyBuff

CHEAK_W:
	;如果是暂停状态,只检测 r/esc/p(避免入队多余的键)
	cmp byte [es:pause_flag],0
	jnz int9_DONE

	cmp al,11h	;w按下
	jnz CHEAK_A
	
	mov ah,al
	mov al,'w' 		
	jmp Save2dirBuff
CHEAK_A:
	cmp al,1eh	;a按下
	jnz CHEAK_S

	mov ah,al
	mov al,'a' 	
	jmp Save2dirBuff	
CHEAK_S:
	cmp al,1fh	;s按下
	jnz CHEAK_D
	
	mov ah,al
	mov al,'s' 	
	jmp Save2dirBuff
CHEAK_D:
	cmp al,20h	;d按下
	jnz CHEAK_SPD
	
	mov ah,al
	mov al,'d' 	
	jmp Save2dirBuff
	
CHEAK_SPD:		;F1~F10 (键值'0'~'9')
	cmp al,3bh
	jb int9_DONE
	cmp al,44h
	ja int9_DONE

	mov ah,al
	sub al,3bh	;从scan code转ascII
	add al,'0'
	jmp Save2keyBuff
Save2dirBuff:
	call Enqueue_hdir	;存入hdir缓冲,每个字数据高字节是扫描码,低字节是ascII
	jmp int9_DONE
Save2keyBuff:
	call Enqueue_key
int9_DONE:
	ret	



;功能:把设置按键数据存入环形队列缓冲区
;参数:ax
;返回:无
Enqueue_key:
	push ds		
	push bx		

	setDS KEYBUFF_DS		;缓冲区段地址 		
				
	mov bx,[KEYBUFF_TAIL]	;取队列的尾指针 		
	mov si,bx			;si=队列尾指针		
	add si,2			;si指向下一个可能的位置
	cmp si,KEYBUFF_LAST	;越出缓冲区吗?		
	jb EN_OK1		;没有越界,转移
	mov si,KEYBUFF_FIRST	;越界了,循环到缓冲区头部 
EN_OK1:
	cmp si,[KEYBUFF_HEAD]	;和队列头指针比较		
	jz EnqueueDONE1		;相等表示缓冲已满,不再存储数据
	cmp [bx-2],ax
	jz EnqueueDONE_h		;如果按键和上次一样,也不存了,这样可以避免按住不放时重复存入按键,操作更灵敏			
	
	mov [bx],ax		;按键数据存入队列	
	mov [KEYBUFF_TAIL],si	;保存队列尾指针
EnqueueDONE1:
	pop bx
	pop ds
	ret



;功能:把hdir按键数据存入环形队列缓冲区
;参数:ax存储要保存的数据
;返回:无
Enqueue_hdir:
	push ds		
	push bx		

	setDS HDIRBUFF_DS	;缓冲区段地址 		
				
	mov bx,[HDIRBUFF_TAIL]	;取队列的尾指针 		
	mov si,bx		                ;si=队列尾指针		
	add si,2			;si指向下一个可能的位置
	cmp si,HDIRBUFF_LAST 	;越出缓冲区吗?		
	jb EN_OK_h		;没有越界,转移
	mov si,HDIRBUFF_FIRST	;越界了,循环到缓冲区头部 
EN_OK_h:
	cmp si,[HDIRBUFF_HEAD]	;和队列头指针比较		
	jz EnqueueDONE_h		;相等表示缓冲已满,不再存储数据
	cmp [bx-2],ax
	jz EnqueueDONE_h		;如果按键和上次一样,也不存了,这样可以避免按住不放时重复存入按键,操作更灵敏

	mov [bx],ax		;按键数据存入队列	
	mov [HDIRBUFF_TAIL],si	;保存队列尾指针
EnqueueDONE_h:

	pop bx
	pop ds
	ret



;功能:hdir环形队列出队一个(队首指针后移)
;参数:;返回:无
Dequeue_hdir:
	push ds		

	setDS HDIRBUFF_DS
	mov si,[HDIRBUFF_HEAD]
	add si,2
	cmp si,HDIRBUFF_LAST 
	jb .LABh
	mov si,HDIRBUFF_FIRST
.LABh:
	mov [HDIRBUFF_HEAD],si
	pop ds
	ret	


;功能:把tdir按键数据存入环形队列缓冲区
;参数: DX:AX存储要保存的数据
;返回:无
Enqueue_tdir:
	push ds			
	push bx			

	setDS TDIRBUFF_DS	;缓冲区段地址 		
				
	mov bx,[TDIRBUFF_TAIL]	;取队列的尾指针 		
	mov si,bx		                ;si=队列尾指针		
	add si,4			;si指向下一个可能的位置
	cmp si,TDIRBUFF_LAST 	;越出缓冲区吗?		
	jb EN_OK_t		;没有越界,转移
	mov si,TDIRBUFF_FIRST	;越界了,循环到缓冲区头部 
EN_OK_t:
	cmp si,[TDIRBUFF_HEAD]	;和队列头指针比较		
	jz EnqueueDONE_t		;相等表示缓冲已满,不再存储数据

	mov [bx],dx		;按键数据存入队列	
	mov [bx+2],ax
	mov [TDIRBUFF_TAIL],si	;保存队列尾指针
EnqueueDONE_t:

	pop bx
	pop ds
	ret



;功能:tdir环形队列出队一个(队首指针后移)
;参数:;返回:无
Dequeue_tdir:
	push ds		

	setDS TDIRBUFF_DS
	mov si,[TDIRBUFF_HEAD]
	add si,4
	cmp si,TDIRBUFF_LAST 
	jb .LABt
	mov si,TDIRBUFF_FIRST
.LABt:
	mov [TDIRBUFF_HEAD],si
	pop ds
	ret	

;功能:清空所有循环队列
;参数:;返回:无
Queue_clear:
	push ax

	push es
	mov ax,KEYBUFF_DS 
	mov es,ax
	mov ax,KEYBUFF_FIRST
	mov [es:KEYBUFF_HEAD],ax
	mov [es:KEYBUFF_TAIL],ax
	
	pop es
	mov ax,HDIRBUFF_FIRST
	mov [es:HDIRBUFF_HEAD],ax
	mov [es:HDIRBUFF_TAIL],ax
	
	mov ax,TDIRBUFF_FIRST
	mov [es:TDIRBUFF_HEAD],ax
	mov [es:TDIRBUFF_TAIL],ax	

	pop ax
	ret

7. 边框显示相关

;功能:初始化窗口(清屏)------------------------------------------------------
;说明:双重循环,在25x80的屏幕缓冲区中全写空格
;参数:;返回:无
clear_window:
	push ax
	push ds
	push cx
	push bx
	 
	setDS lineAddr(0)		;ds指向第0行起始
	mov ah,00000111B		;屏幕所有点清成空格
	mov al,' '
	mov cx,25				;0~24行(最后一次循环指针已经指向25行了,但没赋值)
CLEARWINOW:
	push cx					;保护外层循环计数
	mov cx,80				;0~79列
	mov bx,0	

CLEARLINE:	
	mov word [ds:bx],ax
	add bx,2				;下一列
	loop CLEARLINE
	
	pop cx					;恢复外层循环计数
	
	mov bx,ds				;转向下一行
	add bx,0ah
	mov ds,bx
	loop CLEARWINOW

	pop bx
	pop cx
	pop ds
	pop ax	
	ret

;功能:显示所有边框------------------------------------------------------
;说明:1行和第24行全显示*号,其他行只有第0列和第79列显示*;参数: 堆栈一个字低字节存颜色
;返回: 无
show_all_frames:
	push bp
	mov bp,sp

	push ds
	push bx
	push cx
	push ax

	;显示横向边框
	setDS lineAddr(0)	;ds指向第0行起始
	mov bx,0			;偏移初始化为0	
	mov ah,[bp+4]		;颜色
	mov al,2ah			;*号

	mov cx,80			;ds开始80个字(正好一行)填入边框字符
show_up_frame:
	mov [ds:bx],ax	
	add bx,2		
	loop show_up_frame

	setDS  lineAddr(24)	;ds指向第24行起始
	mov bx,0			;偏移初始化为0

	mov cx,80	;ds开始80个字(正好一行)填入边框字符
show_down_frame:
	mov ah,[bp+4]	;颜色
	mov al,2ah	;*号
	mov [ds:bx],ax	
	add bx,2		
	loop show_down_frame

	;显示纵向边框
	setDS lineAddr(1)  	;ds指向第1行起始	
	mov cx,23			;1~23行(最后一次循环指针已经指向24行了,但没赋值)
	mov ah,[bp+4]		;颜色
	mov al,2ah			;*号
show_lengthwise_frame:
	mov bx,0			;最左边的*
	mov [ds:bx],ax
	add bx,160-2		;最右边的*
	mov [ds:bx],ax
	mov bx,ds			;转向下一行
	add bx,0ah
	mov ds,bx
	loop show_lengthwise_frame
	
	call show_score_and_spd		;显示得分	

	pop ax
	pop cx
	pop bx
	pop ds

	pop bp
	ret

;功能:显示得分和当前设置的速度------------------------------------------------------
;说明:在第0行中间显示得分,第24行中间显示速度。先把相关位置清为'-'符号,再显示得分和速度
;参数: 数据段score,spd
;返回: 无
show_score_and_spd:
	push bp
	mov bp,sp

	push ds
	push ax
	push bx	
	push cx	
	push dx

	
    ;清空score显示位置
	setDS lineAddr(0)		
	mov bx,68		
	mov ah,[es:info_color]	;颜色
	mov al,'-' 				;清成'-'(由于判断撞墙的机制,不可以清成空格)	
	mov cx,7				;7格
CLEARSCORE:
	mov [ds:bx],ax
	add bx,2
	loop CLEARSCORE 	
	
    ;清空spd显示位置
	setDS lineAddr(24)	
	mov bx,68	
	mov cx,7				;7格
CLEARSPD:
	mov [ds:bx],ax
	add bx,2
	loop CLEARSPD	
	
	;打印提示"spd:"和spd值
	mov ah,[es:info_color]	;颜色
	mov bx,70	
	mov al,'s'
	mov [ds:bx],ax
	add bx,2
	mov al,'p'
	mov [ds:bx],ax
	add bx,2
	mov al,'d'
	mov [ds:bx],ax
	add bx,2
	mov al,':'
	mov [ds:bx],ax
	add bx,2
	mov al,[es:spd]
	mov [ds:bx],ax

	;打印score的十进制值
	setDS cs	
	mov ax,[score]

	setDS lineAddr(0)		;设置score的位置
	mov bx,72	
	call print_num
	
	pop dx
	pop cx
	pop bx
	pop ax
	pop ds

	pop bp
	ret

;功能:显示一个数的十进制值----------------------------------------------------------
;说明:循环除10取余数分解各位,每一位入栈,打印时依次出栈打印,实现顺序显示
;参数: 被显示数ax,ds:bx已经指向显示位置
;返回: 无
print_num:
	push cx
	push bx
	push dx
	push ax
	push si	
	
	mov si,bx 		

	mov cx, -1
	mov bx,10
PRINTSCORE1:
	xor dx, dx
	div bx
	push dx
	cmp ax, 0
	loopne PRINTSCORE1

	not cx
PRINTSCORE2:
	pop ax
	mov ah,[es:info_color]	;颜色
	add al, '0'
	
	mov [ds:si],ax
	add si,2
	LOOP PRINTSCORE2

	pop si
	pop ax
	pop dx
	pop bx
	pop cx	
	ret

8. 支持函数

  • 包括生成新食物和打印提示信息的函数
;功能: 生成一个新食物
;参数:;返回: 无
draw_food:
	push ax
	push cx
	push dx	
	push bx
	push ds

	;读实时种(CH:CL=:分 DH:DL=:1/100秒,设dh存的秒为x1,dl存的百分之秒为x2
	mov ah,2ch
	int 21h
	
	;算出当前实时秒个位,加1后压栈(1~10)
	xor ax,ax
	mov al,dh	
	mov bl,10
	div bl
	mov al,ah
	xor ah,ah
	inc ax
	push ax	
	
	;判断实时秒是奇还是偶,生成随机数ax
	xor ax,ax
	mov al,dh
	mov bl,2
	div bl
	cmp ah,0
	jz EVEN	
		
	pop ax
	mul dl	;实时秒是奇数:ax=(x1+1)*x2

	jmp cheakDone
EVEN:
	mov ax,11
	pop bx
	sub ax,bx
	mul dl	;实时秒是偶数:ax=(10-x1)*x2		
	
cheakDone:

	;用ax对行数和列数取模,得到随机食物位置
	push ax		;暂存随机数
	xor dx,dx
	mov bx,78	;dx:ax除bx,商在ax,余数在dx
	div bx
	add dx,1		;1~78,x坐标(每列2个字节)

	pop ax		;恢复随机数
	push dx		;暂存x坐标
	xor dx,dx
	mov bx,23	;dx:ax除bx,商在ax,余数在dx
	div bx
	inc dx		;1~23,y坐标
	
	;根据食物位置设置ds和bx,令[ds:bx]指向food所在点
	mov ax,displayBase		
	mov cx,dx
getYAddr:
	add ax,0ah		
	loop getYAddr	

	mov ds,ax		;至此ds指向food所在行首
			
	pop bx			;恢复x坐标
	add bx,bx			;至此[ds:bx]指向food所在点
	
	;如果随机到蛇身或者其他食物点,调整点位置(向右一直移动直到合适为止,注意换行)
	cmp byte[ds:bx],'*'	
	jz ADJUST
	cmp byte[ds:bx],'#'	
	jnz ADJUSTDONE

ADJUST:
	add bx,2
	cmp bx,156		;>78,重置到1
	jb ADJUSTDONE
	mov bx,2		

	mov ds,ax
	add ax,0ah
	cmp ax,lineAddr(25)	;>=25,重置到1
	jb ADJUSTDONE
	
	mov ax,lineAddr(1)
	mov ds,ax

ADJUSTDONE:
	;如果移动后还不行,继续移动直到可以
	cmp byte[ds:bx],'*'	
	jz ADJUST
	cmp byte[ds:bx],'#'	
	jz ADJUST
	
	;画食物
	mov ah,[es:food_color]
	mov al,'#'			;#号
	mov [ds:bx],ax
	
	pop ds
	pop bx
	pop dx
	pop cx
	pop ax
    ret



;在窗口指定位置显示字符串,字符串以'$'结束
;参数:si指向字符串首地址,ds:bx指向屏上显示起始位置
;返回:无
show_str:
	push ax
show_str_start:
	mov al,byte [es:si]
	cmp al,'$'
	jz show_str_end

	mov ah,byte [es:info_color]
	mov [ds:bx],ax
	inc si
	add bx,2
	jmp show_str_start
	
show_str_end:
	pop ax
	ret
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

云端FFF

所有博文免费阅读,求打赏鼓励~

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值