File Coverage

blib/lib/Game/Marad.pm
Criterion Covered Total %
statement 103 106 97.1
branch 42 42 100.0
condition 33 33 100.0
subroutine 13 14 92.8
pod 8 8 100.0
total 199 203 98.0


line stmt bran cond sub pod time code
1             # -*- Perl -*-
2              
3             package Game::Marad 0.06;
4 2     2   238710 use 5.26.0;
  2         17  
5 2     2   1771 use Object::Pad 0.52;
  2         27390  
  2         10  
6             class Game::Marad :strict(params);
7              
8             use constant {
9 2         5371 BOARD_SIZE => 9,
10             MIDDLE => 4, # the middle square is used for scoring
11              
12             MAX_MOVES => 4,
13              
14             TYPE_MASK => 0b0000_1111, # piece type
15             PLAYER_BIT => 6, # owned by player 1 or 2?
16             MOVED_MASK => 0b1000_0000, # has the piece moved?
17              
18             MOVE_NOPE => 0,
19             MOVE_SQUARE => 1, # NOTE also Rook
20             MOVE_DIAGONAL => 2, # NOTE also Bishop
21              
22             PIECE_EMPTY => 0,
23             PIECE_ROOK => 1,
24             PIECE_BISHOP => 2,
25             PIECE_KING => 3, # NOTE Rook and Bishop bits are set
26              
27             XX => 0,
28             YY => 1,
29 2     2   870 };
  2         4  
30              
31             # NOTE the client is expected to be well behaved and to not modify the
32             # contents of board nor the score (hiding these references would only
33             # make such impolite behavior slightly more difficult)
34 1     1 1 4 has $board :reader;
  1         10  
35 2     2 1 8 has $move_count :reader;
  2         7  
36 3     3 1 11 has $player :reader;
  3         14  
37 5     5 1 407 has $score :reader;
  5         32  
38              
39             ADJUST {
40             $board = [
41             [qw/0 0 0 0 0 0 0 0 0/], # starting game state
42             [qw/0 2 0 0 0 0 0 66 0/],
43             [qw/0 1 0 0 0 0 0 65 0/],
44             [qw/0 1 0 0 0 0 0 65 0/],
45             [qw/0 3 0 0 0 0 0 67 0/],
46             [qw/0 1 0 0 0 0 0 65 0/],
47             [qw/0 1 0 0 0 0 0 65 0/],
48             [qw/0 2 0 0 0 0 0 66 0/],
49             [qw/0 0 0 0 0 0 0 0 0/],
50             ];
51             $move_count = _move_count();
52             $player = 0;
53             $score = [ 0, 0 ];
54             }
55              
56             ########################################################################
57             #
58             # METHODS
59              
60 7     7 1 28 method is_owner( $x, $y ) {
  7         13  
  7         8  
  7         9  
  7         9  
61 7 100 100     65 return 0 if $x < 0 or $x >= BOARD_SIZE or $y < 0 or $y >= BOARD_SIZE;
      100        
      100        
62 3         16 my $piece = $board->[$y][$x];
63 3 100       12 return 0 if $piece == PIECE_EMPTY;
64 2 100       17 return 0 unless ( $piece >> PLAYER_BIT & 1 ) == $player;
65 1         5 return 1;
66             }
67              
68             # try to carry out a game move involving two points, generally from a
69             # player selecting a piece to move and a direction (via the destination
70             # point) to move in. the move may not be possible for various reasons.
71             # if possible a move may cause a bunch of changes to the board and other
72             # game state
73 13     13 1 167 method move( $srcx, $srcy, $dstx, $dsty ) {
  13         19  
  13         28  
  13         18  
  13         17  
  13         18  
  13         13  
74 13 100 100     111 return 0, "out of bounds"
      100        
      100        
75             if $srcx < 0
76             or $srcx >= BOARD_SIZE
77             or $srcy < 0
78             or $srcy >= BOARD_SIZE;
79              
80 9         20 my $piece = $board->[$srcy][$srcx];
81 9 100       27 return 0, "not a piece" if $piece == PIECE_EMPTY;
82              
83 8 100       28 return 0, "not owner" unless ( $piece >> PLAYER_BIT & 1 ) == $player;
84              
85 7         16 my ( $move_type, $stepx, $stepy ) = _move_type( $srcx, $srcy, $dstx, $dsty );
86 7 100       23 return 0, "invalid move" if $move_type == MOVE_NOPE;
87              
88             # this is probably too clever: the king by virtue of being number 3
89             # has both the square and diagonal move bits set, while rooks and
90             # bishops have only one of them set
91 6         14 my $piece_type = $piece & TYPE_MASK;
92 6 100       18 return 0, "invalid move type" unless ( $move_type & $piece_type ) > 0;
93              
94 5         15 _move_stack( $board, $move_count, $srcx, $srcy, $stepx, $stepy );
95              
96             # score points for motion in the middle
97 5         17 my $center = $board->[MIDDLE][MIDDLE];
98 5 100 100     23 if ( $center > 0 and ( $center & MOVED_MASK ) == MOVED_MASK ) {
99 2         4 $score->[ $center >> PLAYER_BIT & 1 ]++;
100             }
101             # clear any moved bits (keeps the map clean, only the middle moved
102             # bit really needs to be cleared, provided things that iterate over
103             # the $board extract the type and owner)
104 5         11 for my $row ( $board->@* ) {
105 45         58 for my $col ( $row->@* ) {
106 405         496 $col &= ~MOVED_MASK;
107             }
108             }
109              
110 5         8 $player ^= 1;
111 5 100       14 $move_count = _move_count() if $player == 0;
112              
113 5         68 return 1, "ok";
114             }
115              
116             # boards of different sizes might be supported in which case clients may
117             # need something like the following to obtain that information
118 1     1 1 7 method size() { return BOARD_SIZE }
  1         2  
  1         2  
  1         17  
119              
120             # Zobrist Hashing metadata. the caller will need to make suitable use of
121             # this, such as by indexing the [ type, owner, location ] values into an
122             # 3x2x81 array populated with 64-bit random values as generated perhaps
123             # by the ->irand64 method of the Math::Random::PCG32 module and then ^=
124 1     1 1 11 method zobrist() {
  1         4  
  1         22  
125 1         4 my @meta;
126 1         22 my $location = 0;
127 1         7 for my $row ( $board->@* ) {
128 9         13 for my $piece ( $row->@* ) {
129 81 100       136 next if $piece == 0;
130 14         39 my $type = ($piece & TYPE_MASK) - 1;
131 14         19 my $owner = $piece >> PLAYER_BIT & 1;
132 14         26 push @meta, [ $type, $owner, $location++ ];
133             }
134             }
135 1         4 return @meta;
136             }
137              
138             ########################################################################
139             #
140             # SUBROUTINES
141              
142             # this many moves happen in each turnpair
143 0     0   0 sub _move_count () { 1 + int rand(MAX_MOVES) }
  0         0  
  0         0  
144              
145             # moves stuff with less recursion than in prior versions (before 0.05)
146 10     10   10008 sub _move_stack ( $grid, $moves, $srcx, $srcy, $stepx, $stepy ) {
  10         18  
  10         16  
  10         14  
  10         11  
  10         16  
  10         12  
  10         12  
147 10         17 my $point = [];
148 10         26 $point->@[XX,YY] = ($srcx, $srcy);
149 10         18 my @stack = $point;
150 10         28 while ( $moves > 0 ) {
151 37         46 my $point = [];
152 37         71 $point->@[ XX, YY ] = ( $stack[-1][XX] + $stepx, $stack[-1][YY] + $stepy );
153             # edge: ran out of space for pushing
154             last
155 37 100 100     216 if $point->[XX] < 0
      100        
      100        
156             or $point->[XX] >= BOARD_SIZE
157             or $point->[YY] < 0
158             or $point->[YY] >= BOARD_SIZE;
159 30         55 push @stack, $point;
160             # empty cell: swap along the stack to advance the pieces
161 30 100       67 if ( $grid->[ $stack[-1][YY] ][ $stack[-1][XX] ] == PIECE_EMPTY ) {
162 27         53 for my $i ( reverse 0 .. ( $#stack - 1 ) ) {
163             # downside: this may happen more than it needs to
164 37         74 $grid->[ $stack[$i][YY] ][ $stack[$i][XX] ] |= MOVED_MASK;
165 37         47 my $j = $i + 1;
166 37         100 ( $grid->[ $stack[$i][YY] ][ $stack[$i][XX] ],
167             $grid->[ $stack[$j][YY] ][ $stack[$j][XX] ]
168             )
169             = (
170             $grid->[ $stack[$j][YY] ][ $stack[$j][XX] ],
171             $grid->[ $stack[$i][YY] ][ $stack[$i][XX] ]
172             );
173             # in theory one could collect a list of moves for use by
174             # an animation routine, or have a callback for that. but
175             # that would use more CPU and memory
176             }
177 27         43 shift @stack;
178 27         59 $moves--;
179             }
180             # non-empty cell: put it onto the stack next time around
181             }
182             }
183              
184             # given two points, what sort of movement is it? (may not be valid)
185 17     17   13196 sub _move_type ( $x1, $y1, $x2, $y2 ) {
  17         26  
  17         20  
  17         22  
  17         25  
  17         21  
186 17 100 100     56 return MOVE_NOPE, undef, undef if $x1 == $x2 and $y1 == $y2;
187              
188 15         31 my ( $dy, $plus_x ) = ( $y2 - $y1, $x2 > $x1 );
189 15 100       46 return MOVE_SQUARE, ( $plus_x ? 1 : -1 ), 0 if $dy == 0;
    100          
190              
191 11         21 my ( $dx, $plus_y ) = ( $x2 - $x1, $y2 > $y1 );
192 11 100       31 return MOVE_SQUARE, 0, ( $plus_y ? 1 : -1 ) if $dx == 0;
    100          
193              
194 9 100       58 return MOVE_DIAGONAL, ( $plus_x ? 1 : -1 ), ( $plus_y ? 1 : -1 )
    100          
    100          
195             if abs($dx) == abs($dy);
196              
197 1         6 return MOVE_NOPE, undef, undef;
198             }
199              
200             1;
201             __END__