Mosaic: Click handler
Now the game has to respond to mouse clicks. First of all, change the following procedure and add the second one:
CurrentBlankPos dd ?
Change the old InitGame to this:
; InitGame
;================================================================================
InitGame proc hWnd:DWORD
mov eax, offset TileTable
mov dword ptr [eax], 04030201h
mov dword ptr [eax+04h], 08070605h
mov dword ptr [eax+08h], 0C0B0A09h
mov dword ptr [eax+0Ch], 100F0E0Dh
invoke SetBitmap, hWnd, IMAGETYPE_NUMBERS
ret
InitGame endp
Aadd this procedure:
;================================================================================
; New Game
;================================================================================
NewGame proc hWnd:DWORD
; Set initial tile positions:
; not that the last index is 0, this is the blank space
mov eax, offset TileTable
mov dword ptr [eax], 04030201h
mov dword ptr [eax+04h], 08070605h
mov dword ptr [eax+08h], 0C0B0A09h
mov dword ptr [eax+0Ch], 000F0E0Dh
mov CurrentBlankPos, 16
invoke InvalidateRect, hWnd, NULL, FALSE
ret
NewGame endp
The initgame fills the TileTable array with tilenumbers in the right order (Note: the values are stored in memory reversed, so 04030201 hex will become 01 02 03 04 in memory)
NewGame is a new procedure, it also fills the TileTable, but leaves out nr 16. (byte 00 at index 15). The position of the blank tile is stored in the new variable CurrentBlankPos. InvalidateRect invalidates the main window contents, which forces a redraw (to show the new tiles).
.IF eax==WM_CREATE
... other code
invoke NewGame, hWnd ;TEMPORARY LINE!!!!!!
....
We will add the temporary line above, it starts a new game at the start of the program. It will be removed later.
9.1 - Process clicks
When you assemble the program, it will show 15 tiles and one blank space. We'll add a handler for mouseclicks now:
Two helper functions
We need two functions we use in the mousehandler:
In mosaic.inc:
TILE_BELOW equ 1
TILE_LEFT equ 2
TILE_RIGHT equ 3
In your source code:
GetTile PROTO STDCALL :DWORD
;================================================================================
; GetPosOf
;================================================================================
GetPosOf proc lpPoint:DWORD, lpPointDest:DWORD,dwType:DWORD
mov edx, lpPoint
mov ecx, lpPointDest
assume edx:ptr POINT
assume ecx:ptr POINT
push [edx].x
push [edx].y
pop [ecx].y
pop [ecx].x
.IF dwType==TILE_ABOVE
dec [ecx].y
.ELSEIF dwType==TILE_BELOW
inc [ecx].y
.ELSEIF dwType==TILE_LEFT
dec [ecx].x
.ELSEIF dwType==TILE_RIGHT
inc [ecx].x
.ENDIF
assume edx:nothing
assume ecx:nothing
ret
GetPosOf endp
;================================================================================
; GetTile
;================================================================================
GetTile proc lpPoint:DWORD
mov eax, lpPoint
assume eax:ptr POINT
mov ecx, [eax].x
cmp ecx, 3
jg gt_invalid
cmp ecx, 0
jl gt_invalid
mov edx, [eax].y
cmp edx, 3
jg gt_invalid
cmp edx, 0
jl gt_invalid
shl edx, 2 ;multiply by 4
add edx, ecx
assume eax:nothing
mov eax, edx
inc eax
ret
gt_invalid:
xor eax, eax
ret
GetTile endp
The four new constants are used to identify the relative position of a tile. For example, TILE_ABOVE identifies the tile above another tile. GetPos takes three parameters: lpPoint, lpPointDest, dwType. lpPoint is a pointer to a POINT structure. The coordinates in this structure (x and y), are not pixel coordinates, but tile coordinates (so x and y range from 0 to 3, for all rows and columns). The same applies to lpPointDest, but this structure will be filled in by the procedure. dwType is one of the tile position constants (TILE_ABOVE etc.) The function takes the tile from lpPoint, then it calculates which tile is above, next, etc. above that tile (according to dwType). The result tile is placed in lpPointDest. NOTE: This position does NOT have to be valid. It can be a tile with coordinates (-1, 3) for example. This means that there's no tile at the relative position given by dwType (e.g. there's no tile left to tile 1). This means the program has to check if the tile returned is valid before using it.
GetTile retrieves the 1-based tilenumber from a tile position. It also checks if the given coordinate is valid. It returns 0 if lpPoint indentifies an non existing tile. Otherwise it returns the tile number. It puts the x coordinate in ecx, the y coordinate in edx. It checks if the coordinates are valid with a few compares (note that jg and jl are used, these are jumps for signed compares, because coordinates can be negative). If the coordinates are valid it calculates the tilenumber by this forumula:
TileNumber = (Y * 4 + X) + 1.
The click handler
Add this new message handler to WndProc:
ELSEIF eax==WM_LBUTTONDOWN
mov eax, lParam
and eax, 0ffffh
mov ecx, lParam
shr ecx, 16
invoke ChildWindowFromPoint, hWnd, eax,ecx
.IF eax==hStatic ; clicked in static window?
invoke ProcessClick, hWnd, lParam
.ENDIF
...
The WM_LBUTTONDOWN message is sent to the main window if the user clicks on it. But we only need to process it if the user clicked on the static control. ChildWindowFromPoint returns the child window from a given coordinate. If this handle is the same as the static control window handle, the coordinate is passed to the function ProcessClick, which is defined below:
RectUpdate RECT <15,55,15+220,55+220>
.code
ProcessClick PROTO STDCALL :DWORD, :DWORD
;================================================================================
; ProcessClick
;================================================================================
ProcessClick proc uses ebx hWnd:DWORD, Pos:DWORD
LOCAL TempRect:RECT
LOCAL DestPoint:POINT
LOCAL TileCoords:POINT
; --- get the border width of the static control ---
invoke GetClientRect, hStatic, ADDR TempRect
mov eax, TempRect.bottom
mov edx, 220
sub edx, eax
shr edx, 1 ;edx contains the border width now
; --- extract the X and Y coordinates from Pos ---
mov eax, Pos
mov ecx, Pos
shr ecx, 16 ; ecx = Y (high word of pos)
and eax, 0ffffh ; eax = X (low word of pos)
; The static control is at (15, 55, 220, 220). The X and Y coordinates
; (eax and ecx) are relative to the main window, not the static control.
; First, the coordinates of the control are substracted from X and Y
; (eax=eax-15, ecx=ecx-55), then the border width (edx) of the control has to
; be substracted from X and T(eax=eax-edx, ecx=ecx-edx)
sub eax, 15
sub ecx, 55
sub eax, edx
sub ecx, edx
; The left and top margin is 9 pixels
; Check if clicked on one of the tiles (
; this is true if 9<(200+9) and 9<(200+9)
.IF eax>8 && eax<50*4+9
.IF ecx>8 && ecx<50*4+9
; Get coordinates of tile clicked on (coordinates 0,1,2,3)
sub eax, 9
sub ecx, 9
mov ebx, 50
cdq
div ebx
mov TileCoords.x, eax
mov eax, ecx
cdq
div ebx
mov TileCoords.y, eax
; coordinates are now in TileCoords
; now look if it can be moved:
xor ebx, ebx
.WHILE ebx<4 ;tile_above, left etc.
invoke GetPosOf, ADDR TileCoords, ADDR DestPoint, ebx
invoke GetTile, ADDR DestPoint
.IF eax!=NULL
dec eax
mov edx, eax
mov cl, byte ptr [offset TileTable + eax]
; cl = tile at that pos
.IF cl==NULL
push edx
invoke GetTile, ADDR TileCoords
mov ecx, eax
dec ecx
mov al, byte ptr [offset TileTable + ecx]
mov byte ptr [offset TileTable + ecx],0
pop edx
mov byte ptr [offset TileTable + edx], al
.BREAK
.ENDIF
.ENDIF
inc ebx
.ENDW
.ENDIF
.ENDIF
invoke InvalidateRect, hWnd, ADDR RectUpdate, FALSE
ret
ProcessClick endp
Let's examine this function step by step:
This part first gets the client rectangle of the static control, this is the area of the control that can be drawn. Because this area does not include the borders, we can substract this size from the size we gave the control (220x220 pixels), resulting in the border width x 2 (for both borders). Divide this value by 2 (shr edx, 1) and you have the border width of one border (in edx here).
invoke GetClientRect, hStatic, ADDR TempRect
mov eax, TempRect.bottom
mov edx, 220
sub edx, eax
shr edx, 1 ;edx contains the border width now
The X and Y coordinates are extracted from the coordinates the user clicked on
mov eax, Pos
mov ecx, Pos
shr ecx, 16 ; ecx = Y (high word of pos)
and eax, 0ffffh ; eax = X (low word of pos)
The static control is at (15, 55, 220, 220). The X and Y coordinates (eax and ecx) are relative to the main window, not the static control.
First, the coordinates of the control are substracted from X and Y (eax=eax-15, ecx=ecx-55), then the border width (edx) of the control has to be substracted from X and T(eax=eax-edx, ecx=ecx-edx)
sub ecx, 55
sub eax, edx
sub ecx, edx
The left and top margin is 9 pixels. Check if clicked on one of the tiles (this is true if 9<(200+9) and 9<(200+9))
.IF ecx>8 && ecx<50*4+9
The coordinates in pixels now need to be converted to Tile Coordinates (x=column,y=row). First the margin is substracted from the X and Y coordinates (sub eax,9 / sub edx,9). Then these coordinates are divided by 50. The result (rounded below due to integer math) will be the X and Y coordinates. These are stored in TileCoords:
sub eax, 9
sub ecx, 9
mov ebx, 50
cdq
div ebx
mov TileCoords.x, eax
mov eax, ecx
cdq
div ebx
mov TileCoords.y, eax
; coordinates are now in TileCoords
The following loop counts from 0 to 3 with ebx. 0 to 3 have the same meaning as the TILE_ABOVE, TILE_BELOW, ... constants. So the code is executed 4 times, each time looking if the tile above, below, left or right from the tile we clicked on is the current blank space. If this is true, it can be moved, otherwise is cannot:
xor ebx, ebx
.WHILE ebx<4 ;tile_above, left etc.
Get position next, below or above (depending on ebx) the tile clicked on and see if it's valid tile with GetTile.
invoke GetTile, ADDR DestPoint
If it's a valid tile (eax is not 0):
Decrease the tilenumber to get an index (dec eax, 1-based to 0-based). Get the tile at that position (in CL):
mov edx, eax
mov cl, byte ptr [offset TileTable + eax]
If the tilenumber at that position is 0, it means it's a blank space, and the clicked tile can be moved:
Swap the clicked tile and the blank space in the tiletable:
invoke GetTile, ADDR TileCoords ;get the tilepos clicked on
mov ecx, eax ;put this tilepos in ecx
dec ecx ;decrease to get index
mov al, byte ptr [offset TileTable + ecx] ;get the tilenumber
mov byte ptr [offset TileTable + ecx],0 ;put 0 in it (blank)
pop edx
mov byte ptr [offset TileTable + edx], al ;put tilenumber at blank pos
Break (out of the WHILE/ENDW loop), further processing is not needed:
.ENDIF
.ENDIF
inc ebx
.ENDW
.ENDIF
.ENDIF
Update the static control window. This is done by only invalidating the part of the main window that contains the static control. RectUpdate contains the coordinates of the control.
ret
ProcessClick endp
9.2 - Done
If you've done everything right, your files should look like this: mosaic6.zip.
When you test the program, you should be able to shuffle the tiles.