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:
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:
In your .code:
;================================================================================
; 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:
In your .code:
;================================================================================
; 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:
add Seed, eax
Reset the blank tile to pos 16
LastBlankTile will hold the last position of the blank tile
NrOfShuffles will count the number of shuffles
the shuffle loop:
Get coordinates (column & row) of the tile and extract X and Y
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).
Based on the value of the lowest byte of the random number, the row or column is decreased or increased by one.
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.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:
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.
jmp @st_try_again
.ENDIF
Another shuffle going to be done:
Set the current tile (which will be the blank tile) to the LastBlankTile:
Get the numbers of the two tiles that have to be swapped in eax and ecx
mov CurrentBlankTile, eax
Decrease both to get an index:
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 byte ptr [offset TileTable + eax],0
mov byte ptr [offset TileTable + ecx], bl
Get the difficulty in eax ( = nr of shuffles to do):
Done yet?
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:
;================================================================================
; 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):
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 :)