Mosaic: Click handler

Now the game has to respond to mouse clicks. First of all, change the following procedure and add the second one:

.data?
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:

NewGame    PROTO    STDCALL :DWORD
;================================================================================
;                            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_ABOVE equ 0
TILE_BELOW equ 1
TILE_LEFT  equ 2
TILE_RIGHT equ 3

In your source code:

GetPosOf PROTO STDCALL :DWORD, :DWORD, :DWORD
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:

.data
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).

   ; --- 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

The X and Y coordinates are extracted from the coordinates the user clicked on

    ; --- 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

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:

            ; 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

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:

            ; now look if it can be moved:
            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  GetPosOf, ADDR TileCoords, ADDR DestPoint, ebx
                invoke  GetTile,  ADDR DestPoint

If it's a valid tile (eax is not 0):

                .IF     eax!=NULL

Decrease the tilenumber to get an index (dec eax, 1-based to 0-based). Get the tile at that position (in CL):

                    dec     eax
                    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:

                    .IF     cl==NULL

Swap the clicked tile and the blank space in the tiletable:

                        push    edx      ;save the blank space tile
                        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:

                        .BREAK
                    .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.

    invoke  InvalidateRect, hWnd, ADDR RectUpdate, FALSE
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.