代码:
;********************************************************************************* ; 程 序: 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