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
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章