Mosaic: Making it fun

Only shuffling pieces that are already in the right order on start can be pretty boring ;-). What we need is a procedure that shuffles the tiles so the puzzle can be solved.

12.1 - Removing the temporary code

First remove this line from the WM_CREATE handler in WndProc:

invoke NewGame, hWnd ;TEMPORARY LINE!!!!!!

This way we can test the 'New game' button & menu item correctly.

12.2 - Random function

To random shuffle the tiles, we need a random function.

In your .data:

Seed                dd  89456278h

In your .code:

GetRandomNumber     PROTO STDCALL
;================================================================================
;                           GetRandomNumber
;================================================================================
GetRandomNumber proc uses edx ecx ebx
   mov      eax, Seed
   mov      edx, 84054842h
   mul      edx
   inc      eax
   push     eax
   invoke   GetTickCount
   mov      ebx, eax
   invoke   GetTickCount
   bswap    eax
   mov      ecx, eax
   pop      eax
   add      eax, ecx
   mov      Seed, eax
   add      Seed, ebx

ret
GetRandomNumber endp

This function is not perfectly random, but it will do for our purposes. It does some calculations on the current Seed value, changing the Seed each time. GetTickCount is used to get 'a bit' random numbers, it returns the number of miliseconds since the computer has started.

12.3 - ShuffleTiles

Add this to NewGame, before InvalidateRect:

    invoke  ShuffleTiles

In your .code:

ShuffleTiles        PROTO STDCALL

;================================================================================
;                           ShuffleTiles
;================================================================================
ShuffleTiles    proc uses ebx
LOCAL   CurrentBlankTile:DWORD
LOCAL   TempPoint:POINT
LOCAL   NrOfShuffles:DWORD
LOCAL   LastBlankTile:DWORD

    invoke  GetTickCount
    add     Seed, eax
    mov     CurrentBlankTile, 16
    mov     LastBlankTile, 0
    mov     NrOfShuffles, 0
@st_try_again:
    invoke  GetCoordinates, CurrentBlankTile
    ;   int     3
    mov     ecx, eax
    mov     edx, eax
    and     edx, 0ffffh ;edx = x
    shr     ecx, 16     ;ecx = y
    invoke  GetRandomNumber
    .IF     al<40h
        dec ecx
    .ELSEIF al>=40h && al <80h
        inc edx
    .ELSEIF al>=80h && al <0C0h
        inc ecx
    .ELSE
        dec edx
    .ENDIF
    mov     TempPoint.x, ecx
    mov     TempPoint.y, edx
    invoke  GetTile, ADDR TempPoint
    .IF     eax==NULL
        jmp     @st_try_again
    .ENDIF
    .IF     LastBlankTile==eax
        jmp     @st_try_again
    .ENDIF
    inc     NrOfShuffles


    mov     LastBlankTile, eax
    mov     ecx, CurrentBlankTile
    mov     CurrentBlankTile, eax
    dec     eax
    dec     ecx
    mov     bl, byte ptr [offset TileTable + eax]
    mov     byte ptr [offset TileTable + eax],0
    mov     byte ptr [offset TileTable + ecx], bl
    mov     eax, Difficulty
    .IF     NrOfShuffles<eax
        jmp     @st_try_again
    .ENDIF
ret
ShuffleTiles    endp

When a new game is started (NewGame), ShuffleTiles is called. This function shuffles the tiles according to the difficulty level. The difficulty level in Difficulty is actually the number of times a tile is moved when shuffling.

Explanation

What basically happens, is that in the shuffle loop, each time at random, one of the tiles surrounding the blank tile is swapped with the blank tile.

Get a new seed from the system timer:

    invoke  GetTickCount
    add     Seed, eax

Reset the blank tile to pos 16

    mov     CurrentBlankTile, 16

LastBlankTile will hold the last position of the blank tile

    mov     LastBlankTile, 0

NrOfShuffles will count the number of shuffles

    mov     NrOfShuffles, 0

the shuffle loop:

@st_try_again:

Get coordinates (column & row) of the tile and extract X and Y

    invoke  GetCoordinates, CurrentBlankTile
    mov     ecx, eax
    mov     edx, eax
    and     edx, 0ffffh ;edx = x
    shr     ecx, 16     ;ecx = y

Get a random number (note: ecx and edx are preserved by GetRandomNumber. With normal functions, ecx and edx would be lost).

    invoke  GetRandomNumber

Based on the value of the lowest byte of the random number, the row or column is decreased or increased by one.

    .IF     al<40h
        dec ecx
    .ELSEIF al>=40h && al <80h
        inc edx
    .ELSEIF al>=80h && al <0C0h
        inc ecx
    .ELSE
        dec edx
    .ENDIF

Store the new row & column values in a POINT structure and get the tilenumber at that position:

    mov     TempPoint.x, ecx
    mov     TempPoint.y, edx
    invoke  GetTile, ADDR TempPoint

As increasing or decreasing the row or column might give invalid X or Y coordinate (negative or out of borders). In that case, GetTile returns 0, and the above code is tried again:

    .IF     eax==NULL
        jmp     @st_try_again
    .ENDIF

If the tile we are going to swap with the blank tile was the last blank tile, try again too. If we would allow this, a swap can undo the previous swap, and this would be pretty useless.

    .IF     LastBlankTile==eax
        jmp     @st_try_again
    .ENDIF

Another shuffle going to be done:

    inc     NrOfShuffles

Set the current tile (which will be the blank tile) to the LastBlankTile:

    mov     LastBlankTile, eax

Get the numbers of the two tiles that have to be swapped in eax and ecx

    mov     ecx, CurrentBlankTile
    mov     CurrentBlankTile, eax

Decrease both to get an index:

    dec     eax
    dec     ecx

Swap the tiles (move the tilenumber (bl) at the position with the tile to the position with the blank space, and put 0 (blank) in the position of the tile).

    mov     bl, byte ptr [offset TileTable + eax]
    mov     byte ptr [offset TileTable + eax],0
    mov     byte ptr [offset TileTable + ecx], bl

Get the difficulty in eax ( = nr of shuffles to do):

    mov     eax, Difficulty

Done yet?

    .IF     NrOfShuffles<eax
        jmp     @st_try_again
    .ENDIF

12.4 - Solved?

It will also be nice to see a message if you solved the puzzle. Add a new procedure for it:

In your .code:

CheckIfSolved        PROTO STDCALL    :DWORD
;================================================================================
;                            Check if puzzle is solved
;================================================================================
CheckIfSolved proc uses ebx hWnd:DWORD
LOCAL hours:DWORD
LOCAL minutes:DWORD
LOCAL seconds:DWORD
    mov        eax, offset TileTable
    .IF        dword ptr [eax]==04030201h
        .IF        dword ptr [eax+4]==08070605h
            .IF     dword ptr [eax+8]==0C0B0A09h
                .IF        dword ptr [eax+0Ch]==000F0E0Dh
                    mov        dword ptr [eax+0Ch], 100F0E0Dh
                    invoke    MessageBox, NULL, ADDR AppName, \
                                 ADDR AppName, MB_OK + MB_ICONEXCLAMATION
                .ENDIF
            .ENDIF
        .ENDIF
    .ENDIF
ret
CheckIfSolved endp

In ProcessClick, before InvalidateRect (at the last line):

    invoke    CheckIfSolved, hWnd

When you click a tile, the normal processing is done, then CheckIfSolved is called. This procedure compares 4 dwords of the TileTable (4 dwords = 4 * 4 bytes = 16 tiles) with the values they will have when the puzzle is solved. If all IFs match, the tile that is blank (nr16), is filled with nr 16 to complete the figure (mov dword ptr [eax+0Ch], 100F0E0Dh). Then a simple messagebox is shown that displays the application's name. We will change this message later.

When the puzzle is set to easy level, it's fairly easy to solve it. Try it and you will see that the picture will be completed and a messagebox is shown.

Current project files here: mosaic9.zip

The great reward :)

The great reward