本页目录

8086汇编贪吃蛇

变量设置

在数据段中使用以下变量:

datasg segment
    addri9 db 0,0,0,0            ;存放原9号中断的中断向量
    seed   db 0adh               ;随机数种子
    rand   db 0                  ;随机数
    apple  db 44h                ;果实位置,初始设为44h
    dir    db 0                  ;方向,0:up 1:right 2:left 3:down
    dirbuf db 0
    snake  db 4ah,49h,48h,47h    ;蛇尾 --- 蛇头
           db 256 dup(0)
datasg ends

前置技术

在汇编实现贪吃蛇通常需要解决以下几个技术:

延时函数

在汇编中延时函数可以通过执行空循环来实现:

;;;;延迟一段时间
delay:
          push  cx
          mov   cx,7h                            ;增大此值可以减慢蛇的移动速度(间隔)
d1:       push  cx
          mov   cx,0ffffh
d2:       loop  d2
          pop   cx
          loop  d1
          pop   cx
          ret

虽然这种方法不能精确控制延迟多少秒/毫秒,但可以通过调节外层循环的次数(内层也可以)来相对地控制延迟时间。(一层循环最大执行次数是ffffh,仍然太快,因此需要两层循环)

随机数

使用平方取中法实现一个简单的伪随机序列,随机种子预先写死在代码中:

;;;;平方取中随机数
;输出:
;rand     下一个随机数
getrand:
          push  ax
          push  cx
          mov   al,rand
          mul   byte ptr rand
          mov   cl,4
          shr   ax,cl
          cmp   al,0
          jnz   ok
          mov   al,seed                          ;避免陷入0循环
ok:
          mov   rand,al
          pop   cx
          pop   ax
          ret

这里生成的是8位的随机数,每次调用getrand都会更新rand标号处的一个字节的值。这个随机数生成方法有时会陷入0循环,因此产生新的随机数时加了一个判断,如果结果为0则重新从seed开始生成。

处理键盘输入

使用自定义的键盘中断程序(9号中断)解决,大致框架为:

codesg segment
    start:
             mov   ax,datasg
             mov   ds,ax
    ;保存原来9号中断的中断向量到addri9处
             mov   ax,0
             mov   es,ax
             push  es:[9*4]
             pop   word ptr addri9[0]
             push  es:[9*4+2]
             pop   word ptr addri9[2]
    ;更改原来的中断向量
             mov   word ptr es:[9*4],offset int9    ;设置9号中断的IP为offset int9
             mov   es:[9*4+2],cs                    ;设置9号中断的CS为代码段起始地址
             ;...

    ;游戏结束返回
    ret_main:
    ;恢复原来的中断向量
             mov   ax,0
             mov   es,ax
             push  word ptr addri9[0]
             pop   es:[9*4]
             push  word ptr addri9[2]
             pop   es:[9*4+2]
             ;...


    ;;;;自定义键盘中断
    int9:
             push  ax
             push  bx
    ;模拟标志寄存器入栈
             pushf
    ;模拟设置IF=0,TF=0
             pushf
             pop   ax
             and   ah,11111100b
             push  ax
             popf
    ;call指令即可实现CS、IP入栈并跳转到入口地址
             call  dword ptr addri9
    ;自定义的键盘输入处理
             in    al,60h                           ;读取键盘输入送入AL
             cmp   al,dir
             je    int9_dft
             cmp   al,11h                           ;W键
             je    int9_w
             cmp   al,20h                           ;D键
             je    int9_d
             cmp   al,1eh                           ;A键
             je    int9_a
             cmp   al,1fh                           ;S键
             je    int9_s
             jmp   int9_dft
    int9_w:
             mov   dirbuf,0
             jmp   int9_dft
    int9_d:
             mov   dirbuf,1
             jmp   int9_dft
    int9_a:
             mov   dirbuf,2
             jmp   int9_dft
    int9_s:
             mov   dirbuf,3
             jmp   int9_dft
    int9_dft:
             pop   bx
             pop   ax
             iret
codesg ends
end start

在设计键盘响应时,不希望玩家能够直接修改蛇的移动方向为当前方向的反向(不能调转),例如如果当前移动方向为向上,则应只允许修改为向左/向右,而不能修改为向下。为此额外添加了dirbuf变量,用户的键盘输入会实时存入dirbuf,但只在每次主循环中有条件地与dir同步。主循环如下(其中move函数处理蛇移动一步,后面会详细说明):

main_loop:
          call  delay
          mov   al,dirbuf
          xor   al,dir
          cmp   al,3
          je    keep_dir
          xor   al,dir
          mov   dir,al
keep_dir:
          call  move
          jmp   main_loop

代码中dirdirbuf异或为3时,表示方向刚好相差,此时不改变dir的值(je keep_dir);由于异或操作改变了AL的值,因此送入dir前需要再异或一次原来的dir值。

坐标设计与操作显存绘制

通过操作显存绘制图形,显存中用2个字节表示一个字符,低字节表示字符的ASCII码,高字节表示字符的颜色。这里不设置字符,只设置背景色,相邻的两列刚好凑成一个正方形。

游戏的地图大小设计为(含边界),这样刚好可以用4位表示一个坐标分量,一个字节表示一个二维坐标。约定一个字节的高4位表示坐标,低4位表示坐标。例如4ah表示逻辑坐标,对应地图中的第10行、第4列的方格。

由于字符显示区是25行、80列,这里的坐标对应了行号-5(为了让地图显示在屏幕中间),坐标对应了列号/2(因为每个字符占2列)。封装一个write函数,用于将属性写入显存的指定位置:

;;;;在显存[x+5行,2*y列]和[x+5行,2*y+1列]处写入属性BH(等价于逻辑坐标x,y)
write:
;输入:
;BH       属性
;BL[7:4]  y坐标(逻辑列号)
;BL[3:0]  x坐标(逻辑行号)
          push  cx
          mov   di,bx
          and   di,00f0h                         ;(di)=逻辑列号*16
          mov   cl,2
          shr   di,cl                            ;逻辑右移2位,(di)=逻辑列号*4
          mov   cl,bl
          and   cl,0fh                           ;(cl)=逻辑行号
          mov   al,10
          mul   cl                               ;(ax)=逻辑行号*10
          add   ax,0b800h+50                     ;(ax)=逻辑行号*10+b800h+50
          mov   es,ax                            ;(es)=逻辑行号*10+b800h+50
          mov   byte ptr es:[di+1],bh
          mov   byte ptr es:[di+3],bh
          pop   cx
          ret

显存的起始地址是b8000h,屏幕第i行、第j列的偏移地址是80*2*i+2*j,用段地址:偏移地址表示为b800:80*2*i+2*j;我们希望从第5行开始显示地图,因此偏移地址额外加上80*2*5,如果加到段地址上还要除以16,也就是80*2*5/16=50,对应代码中0b800h+50

同理,逻辑行号在偏移地址中数值要乘80*2=160,而表示在段地址中只需乘160/16=10

实现

代码

完整的代码如下:

assume cs:codesg,ds:datasg,ss:stcksg

datasg segment
    addri9 db 0,0,0,0            ;存放原9号中断的中断向量
    seed   db 0adh               ;随机数种子
    rand   db 0                  ;随机数
    apple  db 44h                ;果实位置,初始设为44h
    dir    db 0                  ;方向,0:up 1:right 2:left 3:down
    dirbuf db 0
    snake  db 4ah,49h,48h,47h    ;蛇尾 --- 蛇头
           db 256 dup(0)
datasg ends

stcksg segment
           db 128 dup(0)
stcksg ends

codesg segment
    start:
              mov   ax,datasg
              mov   ds,ax
    ;保存原来9号中断的中断向量到addri9处
              mov   ax,0
              mov   es,ax
              push  es:[9*4]
              pop   word ptr addri9[0]
              push  es:[9*4+2]
              pop   word ptr addri9[2]
    ;更改原来的中断向量
              mov   word ptr es:[9*4],offset int9    ;设置9号中断的IP为offset int9
              mov   es:[9*4+2],cs                    ;设置9号中断的CS为代码段起始地址
    ;初始化
              call  init
    ;主循环
    main_loop:
              call  delay
              mov   al,dirbuf
              xor   al,dir
              cmp   al,3
              je    keep_dir
              xor   al,dir
              mov   dir,al
    keep_dir:
              call  move
              jmp   main_loop
    ;游戏结束返回
    gameover:
    ;恢复原来的中断向量
              mov   ax,0
              mov   es,ax
              push  word ptr addri9[0]
              pop   es:[9*4]
              push  word ptr addri9[2]
              pop   es:[9*4+2]
    ;清空键盘缓冲区
    clear:
              mov   ah, 01h
              int   16h
              je    done
              mov   ah, 00h
              int   16h
              jmp   clear
    done:
              mov   ax,4c00h
              int   21h

    ;;;;延迟一段时间
    delay:
              push  cx
              mov   cx,7h                            ;增大此值可以减慢蛇的移动速度(间隔)
    d1:       push  cx
              mov   cx,0ffffh
    d2:       loop  d2
              pop   cx
              loop  d1
              pop   cx
              ret

    ;;;;平方取中随机数
    ;输出:
    ;rand     下一个随机数
    getrand:
              push  ax
              push  cx
              mov   al,rand
              mul   byte ptr rand
              mov   cl,4
              shr   ax,cl
              cmp   al,0
              jnz   ok
              mov   al,seed                          ;避免陷入0循环
    ok:
              mov   rand,al
              pop   cx
              pop   ax
              ret

    ;;;;在显存[x+5行,2*y列]和[x+5行,2*y+1列]处写入属性BH(等价于逻辑坐标x,y)
    write:
    ;输入:
    ;BH       属性
    ;BL[7:4]  y坐标(逻辑列号)
    ;BL[3:0]  x坐标(逻辑行号)
              push  cx
              mov   di,bx
              and   di,00f0h                         ;(di)=逻辑列号*16
              mov   cl,2
              shr   di,cl                            ;逻辑右移2位,(di)=逻辑列号*4
              mov   cl,bl
              and   cl,0fh                           ;(cl)=逻辑行号
              mov   al,10
              mul   cl                               ;(ax)=逻辑行号*10
              add   ax,0b800h+50                     ;(ax)=逻辑行号*10+b800h+50
              mov   es,ax                            ;(es)=逻辑行号*10+b800h+50
              mov   byte ptr es:[di+1],bh
              mov   byte ptr es:[di+3],bh
              pop   cx
              ret

    ;;;;初始化地图
    init:
    ;清屏
              mov   ax,0b800h+50
              mov   es,ax
              mov   cx,7fffh
              mov   di,0
    i1:       mov   byte ptr es:[di],0
              inc   di
              loop  i1
    ;绘制果实和蛇
              call  getrand
              mov   bh,40h                           ;红底(果实)
              mov   bl,apple
              call  write
              mov   si,0
              mov   bh,20h                           ;绿底(蛇)
    i2:       mov   bl,snake[si]
              call  write
              inc   si
              cmp   byte ptr snake[si],0
              jnz   i2
    ;绘制边界
              mov   bh,70h                           ;白底(边界)
              mov   cx,15                            ;16*16地图(包含边界)
    i3:       mov   bl,cl                            ;x=i,y=0
              call  write
              or    bl,0f0h                          ;x=i,y=15
              call  write
              mov   bl,cl
              push  cx
              mov   cx,4
              shl   bl,cl                            ;赋值y=i
              pop   cx
              or    bl,0fh                           ;x=15,y=i
              call  write
              and   bl,0f0h                          ;x=0,y=i
              call  write
              jcxz  ret_init
              sub   cx,1
              jmp   i3
    ret_init:
              ret

    ;;;;移动一步
    move:
    ;si移动到头部下一个位置
              mov   si,0
    m2:       inc   si
              cmp   snake[si],0
              jnz   m2
    ;复制原头部数据
              mov   al,snake[si-1]
              mov   snake[si],al
    ;根据方向移动蛇头
              cmp   byte ptr dir,0
              je    move_u
              cmp   byte ptr dir,1
              je    move_r
              cmp   byte ptr dir,2
              je    move_l
              cmp   byte ptr dir,3
              je    move_d
              jmp   move_dft
    move_u:
              sub   snake[si],1
              jmp   move_dft
    move_r:
              add   snake[si],10h
              jmp   move_dft
    move_l:
              sub   snake[si],10h
              jmp   move_dft
    move_d:
              add   snake[si],1
              jmp   move_dft
    move_dft:
    ;擦除尾部
              mov   bh,0
              mov   bl,snake[0]
              call  write
    ;绘制新头部
              mov   bh,20h
              mov   bl,snake[si]
              call  write
    ;判定蛇头吃到果实或撞墙
              cmp   bl,apple
              je    eat
              call  is_valid
              cmp   al,0
              je    gameover
    ;移动蛇身
              mov   si,0
    m1:       mov   al,snake[si+1]
              mov   snake[si],al
              inc   si
              cmp   byte ptr snake[si+1],0
              jnz   m1
    ;擦除复制的新头部
              mov   snake[si],0
              jmp   ret_move
    ;吃到果实
    eat:
              mov   al,apple
              mov   snake[si],al
    m3:       call  getrand
              mov   bl,rand
              call  is_valid
              cmp   al,1
              jne   m3                               ;如果生成的位置已经有蛇身,则重新生成
              mov   byte ptr apple,bl
              mov   bh,40h
              mov   bl,apple
              call  write
              ret

    ;;;;判断合法位置:边界、蛇身处判定为不合法(不判断蛇头)
    is_valid:
    ;输入:
    ;BL     坐标
    ;输出:
    ;AL     0(不合法)/1(合法)
              push  bx
              mov   al,0
              cmp   bl,0fh                           ;x=i,y=0
              jb    false
              cmp   bl,0f0h                          ;x=i,y=15
              ja    false
              mov   bh,bl
              and   bh,0fh
              cmp   bh,0fh                           ;x=15,y=i
              je    false
              cmp   bh,0                             ;x=0,y=i
              je    false

              mov   si,0
    v1:       cmp   bl,snake[si]
              je    false
              inc   si
              cmp   byte ptr snake[si+1],0
              jnz   v1

              mov   al,1
    false:
              pop   bx
              ret

    ;;;;自定义键盘中断
    int9:
              push  ax
              push  bx
    ;模拟标志寄存器入栈
              pushf
    ;模拟设置IF=0,TF=0
              pushf
              pop   ax
              and   ah,11111100b
              push  ax
              popf
    ;call指令即可实现CS、IP入栈并跳转到入口地址
              call  dword ptr addri9
    ;自定义的键盘输入处理
              in    al,60h                           ;读取键盘输入送入AL
              cmp   al,dir
              je    int9_dft
              cmp   al,11h                           ;W键
              je    int9_w
              cmp   al,20h                           ;D键
              je    int9_d
              cmp   al,1eh                           ;A键
              je    int9_a
              cmp   al,1fh                           ;S键
              je    int9_s
              jmp   int9_dft
    int9_w:
              mov   dirbuf,0
              jmp   int9_dft
    int9_d:
              mov   dirbuf,1
              jmp   int9_dft
    int9_a:
              mov   dirbuf,2
              jmp   int9_dft
    int9_s:
              mov   dirbuf,3
              jmp   int9_dft
    int9_dft:
              pop   bx
              pop   ax
              iret
codesg ends
end start

演示

img