
=head1 NAME

Mines::Field - A minesweeper field

=head1 SYNOPSIS

    # setup field properties
    $P0 = new 'Hash'
    $P0['width']  = 40
    $P0['height'] = 28
    $P0['mines']  = 0.1075
    $P0['level']  = 123
    $P0['screen'] = screen
    $P0['debug']  = debug

    # create the field
    field = new "Mines::Field", $P0

    # draw the field
    field.'draw'()

=head1 DESCRIPTION

This is an implementation of a Minesweeper game field.
It draws the field onto a SDL surface.

=head1 METHODS

An Mines::Field object has the following methods:

=over 4

=cut

.include "iterator.pasm"
.loadlib 'math_ops'
.namespace ["Mines::Field"]

# size of a single mine field, in pixels
.const int FIELD_WIDTH  = 16
.const int FIELD_HEIGHT = 16

# mine field values
.const int VAL_INVALID            = -1
.const int VAL_FREE               =  0
.const int VAL_1                  =  1
.const int VAL_2                  =  2
.const int VAL_3                  =  3
.const int VAL_4                  =  4
.const int VAL_5                  =  5
.const int VAL_6                  =  6
.const int VAL_7                  =  7
.const int VAL_8                  =  8
.const int VAL_MINE_EXPLODED      =  9
.const int VAL_NO_MINE            = 10
.const int VAL_MINE               = 11
.const int VAL_FLAG_NO_MINE       = 12
.const int VAL_FLAG_MINE          = 13
.const int VAL_UNSURE_NO_MINE     = 14
.const int VAL_UNSURE_MINE        = 15
.const int VAL_UNEXPLORED_NO_MINE = 16
.const int VAL_UNEXPLORED_MINE    = 17
.const int VAL_MARK_NO_MINE       = 18
.const int VAL_MARK_MINE          = 19

# game status
.const int STATUS_PLAYING  = 0
.const int STATUS_CHOOSING = 1
.const int STATUS_LOST     = 2
.const int STATUS_SLEEPING = 3
.const int STATUS_WON      = 4


=item __init( hash )

The constructor. You have to pass a hash filled with
the following values:

=over 4

=item width

Number of horizontal units the field should have.

=item height

Number of vertical units the field should have.

=item level

Initial seed for the PRNG that is used to calculate the field.

=item mines

The amount of mines. 0.0 means no mines, 1.0 means mines everywhere.

=item screen

SDL surface to use for drawing.

=item debug

0 = normal mode
1 = use debug image (position of mines are visible)

=back

=cut

.sub __init :method
    .param pmc args
    .local pmc field
    .local pmc screen
    .local pmc cache
    .local int width
    .local int height
    .local int level
    .local num mines
    .local int val
    .local int debug
    .local pmc lcd
    .local pmc watch

    # get the arguments
    width  = args['width']
    height = args['height']
    level  = args['level']
    mines  = args['mines']
    screen = args['screen']
    debug  = args['debug']
    field  = new 'ResizablePMCArray'
    cache  = new 'ResizablePMCArray'

    watch = new ['SDL'; 'StopWatch'], screen
    watch.'xpos'( 515 )
    watch.'ypos'( 5 )

    lcd = new ['SDL'; 'LCD']
    # This seems to call __init() with too many parameters
    # lcd = 0
    lcd.'_digits'( 4 )
    lcd.'xpos'( 5 )
    lcd.'ypos'( 5 )

    setattribute self, 'field', field
    setattribute self, 'cache', cache
    setattribute self, 'screen', screen
    setattribute self, 'mines_lcd', lcd
    setattribute self, 'watch', watch

    $P0 = new 'Integer'
    $P0 = width
    setattribute self, 'width', $P0

    $P0 = new 'Integer'
    $P0 = height
    setattribute self, 'height', $P0

    $P0 = new 'Float'
    $P0 = mines
    setattribute self, 'mines', $P0

    $P0 = new 'Integer'
    $P0 = -1
    setattribute self, 'markpos', $P0

    $P0 = new 'Integer'
    $P0 = debug
    setattribute self, 'debug', $P0

    # button
    $P0 = new 'String'
    $P0 = "examples/sdl/minesweeper/smiley.png"
    $P0 = new ['SDL'; 'Button'], $P0
    $P0.'states'( 5 )
    $P0.'pos'( 305, 2 )
    $P0.'size'( 30, 30 )

    $P1 = get_hll_global [ 'Mines::Field' ], '_button_clicked'
    $P0.'setAction'( STATUS_PLAYING, $P1 )
    $P0.'setAction'( STATUS_WON, $P1 )
    $P0.'setAction'( STATUS_LOST, $P1 )

    setattribute self, 'status_button', $P0

    self.'newLevel'( level )
.end

=item newLevel( level )

Creates a new level, with the given number as initial seed for the PRNG.

=cut

.sub newLevel :method
    .param int level
    .local pmc field
    .local pmc cache
    .local int size
    .local int width
    .local int height
    .local int val
    .local num mines

    field  = getattribute self, 'field'
    cache  = getattribute self, 'cache'

    $P0    = getattribute self, 'witdh'
    width  = $P0

    $P0    = getattribute self, 'height'
    height = $P0

    $P0    = getattribute self, 'mines'
    mines  = $P0

    size   = width * height

    field  = 0
    cache  = 0

    # fill the field
LOOP:
    level += size

    $I0 = level
    shl $I0, 13
    $I0 = or $I0, level

    $I1 = $I0 * $I0
    $I1 *= 15731
    $I1 += 789221

    $I1 *= $I0
    $I1 += 1376312589

    $N0 = $I1
    $N0 = $N0 / 1073741824.0
    $N0 = 100.0 + $N0
    $N0 *= 100.0
    $I0 = $N0
    $N0 = $N0 - $I0
    level += $I0

    val = VAL_UNEXPLORED_NO_MINE
    ge $N0, mines, NO_MINE
    val = VAL_UNEXPLORED_MINE
NO_MINE:

    push field, val
    push cache, VAL_INVALID

    dec size
    if size <= 0 goto END
    branch LOOP
END:
    # set the game status
    self.'setStatus'( STATUS_PLAYING )
    # reset the watch
    $P0 = getattribute self, "Mines::Field\x0watch"
    $P0.'reset'()
.end

=item draw()

Draws the field, then the LCDs and the smiley button.

=cut

.sub draw :method
    .local pmc screen
    .local pmc image
    .local pmc dest_rect
    .local pmc src_rect
    .local int width
    .local int height
    .local int size
    .local int i
    .local pmc field
    .local pmc cache
    .local pmc cacheit
    .local int debug
    .local int minx
    .local int miny
    .local int maxx
    .local int maxy
    .local pmc mines_lcd
    .local pmc watch
    .local pmc status

    getattribute field, self, 'field'
    field   = iter field
    field   = .ITERATE_FROM_START

    cache   = getattribute self, 'cache'
    cacheit = iter cache
    cacheit = .ITERATE_FROM_START

    screen  = getattribute self, 'screen'

    $P0     = getattribute self, 'width'
    width   = $P0

    $P0     = getattribute self, 'height'
    height  = $P0

    $P0     = getattribute self, 'debug'
    debug   = $P0

    minx    = width  * FIELD_WIDTH
    miny    = height * FIELD_HEIGHT
    maxx    = 0
    maxy    = 0

    if debug goto DEBUG
    image = get_hll_global [ "Mines::Field" ], "field"
    branch IMAGE_OK
DEBUG:
    image = get_hll_global [ "Mines::Field" ], "field_debug"
IMAGE_OK:
    $P0 = new 'Hash'
    $P0['x'] = 0
    $P0['y'] = 0
    $P0['width']  = 0
    $P0['height'] = 0
    dest_rect = new ['SDL'; 'Rect'], $P0
    $P0['width']  = FIELD_WIDTH
    $P0['height'] = FIELD_HEIGHT
    src_rect = new ['SDL'; 'Rect'], $P0

    set size, width
    mul size, height

    i = 0
LOOP:
    if i >= size goto END

    $P0 = shift field
    $I0 = $P0
    $P0 = shift cacheit
    $I1 = $P0

    # current field already drawn?
    if $I0 == $I1 goto NEXT

    cache[i] = $I0

    $I1 = cmod i, width
    $I2 = div i, width

    mul $I1, FIELD_WIDTH
    mul $I2, FIELD_HEIGHT

    add $I1, 0
    add $I2, 32

    dest_rect.'x'( $I1 )
    dest_rect.'y'( $I2 )

    if $I1 > minx goto SKIP1
    minx = $I1
SKIP1:

    if $I2 > miny goto SKIP2
    miny = $I2
SKIP2:

    if $I1 < maxx goto SKIP3
    maxx = $I1
SKIP3:

    if $I2 < maxy goto SKIP4
    maxy = $I2
SKIP4:

    $I1 = cmod $I0, 5
    $I2 = div $I0, 5

    mul $I1, FIELD_WIDTH
    mul $I2, FIELD_HEIGHT

    src_rect.'x'( $I1 )
    src_rect.'y'( $I2 )

    screen.'blit'( image, src_rect, dest_rect )
NEXT:
    inc i
    branch LOOP
END:

    add maxx, FIELD_WIDTH
    add maxy, FIELD_HEIGHT

    if maxx <= minx goto NO_SCREEN_UPDATE
    if maxy <= miny goto NO_SCREEN_UPDATE

    dest_rect.'x'( minx )
    dest_rect.'y'( miny )
    $I0 = maxx - minx
    dest_rect.'width'( $I0 )
    $I0 = maxy - miny
    dest_rect.'height'( $I0 )

    screen.'update_rect'( dest_rect )
NO_SCREEN_UPDATE:

    # update the LCDs
    self.'update_stats'()

    $P0 = getattribute self, "Mines::Field\x0mines_lcd"
    $P0.'draw'( screen )

    $P0 = getattribute self, "Mines::Field\x0watch"
    $P0.'draw'( screen )

    $P0 = getattribute self, "Mines::Field\x0status_button"
    $P0.'draw'( screen )
.end

=item reveal( x, y )

Reveals the field at the specified location.

Returns 1 if successful, 0 otherwise.

=cut

.sub reveal :method
    .param int x
    .param int y
    .local int width
    .local int height
    .local pmc watch

    self."undo_mark"()

    $P0    = getattribute self, 'width'
    width  = $P0

    $P0    = getattribute self, 'height'
    height = $P0

    watch  = getattribute self, 'watch'

    $P0    = getattribute self, 'status'
    if $P0 > STATUS_CHOOSING goto END

    x -= 0
    y -= 32

    if x < 0 goto END
    if y < 0 goto END

    x /= FIELD_WIDTH
    y /= FIELD_HEIGHT

    watch.'start'()
    self.'reveal_recursive'( x, y, width, height )
    self.'check_end'()
    self.'draw'()

    .return (1)

END:
    .return (0)
.end

=item setFlag( x, y )

Sets a flag at the specified location.

Returns 1 if successful, 0 otherwise.

=cut

.sub setFlag :method
    .param int x
    .param int y
    .local int width
    .local pmc field
    .local pmc watch
    .local int pos
    .local int v

    self."undo_mark"()

    $P0   = getattribute self, 'field'
    field = $P0

    $P0   = getattribute self, 'width'
    width = $P0

    watch = getattribute self, 'watch'

    $P0   = getattribute self, 'status'
    if $P0 > STATUS_CHOOSING goto END

    x -= 0
    y -= 32

    if x < 0 goto END
    if y < 0 goto END

    x /= FIELD_WIDTH
    y /= FIELD_HEIGHT

    pos = y * width
    pos += x

    v = field[pos]

    if v == VAL_FLAG_NO_MINE       goto PLUS2
    if v == VAL_FLAG_MINE          goto PLUS2
    if v == VAL_UNSURE_NO_MINE     goto PLUS2
    if v == VAL_UNSURE_MINE        goto PLUS2
    if v == VAL_UNEXPLORED_NO_MINE goto MINUS4
    if v == VAL_UNEXPLORED_MINE    goto MINUS4
    branch END

PLUS2:
    v += 2
    branch DRAW

MINUS4:
    v -= 4

DRAW:
    field[pos] = v

    watch.'start'()
    self."check_end"()
    self."draw"()

    .return (1)

END:
    .return (0)
.end

=item undo_mark( i ) B<(internal)>

Removes the field mark.

Parameters:

=over 4

=item i

Optional. The value does not matter. If you pass an integer value,
the game status is not set. If you pass no integer, the game status
will be set.

Just do not pass an integer, it is for internal use only.
(C<setStatus()> calls undo_mark, which in turn calls setStatus; this
argument is used to avoid recursion)

=back

This method returns nothing.

=cut

.sub undo_mark :method
    .param int i      :optional
    .param int has_i  :opt_flag

    .local pmc markpos
    .local pmc field
    .local pmc status

    $P0     = getattribute self, 'field'
    field   = $P0

    markpos = getattribute self, 'markpos'
    status  = getattribute self, 'status'

    $I0     = markpos

    if $I0 == -1 goto UNDO_DONE
    $I1 = field[$I0]
    $I1 -= 2
    field[$I0] = $I1
UNDO_DONE:
    markpos = -1

    if has_i goto END
    if status != STATUS_CHOOSING goto END
    self."setStatus"( STATUS_PLAYING )
END:
.end

=item mark( x, y )

Sets a mark on the specified field.
This is used to give feedback which field will be revealed.
Only one field is marked at any time, a previous mark will be
removed if you call this method.

=cut

.sub mark :method
    .param int x
    .param int y
    .local int width
    .local pmc markpos
    .local pmc field
    .local pmc watch
    .local int pos
    .local int v

    self."undo_mark"()

    $P0     = getattribute self, 'field'
    field   = $P0

    $P0     = getattribute self, 'width'
    width   = $P0

    markpos = getattribute self, 'markpos'

    x -= 0
    y -= 32

    watch = getattribute self, 'watch'

    $P0   = getattribute self, 'status'
    if $P0 >= STATUS_CHOOSING goto END

    if x < 0 goto END
    if y < 0 goto END

    x /= FIELD_WIDTH
    y /= FIELD_HEIGHT

    pos = y * width
    pos += x

    v = field[pos]

    if v == VAL_UNEXPLORED_NO_MINE goto MARK
    if v == VAL_UNEXPLORED_MINE goto MARK
    branch END
MARK:
    self."setStatus"( STATUS_CHOOSING )
    markpos = pos
    v += 2
    field[pos] = v
DRAW:
    watch.'start'()
    self."check_end"()
    self."draw"()

    .return (1)

END:
    .return (0)
.end

=item click( x, y, b )

This method is used to send mouse click events to the smiley button.

Parameters:

=over 4

=item x

The vertical mouse position.

=item y

The horizontal mouse position.

=item b

0 = mouse button not pressed

1 = mouse button pressed

=back

=cut

.sub click :method
    .param int x
    .param int y
    .param int b

    if y > 32 goto END

    $P0 = getattribute self, 'status_button'
    $I0 = $P0.'click'( x, y, b, self )
    unless $I0 goto END

    $P1 = getattribute self, 'screen'

    $P0."draw"( $P1 )
END:

.end

=item reveal_recursive( x, y, width, height ) B<(internal)>

Reveals the specified field recursively.

The width and height are the dimensions of the board, they
have to be specified for performance reasons.

=cut

.sub reveal_recursive :method
    .param int x
    .param int y
    .param int width
    .param int height
    .local pmc field
    .local int pos

    if x < 0 goto END
    if y < 0 goto END
    if x >= width goto END
    if y >= height goto END

    pos = y * width
    pos += x

    $P0   = getattribute self, 'field'
    field = $P0

    $I0 = field[pos]
    if $I0 == VAL_FREE goto END
    if $I0 == VAL_UNEXPLORED_NO_MINE goto UNEXPLORED
    if $I0 == VAL_UNEXPLORED_MINE goto UNEXPLORED_MINE
    branch END

UNEXPLORED:
    $I0 = _mines_near( field, x, y, width, height )
    field[pos] = $I0
    if $I0 != 0 goto END

    dec y
    dec x
    self.'reveal_recursive'( x, y, width, height )
    inc x
    self.'reveal_recursive'( x, y, width, height )
    inc x
    self.'reveal_recursive'( x, y, width, height )

    inc y
    dec x
    dec x
    self.'reveal_recursive'( x, y, width, height )
    inc x
    self.'reveal_recursive'( x, y, width, height )
    inc x
    self.'reveal_recursive'( x, y, width, height )

    inc y
    dec x
    dec x
    self.'reveal_recursive'( x, y, width, height )
    inc x
    self.'reveal_recursive'( x, y, width, height )
    inc x
    self.'reveal_recursive'( x, y, width, height )
    branch END

UNEXPLORED_MINE:
    field[pos] = VAL_MINE_EXPLODED
    self.'lost'()

END:
    .return ()
.end

=item lost()

Called if a mine is revealed.
Shows the position of all mines on the field.

=cut

.sub lost :method
    .local pmc field
    .local int i
    .local int max

    self."setStatus"( STATUS_LOST )

    field = getattribute self, 'field'
    max   = field
    i     = 0

LOOP:
    if i >= max goto END

    $I1 = field[i]

    if $I1 == VAL_UNEXPLORED_MINE goto MINE
    if $I1 == VAL_UNSURE_MINE goto MINE
    if $I1 == VAL_FLAG_NO_MINE goto NO_MINE
    branch NEXT

MINE:
    $I1 = VAL_MINE
    branch NEXT

NO_MINE:
    $I1 = VAL_NO_MINE
    branch NEXT

NEXT:
    field[i] = $I1
    inc i
    branch LOOP
END:
.end

=item won()

Called when you have won.

=cut

.sub won :method
    self."setStatus"( STATUS_WON )
.end

=item check_end()

Checks if you have won.

=cut

.sub check_end :method
    .local pmc field

    field = getattribute self, 'field'
    field = iter field
    field = .ITERATE_FROM_START
LOOP:
    unless field goto WON

    $I0 = shift field

    if $I0 <= 8 goto LOOP

    if $I0 == VAL_MINE goto END
    if $I0 == VAL_MINE_EXPLODED goto END
    if $I0 == VAL_UNSURE_MINE goto END
    if $I0 == VAL_FLAG_NO_MINE goto END
    if $I0 == VAL_UNEXPLORED_MINE goto END

    branch LOOP

WON:
    self."won"()
    .return (1)
END:
    .return (0)
.end

=item setStatus( newStatus )

Sets a new game status.

=cut

.sub setStatus :method
    .param int s
    .local pmc screen
    .local pmc watch
    .local pmc status

    screen = getattribute self, 'screen'
    watch  = getattribute self, 'watch'
    status = getattribute self, 'status'

    if s == STATUS_PLAYING goto OK
    if s == STATUS_CHOOSING goto OK

    # not playing at the moment, stop the watch
    watch.'stop'()
OK:
    # no need to start the watch here

    status = s
    self."undo_mark"( 1 )
    watch.'draw'( screen )

DONE:
.end

=item update_stats()

Counts the unrevealed mines and updates the LCD.

=cut

.sub update_stats :method
    .local pmc field
    .local int size
    .local pmc count
    .local pmc mines_lcd

    field     = getattribute self, 'field'
    mines_lcd = getattribute self, 'mines_lcd'

    size  = field
    field = iter field
    field = .ITERATE_FROM_START
    count = new 'ResizablePMCArray'

LOOP:
    unless field goto END
    $I0 = shift field
    $I1 = count[$I0]
    inc $I1
    count[$I0] = $I1
    branch LOOP
END:

    $I0 = count[VAL_UNEXPLORED_MINE]
    $I1 = count[VAL_UNSURE_MINE]
    $I2 = count[VAL_FLAG_NO_MINE]
    $I3 = count[VAL_MINE]
    $I4 = count[VAL_MINE_EXPLODED]
    $I5 = count[VAL_MARK_MINE]

    $I0 += $I1
    $I0 -= $I2
    $I0 += $I3
    $I0 += $I4
    $I0 += $I5
    mines_lcd = $I0
.end

=back

=head1 INTERNAL FUNCTIONS

=over 4

=item __onload()

Loads necessary modules and creates the B<Mines::Field> class.
This method is called automatically when this module is loaded.

=cut

.sub __onload :load
    .local pmc class
    class = get_class "Mines::Field"
    unless null class goto END

    .local pmc tmp
    .local pmc size
    .local pmc size_rect
    .local pmc prop
    .local pmc from_rect
    .local pmc image
    .local int rect_type
    .local int surface_type

    load_bytecode "examples/sdl/minesweeper/eventhandler.pir"
    load_bytecode "library/SDL/Button.pir"
    load_bytecode "library/SDL/StopWatch.pir"
    load_bytecode "library/SDL/Image.pir"

    # XXX: remove
    load_bytecode "library/Data/Dumper.pir"

    $P0 = new 'String'
    $P0 = "examples/sdl/minesweeper/mines.png"
    image = new ['SDL'; 'Image'], $P0
    set_hll_global [ "Mines::Field" ], "field", image

    $P0 = new 'String'
    $P0 = "examples/sdl/minesweeper/mines_debug.png"
    image = new ['SDL'; 'Image'], $P0
    set_hll_global [ "Mines::Field" ], "field_debug", image

    newclass $P0, "Mines::Field"
    addattribute $P0, 'field'
    addattribute $P0, 'cache'
    addattribute $P0, 'screen'
    addattribute $P0, 'width'
    addattribute $P0, 'height'
    addattribute $P0, 'mines'
    addattribute $P0, 'markpos'
    addattribute $P0, 'debug'
    addattribute $P0, 'mines_lcd'
    addattribute $P0, 'watch'
    addattribute $P0, 'status_button'
END:
.end

=item _mines_near( field, x, y, width, height )

Returns the number of mines in the neighbourhood
of the field at the specified location.

The width and height are the dimensions of the board.

=cut

.sub _mines_near
    .param pmc field
    .param int x
    .param int y
    .param int width
    .param int height
    .local int mines
    .local int i
    .local int i1
    .local int i2
    .local int j
    .local int j2
    .local int pos

    mines = 0

    set i1, x
    dec i1
    set i2, x
    inc i2

    set j, y
    dec j
    set j2, y
    inc j2

LOOPj:
    i = i1
LOOPi:
    if i > i2 goto ENDi
    if j < 0 goto ENDi
    if j >= height goto ENDi
    if i < 0 goto NEXTi
    if i >= width goto NEXTi

    pos = j * width
    pos += i

    $I0 = _mine_at( field, pos )
    unless $I0 goto NEXTi

    inc mines

NEXTi:
    inc i
    branch LOOPi
ENDi:
    inc j
    if j <= j2 goto LOOPj

    .return (mines)
.end

=item _mine_at( field, pos )

Returns 1 if a mine is in the given field
at the specified position, 0 otherwise.

=cut

.sub _mine_at
    .param pmc field
    .param int pos
    .local int v

    v = field[pos]
    if v == VAL_MINE goto MINE
    if v == VAL_MINE_EXPLODED goto MINE
    if v == VAL_FLAG_MINE goto MINE
    if v == VAL_UNSURE_MINE goto MINE
    if v == VAL_UNEXPLORED_MINE goto MINE
    if v == VAL_MARK_MINE goto MINE
    .return (0)
MINE:
    .return (1)
.end

=item _button_clicked( onField )

Called if you click the smiley button.
It starts a new level.

=cut

.sub _button_clicked
    .param pmc field

    $I0 = time
    field.'newLevel'( $I0 )
    field.'draw'()
.end

=back

=head1 AUTHOR

Jens Rieks E<lt>parrot at jensbeimsurfen dot deE<gt> is the author
and maintainer.
Please send patches and suggestions to the Perl 6 Internals mailing list.

=head1 COPYRIGHT

Copyright (C) 2004-2008, Parrot Foundation.

=cut

# Local Variables:
#   mode: pir
#   fill-column: 100
# End:
# vim: expandtab shiftwidth=4 ft=pir:
