Mosaic: Time and moves

The statusbar will show the time you've been playing and the number of moves you have made.

13.1 - Variables

In your .data?:

LastUM          dd      ?
LastH           dd      ?
LastM           dd      ?
LastS           dd      ?

UserMoves       dd      ?
StartTime       dd      ?

These variables are used. LastUM, LastH, LastM and LastS hold the last usermove (counts the # of moves) and the last hour, minute and second of playing displayed. Because the time you are playing and the number of moves you've made so far are constantly shown, constantly updating it would cause screen flickering. If the values need to be updated, they are first compared to the last values and only if a value has changed it needs to be updated. This reduces flickering. UserMoves is a variable that counts the number of moves the user has made, StartTime holds -1 if the user has not yet started playing, or the result of GetTickCount when the user started playing. If GetTickCount is called again and StartTime is substracted from the result, you have the number of miliseconds since the user started playing.

13.2 - Timer

The updating is done with a timer. A timer sends a WM_TIMER message at a given interval. Each time we receive a WM_TIMER message, the program checks if an update is needed.

In mosaic.inc:

ID_TIMER1           equ     220

Add this line to the WM_CREATE handler of WndProc:

        invoke  SetTimer, hWnd, ID_TIMER1, 500, NULL

Add this as first line in the WM_DESTROY handler of WndProc:

        invoke  KillTimer, hWnd, ID_TIMER1

Add this to the messagehandler in Wndproc

.ELSEIF eax==WM_TIMER
        mov     eax, wParam
        .IF     eax==ID_TIMER1
            invoke  UpdateInfo
        .ENDIF

The timer is created with SetTimer. It creates a timer with ID: ID_TIMER1, and sets it's interval to 500ms (0.5 s). So twice a second a WM_TIMER message is sent. KillTimer deletes the timer when the program is closed. The WM_TIMER handler checks if the message comes from the timer with id ID_TIMER1, and if yes, calls UpdateInfo (see below).

13.3 - UpdateInfo

UpdateInfo calculates the number of moves the user has made and the time the user has been playing. If it has changed since the last time, the statusbar is filled with new information.

In your .data:

MovesFormat         db      "%lu move(s)",0
TimeFormat          db      "%02lu:%02lu:%02lu",0
NullString          db      0

In your .code:
[code=asm]
UpdateInfo  PROTO   STDCALL

;================================================================================
;                           UpdateInfo
;================================================================================
UpdateInfo  proc    uses ebx edi esi
LOCAL hours:DWORD
LOCAL minutes:DWORD
LOCAL seconds:DWORD
.IF     StartTime==-1 || UserMoves==0
    invoke  SendMessage, hStatus, SB_SETTEXT, 1, ADDR NullString
    invoke  SendMessage, hStatus, SB_SETTEXT, 2, ADDR NullString
.ELSE
    mov     eax, UserMoves
    .IF     eax!=LastUM
        invoke  wsprintf, ADDR Buffer, ADDR MovesFormat, UserMoves
        invoke  SendMessage, hStatus, SB_SETTEXT, 1, ADDR Buffer
    .ENDIF

    invoke  GetTickCount
    sub     eax, StartTime

    mov     ecx, 1000
    cdq
    div     ecx
    ; eax is number of seconds
    mov     ecx, 3600
    cdq
    div     ecx
    ; eax is number of hours, edx is remainder (remaining number of seconds)
    mov     hours, eax

    mov     eax, edx
    mov     ecx, 60
    cdq
    div     ecx
    ; eax is number of minutes, edx is remaining number of seconds,
    mov     minutes, eax
    mov     seconds, edx

    xor     eax, eax
    mov     ecx, LastH
    .IF     ecx!=hours
        inc     eax
    .ENDIF
    mov     ecx, LastM
    .IF     ecx!=minutes
        inc     eax
    .ENDIF
    mov     ecx, LastS
    .IF     ecx!=seconds
        inc     eax
    .ENDIF
    .IF     eax!=NULL
        invoke  wsprintf, ADDR Buffer, ADDR TimeFormat, hours, minutes, seconds
        invoke  SendMessage, hStatus, SB_SETTEXT, 2, ADDR Buffer
    .ENDIF
    push    hours
    push    minutes
    push    seconds
    pop     LastS
    pop     LastM
    pop     LastH

    push    UserMoves
    pop     LastUM
.ENDIF

ret
UpdateInfo  endp

Examination

If the starttime = -1 (user has not yet started), or the user has not made any moves yet, the statusbar is filled with Nullstrings:

.IF     StartTime==-1 || UserMoves==0
    invoke  SendMessage, hStatus, SB_SETTEXT, 1, ADDR NullString
    invoke  SendMessage, hStatus, SB_SETTEXT, 2, ADDR NullString

If the user has started:

.ELSE

Is the UserMoves variable changed since the last update?

    mov     eax, UserMoves
    .IF     eax!=LastUM

If yes, print the UserMoves as string in Buffer (see your win32 programmer's reference on wsprintf for more info about wsprintf), and put the text in the buffer on the second part of the statusbar:
invoke wsprintf, ADDR Buffer, ADDR MovesFormat, UserMoves

        invoke  SendMessage, hStatus, SB_SETTEXT, 1, ADDR Buffer
    .ENDIF

Get the current tickcount (nr of ms since startup), and substract the StartTime from it:

    invoke  GetTickCount
    sub     eax, StartTime

Eax holds the number of ms playing, divide by 1000 to get the number of seconds:

    mov     ecx, 1000
    cdq
    div     ecx
    ; eax is number of seconds

Divide by 3600 to get hours:

    mov     ecx, 3600
    cdq
    div     ecx

Eax is number of hours, edx is remainder (remaining number of seconds). Store hours.

    mov     hours, eax

Divide remaining seconds by 60 to get minutes

    mov     eax, edx
    mov     ecx, 60
    cdq
    div     ecx

eax is number of minutes, edx is remaining number of seconds. Store both:

   mov     minutes, eax
   mov     seconds, edx

Here eax is used as 'changed since last update flag'. With the xor eax, eax it is set to 0, but if any of the values (hours, minutes, seconds) has changed since the last update, eax is increased by 1.

    xor     eax, eax
    mov     ecx, LastH
    .IF     ecx!=hours
        inc     eax
    .ENDIF
    mov     ecx, LastM
    .IF     ecx!=minutes
        inc     eax
    .ENDIF
    mov     ecx, LastS
    .IF     ecx!=seconds
        inc     eax
    .ENDIF

If eax is 1 or more (1 or more values changed), thus eax!=0, the time is converted to a string with wsprintf and the 3rd part of the statusbar is filled with the time

    .IF     eax!=NULL
        invoke  wsprintf, ADDR Buffer, ADDR TimeFormat, hours, minutes, seconds
        invoke  SendMessage, hStatus, SB_SETTEXT, 2, ADDR Buffer
    .ENDIF

Make the current values the last values for the next time:

    push    hours
    push    minutes
    push    seconds
    pop     LastS
    pop     LastM
    pop     LastH

    push    UserMoves
    pop     LastUM
.ENDIF
ret
UpdateInfo  endp[/ex]

[h2]13.3 - More updates[/h2]

Sometimes an update should be forced, for example when the user moved a tile:

In ProcessClick, before .BREAK:
[code=asm]
inc     UserMoves
.IF     UserMoves==1    ;first move?
    invoke  GetTickCount
    mov     StartTime, eax
.ENDIF
invoke  UpdateInfo

This increases the UserMoves everytime a tile is clicked. The compare of UserMoves with 1 is to find out if this is the user's first move. If it is, the StartTime is set to current tickcount. So the clock starts ticking when the user makes it's first move.

Initializing everything

What also has to be done, is initializing all values:

Add this to NewGame (just before invoke ShuffleTiles):

    mov     UserMoves, 0
    mov     StartTime, -1
    mov     La[ex]s[/ex]tUM, -1
    mov     LastH, -1
    mov     LastM, -1
    mov     LastS, -1

When a new game is started, the starttime is reset, number of moves is set to 0 and all the lastXX values are set to -1.

Solved?

Now we can add a new 'solved'-message:

In your .data:

SolvedFormat        db      "Congratulations!",0ah, "You solved the puzzle in %02lu:%02lu:%02lu with %lu moves.",0

New CheckIfSolved, replace the old one:

;================================================================================
;                           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
                    ;GET TIME
                    invoke  GetTickCount
                    sub     eax, StartTime

                    mov     ecx, 1000
                    cdq
                    div     ecx
                    ; eax is number of seconds
                    mov     ecx, 3600
                    cdq
                    div     ecx
                    ; eax is number of hours, edx is remainder (remaining number of seconds)
                    mov     hours, eax

                    mov     eax, edx
                    mov     ecx, 60
                    cdq
                    div     ecx
                    ; eax is number of minutes, edx is remaining number of seconds,
                    mov     minutes, eax
                    mov     seconds, edx

                    mov     StartTime, -1
                    mov     ebx, UserMoves
                    mov     UserMoves, 0
                    invoke  InvalidateRect, hWnd, ADDR RectUpdate, FALSE
                    invoke  wsprintf, ADDR Buffer, ADDR SolvedFormat,\
                             hours, minutes, seconds, ebx
                    invoke  MessageBox, NULL, ADDR Buffer, ADDR AppName, MB_OK + MB_ICONEXCLAMATION

                .ENDIF
            .ENDIF
        .ENDIF
    .ENDIF
ret
CheckIfSolved endp

The procedure is the same as the previous one, but instead of the simple messagebox, the time is calculated (same code as in UpdateInfo) and together with the UserMoves shown in the messagebox.

13.4 - Done

You should now see a clock ticking and your moves count when you play the game:

Time is included

Reward with time

Current project code here: mosaic10.zip.