;*********************************************************************************
; 程 序: snake.exe *
; 源文件: snake.asm *
; 环 境: masmplus 1.2 *
; 编 译: ml /c /coff /Fo"snake.obj" ".\snake.asm" *
; 链 接: link /out:snake.exe /subsystem:windows /merge:.rdata=.text snake.obj *
; 作 者: hiroyukki *
; 日 期: 2011.03.04 *
; 玩 法: P: 暂停和继续 C: 切换颜色模式 上下左右WASD键: 调整蛇头方向 *
; Q或ESC: 退出游戏 N: 开始新游戏 *
; 说 明: 模型如下,因为总共的格子是有限的,所以我采取了静态内存的方法,以 *
; 避免每吃一个食物就申请一次内存的麻烦,在程序初始化时,就把所有空格上 *
; 的结点初始化好了,类似内存池的机制。 *
; 程序共 23 * 17 = 391个格子,故在 data? 域申请 391 个 DWORD。这些 *
; DWORD的4个字节从高到低分别表示一个结点的 x位置 y位置 结点类型(食物还 *
; 是蛇身,绘制时根据此指定不同颜色) 有效标志(即蛇到达的位置的结点才是 *
; 有效的)。 *
; 程序时刻记录着当前头的结点,在每次移动时根据方向不同来获取下一个 *
; 将要到达的位置,如果该位置是墙或蛇身,则撞死,否则把下一个位置上的结 *
; 点标志为有效(同时应该把当前蛇尾标志为无效)。 *
; 当前的所有有效结点都存放在另一个数组中,此数组看做一个结点的链表 *
; 数组的第一个元素是蛇尾,最后一个非0元素是蛇头。每当吃掉一个食物或者 *
; 前进一步,视做换掉了蛇头。但是不一样的是,吃掉食物时只简单地把新结点 *
; 放到数组的第一个0元素,即成为新的蛇头。而前进的情况下,在数组最后加 *
; 入新结点后,要把蛇尾,即数组中第一个结点移除,做法是把数组后面的结点 *
; 依次左移。绘制时其实是使用这个链表里的元素进行绘制,所以要保持链表里 *
; 元素与全局结点列表里的相应元素同步。 *
; 共 23 * 17个结点,第一个结点位置为 (1, 1),最后一个为(23, 17)。 *
; 这些结点在一个一维数组中,则由 (x, y)得到相应结点在一维数组中的索引的 *
; 公式为 (y - 1) * 23 + (x - 1) *
; 为了进一步减小可执行文件体积,合并了只读数据段和代码段,然后使用 *
; upx 压,EXE为 4096 bytes 。 *
;*********************************************************************************
.386
.model flat, stdcall
option casemap:none
;***************************************************************************
; Include
;***************************************************************************
include windows.inc
include gdi32.inc
includelib gdi32.lib
include user32.inc
includelib user32.lib
include kernel32.inc
includelib kernel32.lib
includelib Coredll.lib
;***************************************************************************
; 这个结构并未使用,只是大致描述下一个结点的样子,一个节点是一个DWORD *
; 从高位到低位分别是: x坐标 y坐标 类型(食物或蛇身) 有效标志 *
; 每个字段点一个字节 *
;***************************************************************************
Node struct
x BYTE ?
y BYTE ?
ntype BYTE ?
bValid BYTE ?
Node ends
TIMER equ 1
FOOD equ 0
BODY equ 1
RIGHT equ 2
LEFT equ 3
UP equ 4
DOWN equ 5
;***************************************************************************
; 数据段
;***************************************************************************
.data?
hInstance dd ?
hWinMain dd ?
hDc dd ?
isPaused dd ?
isAlive dd ?
bRanColor dd ?
bTurnPending dd ?
direction dd ?
food dd ?
nodes dd 391 dup (?)
nodeList dd 391 dup (?)
curX dd ?
curY dd ?
foodX dd ?
foodY dd ?
score dd ?
rClient RECT <>
wndWidth dd ?
wndHeight dd ?
szScoreBuffer db 32 dup (?)
seed dd ?
speed dd ?
timerCounts dd ?
eatCounts dd ?
.const
szClassName db '1', 0
szCaptionMain db '贪食蛇', 0
szCaptionPause db '已暂停', 0
szCaptionDie db '游戏结束,请按N键开始新游戏', 0
szDown db '下', 0
szUp db '上', 0
szRight db '右', 0
szLeft db '左', 0
szScore db '得分:%d', 0
;***************************************************************************
; 代码段
;***************************************************************************
.code
;***************************************************************************
; 随机数生成
;***************************************************************************
_NextInt proc uses edx ebx max
push edx
invoke GetTickCount
add eax, seed
add seed, eax
xor edx, edx
mov ebx, max
div ebx
xor seed, eax
mov eax, edx
inc eax
pop edx
ret
_NextInt endp
;***************************************************************************
; 初始化所有蛇身结点把前三个置为有效
;***************************************************************************
_InitNodes proc
push edi
push edx
push ebx
push ecx
xor edi, edi
@@:
xor edx, edx
mov eax, edi
mov ebx, 17h
div ebx
; 商是 y 坐标,余数是 x 坐标
xor ecx, ecx
or ecx, edx
inc ecx
shl ecx, 8
or ecx, eax
inc ecx
shl ecx, 8
or ecx, BODY
shl ecx, 8
; ecx 为结点, ebx 为有效结点列表
; nodeList 数组首元素即蛇尾,最后一个不为0的元素为蛇头
xor ebx, ebx
.if edi < 4
or ecx, 1
mov ebx, ecx
.endif
mov [offset nodes + edi * 4], ecx
mov [offset nodeList + edi * 4], ebx
inc edi
.if edi >= 187h
jmp @F
.endif
jmp @B
@@:
pop ecx
pop ebx
pop edx
pop edi
ret
_InitNodes endp
;***************************************************************************
; 节点构造函数,参数分别是X坐标,Y坐标,类型(食物还是身体),有效标志
;***************************************************************************
_CreateNode proc nx, ny, nntype, nInvalid
xor eax, eax
or eax, nx
shl eax, 8
or eax, ny
shl eax, 8
or eax, nntype
shl eax, 8
or eax, nInvalid
ret
_CreateNode endp
;***************************************************************************
; 查看指定结点是否在(x, y)
;***************************************************************************
_IsNodeAt proc node, x, y
push ebx
push ecx
mov eax, node
mov ebx, eax
mov ecx, eax
shr ebx, 24
and ebx, 0ffh
sub ebx, x
shr ecx, 16
and ecx, 0ffh
sub ecx, y
.if ecx == 0 && ebx == 0
xor eax, eax
.endif
pop ecx
pop ebx
ret
_IsNodeAt endp
;***************************************************************************
; 绘制单个节点
;***************************************************************************
_DrawNode proc uses ebx ecx _hDC, hNode
local @stColor: COLORREF
local @hBrush: HBRUSH
local @hOldBrush: HBRUSH
local @stRect: RECT
push ecx
push ebx
mov eax, hNode
; 先取标志状态,如果是无效,直接退出
and eax, 000000ffh
.if !eax
jmp @F
.endif
invoke _IsNodeAt, hNode, curX, curY
mov ecx, eax
mov eax, hNode
and eax, 0000ff00h
shr eax, 8
.if (eax == BODY) && (ecx == 0)
mov @stColor, 00ffffffh
.elseif eax == BODY
.if bRanColor
invoke _NextInt, 255
mov ecx, eax
shl ecx, 8
invoke _NextInt, 255
or ecx, eax
shl ecx, 8
invoke _NextInt, 255
or ecx, eax
mov @stColor, ecx
.else
mov @stColor, 000000ffh
.endif
.else
mov @stColor, 0000ff00h
.endif
invoke CreateSolidBrush, @stColor
mov @hBrush, eax
invoke SelectObject, _hDC, @hBrush
mov @hOldBrush, eax
; 算出左右要画的地方
mov ecx, hNode
and ecx, 0ff000000h
shr ecx, 24
xor eax, eax
mov al, cl
mov bl, 20
mul bl
sub eax, 4
mov @stRect.left, eax
add eax, 18
mov @stRect.right, eax
; 算出上下要画的地方
mov ecx, hNode
and ecx, 00ff0000h
shr ecx, 16
xor eax, eax
mov al, cl
mov bl, 20
mul bl
sub eax, 4
mov @stRect.top, eax
add eax, 18
mov @stRect.bottom, eax
invoke FillRect, _hDC, addr @stRect, NULL
invoke SelectObject, _hDC, @hOldBrush
; 狂他妈漏啊……
invoke DeleteObject, @hBrush
@@:
pop ebx
pop ecx
ret
_DrawNode endp
;***************************************************************************
; 画蛇身,其实就是遍历所有结点画,绘画函数会自己根据结点的状态来选择绘
; 制方式。
;***************************************************************************
_DrawBody proc uses ecx edi _hDC
push edi
push ecx
push eax
xor edi, edi
mov ecx, 187h
@@:
mov eax, [offset nodeList + edi * 4]
.if !eax
jmp @F
.endif
invoke _DrawNode, _hDC, eax
inc edi
loop @B
@@:
pop eax
pop ecx
pop edi
ret
_DrawBody endp
;***************************************************************************
; 绘制分数
;***************************************************************************
_DrawScore proc _hDC
local @oldColor: COLORREF
local @oldBkMode: DWORD
local @szBuffer[128]: BYTE
local @hFont: HFONT
local @hOldFont: HFONT
local @hPen: HPEN
local @hOldPen: HPEN
local @strLen: DWORD
push ebx
invoke wsprintf, offset szScoreBuffer, offset szScore, score
; 求字符串长度
xor eax, eax
@@:
mov bl, BYTE ptr [offset szScoreBuffer + eax]
.if bl != 0
inc eax
jmp @B
.endif
mov @strLen, eax
invoke SetBkMode, _hDC, TRANSPARENT
mov @oldBkMode, eax
invoke SetTextColor, _hDC, 0000ffffh
mov @oldColor, eax
invoke CreateFont, 14, 7, 0, 0, 8, FALSE, FALSE, FALSE, GB2312_CHARSET, OUT_CHARACTER_PRECIS, CLIP_CHARACTER_PRECIS, 5, DEFAULT_PITCH, NULL
mov @hFont, eax
invoke SelectObject, _hDC, eax
mov @hOldFont, eax
invoke CreatePen, PS_SOLID, 1, 00ffffffh
mov @hPen, eax
invoke SelectObject, _hDC, eax
mov @hOldPen, eax
invoke TextOut, _hDC, 410, 15, offset szScoreBuffer, @strLen
invoke MoveToEx, _hDC, 10, 10, 0
invoke LineTo, _hDC, 480, 10
invoke LineTo, _hDC, 480, 360
invoke LineTo, _hDC, 10, 360
invoke LineTo, _hDC, 10, 10
invoke SetBkMode, _hDC, @oldBkMode
invoke SetTextColor, _hDC, @oldColor
invoke SelectObject, _hDC, @hOldFont
invoke SelectObject, _hDC, @hOldPen
invoke DeleteObject, @hFont
invoke DeleteObject, @hOldPen
pop ebx
ret
_DrawScore endp
;***************************************************************************
; 获得指定位置上的结点
;***************************************************************************
_GetNodeAt proc xLocation, yLocation
push ebx
mov eax, yLocation
dec eax
mov ebx, 17h
mul ebx
add eax, xLocation
dec eax
mov eax, [offset nodes + eax * 4]
pop ebx
ret
_GetNodeAt endp
;***************************************************************************
; 由传入的X Y 位置查询目标结点的状态(是否激活)
;***************************************************************************
_QueryNodeStat proc xLocation, yLocation
invoke _GetNodeAt, xLocation, yLocation
and eax, 000000ffh
ret
_QueryNodeStat endp
;***************************************************************************
; 激活或压制一个已处于激活状态的结点
; 参数分别是 x 坐标, y坐标, 是否激活的布尔变量
;***************************************************************************
_Activate proc xLocation, yLocation, bActivate
push ebx
push edi
mov eax, yLocation
dec eax
mov ebx, 17h
mul ebx
add eax, xLocation
dec eax
mov edi, eax
mov eax, [offset nodes + eax * 4]
.if bActivate
or eax, 0000001h
.else
and eax, 0ffffff00h
.endif
mov [offset nodes + edi * 4], eax
pop edi
pop ebx
ret
_Activate endp
;***************************************************************************
; 下一个食物,总是随机出现。并且不会出现在蛇身上
; 这种实现似乎比较2,更好的方法应该是把所有结点串成两个链表,蛇身链表和非
; 蛇身链表。每次随机地从非蛇身链表里取出结点插入蛇身链表中,这样就不会造成
; 随机生成的位置在蛇身上从而重新生成了。
; 重新生巢呗陨在蛇身将要填充满整个区域时性能急剧下降。
;***************************************************************************
_NextFood proc uses ebx
invoke _NextInt, 17
mov foodY, eax
invoke _NextInt, 23
mov foodX, eax
invoke _QueryNodeStat, foodX, foodY
.if eax
invoke _NextFood
.endif
invoke _CreateNode, foodX, foodY, FOOD, 1
mov food, eax
ret
_NextFood endp
;***************************************************************************
; 往队列头加一个结点,刚吃了一顿或走了一步后都会调用此函数。
; 第一个参数是要加入的结点,第二个是指示是否移除队列尾。正常移动时需要
; 在队列头加结点的同时移除队列尾,以达到好像在移动的假象……
;***************************************************************************
_AddBody proc node, bRmvTail
push edi
push ebx
push ecx
; 如果是刚吃的,先把类型改成 BODY
mov eax, node
or eax, 00000100h
xor edi, edi
@@:
mov ebx, [offset nodeList + edi * 4]
.if ebx != 0
inc edi
jmp @B
.endif
mov [offset nodeList + edi * 4], eax
; 如果要移除队列尾,则先把队列尾的激活状态压下去,然后集体左移一个索引位置
; 此时edi里保存着队列应有的长度
.if bRmvTail
; 取第一个元素,这是队列头,先反激活它
mov eax, [nodeList]
mov ebx, eax
and ebx, 0ff000000h
shr ebx, 24
mov ecx, eax
and ecx, 00ff0000h
shr ecx, 16
invoke _Activate, ebx, ecx, FALSE
; 然后移动这个队列,把后面的都左移
mov ecx, edi
xor edi, edi
@@:
mov eax, [offset nodeList + edi * 4 + 4]
mov [offset nodeList + edi * 4], eax
inc edi
loop @B
mov [offset nodeList + edi * 4], 0
.endif
pop ecx
pop ebx
pop edi
ret
_AddBody endp
;***************************************************************************
; 吃掉食物,吃掉时候分数增加,并且生成下一个食物
;***************************************************************************
_PieFromSky proc
mov eax, score
add eax, 10
mov score, eax
mov eax, eatCounts
.if eax != 3
inc eatCounts
.else
mov eax, speed
.if eax > 1
dec speed
.endif
mov eatCounts, 0
.endif
invoke _GetNodeAt, foodX, foodY
invoke _AddBody, eax, FALSE
invoke _NextFood
ret
_PieFromSky endp
;***************************************************************************
; 新游戏,初始化各种状态
;***************************************************************************
_NewGame proc
invoke _InitNodes
invoke _NextFood
mov bTurnPending, FALSE
mov isPaused, 0
mov isAlive, 1
mov score, 0
mov speed, 8
mov timerCounts, 0
mov eatCounts, 0
mov curX, 4
mov curY, 1
mov direction, RIGHT
ret
_NewGame endp
;***************************************************************************
; 蛇身移动
;***************************************************************************
_Move proc
local @nextX: DWORD
local @nextY: DWORD
push ebx
push curX
push curY
pop @nextY
pop @nextX
mov bTurnPending, FALSE
.if direction == RIGHT
inc @nextX
.elseif direction == DOWN
inc @nextY
.elseif direction == LEFT
dec @nextX
.elseif direction == UP
dec @nextY
.endif
.if (@nextX == 0) || (@nextX > 23) || (@nextY == 0) || (@nextY > 17)
mov isAlive, 0
jmp @F
.endif
invoke _QueryNodeStat, @nextX, @nextY
.if eax
mov isAlive, 0
jmp @F
.endif
mov eax, foodX
mov ebx, foodY
.if (@nextX == eax) && (@nextY == ebx)
invoke _PieFromSky
.endif
; 头的处理,下一个位置即将激活
invoke _Activate, @nextX, @nextY, 1
push @nextX
push @nextY
pop curY
pop curX
; 把新头加入队列头,并指示移除队列尾
invoke _GetNodeAt, curX, curY
invoke _AddBody, eax, TRUE
@@:
pop ebx
ret
_Move endp
;***************************************************************************
; 窗口过程
;***************************************************************************
_ProcWinMain proc hWnd, uMsg, wParam, lParam
local @stPs:PAINTSTRUCT
local @stRect:RECT
local @hBmp: HBITMAP
local @hOldBmp: HBITMAP
local @memDC: HDC
local @hDC: HDC
mov eax, uMsg
.if eax == WM_PAINT
invoke BeginPaint, hWnd, addr @stPs
mov @hDC, eax
invoke CreateCompatibleBitmap, @hDC, wndWidth, wndHeight
mov @hBmp, eax
invoke CreateCompatibleDC, @hDC
mov @memDC, eax
invoke SelectObject, @memDC, @hBmp
mov @hOldBmp, eax
invoke _DrawBody, @memDC
invoke _DrawNode, @memDC, food
invoke _DrawScore, @memDC
invoke BitBlt, @hDC, rClient.left, rClient.top, wndWidth, wndHeight, @memDC, rClient.left, rClient.top, SRCCOPY
invoke SelectObject, @memDC, @hOldBmp
invoke DeleteObject, @hBmp
invoke DeleteDC, @memDC
invoke EndPaint, hWnd, addr @stPs
.elseif eax == WM_TIMER
.if !isAlive
invoke SetWindowText, hWnd, offset szCaptionDie
jmp @F
.endif
.if isPaused
jmp @F
.endif
mov eax, timerCounts
.if speed != eax
inc timerCounts
jmp @F
.endif
mov timerCounts, 0
invoke _Move
invoke InvalidateRect, hWnd, addr rClient, FALSE
.elseif eax == WM_CLOSE
invoke KillTimer, hWnd, TIMER
invoke DestroyWindow, hWinMain
invoke PostQuitMessage, NULL
.elseif eax == WM_CREATE
invoke SetTimer, hWnd, TIMER, 100, NULL
invoke GetClientRect, hWnd, offset rClient
mov bRanColor, TRUE
mov eax, rClient.right
sub eax, rClient.left
mov wndWidth, eax
mov eax, rClient.bottom
sub eax, rClient.top
mov wndHeight, eax
invoke _NewGame
.elseif eax == WM_KEYDOWN
mov eax, wParam
; 按P键暂停游戏
.if eax == 'P'
.if isPaused
and isPaused, 0
invoke SetWindowText, hWnd, offset szCaptionMain
.else
or isPaused, 1
invoke SetWindowText, hWnd, offset szCaptionPause
.endif
; 按了上键
.elseif eax == 'W' || eax == VK_UP
.if isPaused || bTurnPending
jmp @F
.endif
mov eax, direction
.if eax == RIGHT || eax == LEFT
push UP
pop direction
invoke SetWindowText, hWnd, offset szUp
mov bTurnPending, TRUE
.endif
; 按了下键
.elseif eax == 'S' || eax == VK_DOWN
.if isPaused || bTurnPending
jmp @F
.endif
mov eax, direction
.if eax == RIGHT || eax == LEFT
push DOWN
pop direction
invoke SetWindowText, hWnd, offset szDown
mov bTurnPending, TRUE
.endif
; 按了左键
.elseif eax == 'A' || eax == VK_LEFT
.if isPaused || bTurnPending
jmp @F
.endif
mov eax, direction
.if eax == UP || eax == DOWN
push LEFT
pop direction
invoke SetWindowText, hWnd, offset szLeft
mov bTurnPending, TRUE
.endif
; 按了右键
.elseif eax == 'D' || eax == VK_RIGHT
.if isPaused || bTurnPending
jmp @F
.endif
mov eax, direction
.if eax == UP || eax == DOWN
push RIGHT
pop direction
invoke SetWindowText, hWnd, offset szRight
mov bTurnPending, TRUE
.endif
.elseif eax == 'C'
.if bRanColor
and bRanColor, 0
.else
or bRanColor, 1
.endif
; Q或者ESC退出游戏
.elseif eax == 'Q' || eax == VK_ESCAPE
invoke PostQuitMessage, 0
; 开始新游戏
.elseif eax == 'N'
invoke _NewGame
invoke SetWindowText, hWnd, offset szCaptionMain
.endif
.else
invoke DefWindowProc, hWnd, uMsg, wParam, lParam
ret
.endif
@@:
xor eax, eax
ret
_ProcWinMain endp
;***************************************************************************
; 入口函数
;***************************************************************************
_Main proc
local @stWndClass:WNDCLASSEX
local @stMsg:MSG
mov food, FALSE
invoke RtlZeroMemory, addr @stWndClass, sizeof @stWndClass
invoke GetModuleHandle, NULL
mov @stWndClass.hInstance, eax
invoke LoadCursor, NULL, IDC_ARROW
mov @stWndClass.hCursor, eax
invoke LoadIcon, NULL, IDI_INFORMATION
mov @stWndClass.hIcon, eax
mov @stWndClass.cbSize, sizeof WNDCLASSEX
mov @stWndClass.style, CS_HREDRAW or CS_VREDRAW
mov @stWndClass.lpfnWndProc, offset _ProcWinMain
mov @stWndClass.hbrBackground, COLOR_WINDOW
mov @stWndClass.lpszClassName, offset szClassName
invoke RegisterClassEx, addr @stWndClass
; 显示窗口
invoke CreateWindowEx, WS_EX_CLIENTEDGE, offset szClassName, offset szCaptionMain,\
WS_OVERLAPPEDWINDOW and not WS_MAXIMIZEBOX and not WS_THICKFRAME, 200, 100, \
500, 404, NULL, NULL, hInstance, NULL
mov hWinMain, eax
invoke ShowWindow, hWinMain, SW_SHOWNORMAL
invoke UpdateWindow, hWinMain
; 消息循环
.while TRUE
invoke GetMessage, addr @stMsg, NULL, 0, 0
.break .if eax == 0
invoke TranslateMessage, addr @stMsg
invoke DispatchMessage, addr @stMsg
.endw
ret
_Main endp
start:
call _Main
invoke ExitProcess, NULL
end start
[注意]传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!