ViewVC Help
View File | Revision Log | Show Annotations | Download File
/cvs/Games-Go-SimpleBoard/SimpleBoard.pm
Revision: 1.3
Committed: Mon Jun 23 20:41:16 2008 UTC (15 years, 10 months ago) by root
Branch: MAIN
Changes since 1.2: +63 -8 lines
Log Message:
*** empty log message ***

File Contents

# Content
1 package Games::Go::SimpleBoard;
2
3 =head1 NAME
4
5 Games::Go::SimpleBoard - represent a simple go board
6
7 =head1 SYNOPSIS
8
9 use Games::Go::SimpleBoard;
10
11 =head1 DESCRIPTION
12
13 Please supply a description )
14
15 =head2 EXPORTED CONSTANTS
16
17 Marker types for each board position (ORed together):
18
19 MARK_TRIANGLE # triangle mark
20 MARK_SQUARE # square mark
21 MARK_CIRCLE # circle mark
22 MARK_SMALL_B # small stone, used for scoring or marking
23 MARK_SMALL_W # small stone, used for scoring or marking
24 MARK_B # normal black stone
25 MARK_W # normal whit stone
26 MARK_GRAYED # in conjunction with MARK_[BW], grays the stone
27 MARK_LABEL # a text label
28 MARK_HOSHI # this is a hoshi point (not used much)
29 MARK_MOVE # this is a regular move
30 MARK_KO # this is a ko position
31 MARK_REDRAW # ignored, can be used for your own purposes
32
33 COLOUR_BLACK # used for $board->{last}
34 COLOUR_WHITE # to mark the colour of the last move
35
36 MOVE_HANDICAP # used as "x-coordinate" for handicap moves
37 MOVE_PASS # can be used as "x-coordinate" for handicap moves
38
39 =head2 METHODS
40
41 =over 4
42
43 =cut
44
45 no warnings;
46 use strict;
47
48 use Carp ();
49
50 use base Exporter::;
51
52 our $VERSION = '1.0';
53
54 our @EXPORT = qw(
55 MARK_TRIANGLE MARK_SQUARE MARK_CIRCLE MARK_SMALL_B MARK_SMALL_W MARK_B
56 MARK_W MARK_GRAYED MARK_MOVE MARK_LABEL MARK_HOSHI MARK_KO
57 MARK_REDRAW
58 COLOUR_BLACK COLOUR_WHITE
59 MOVE_HANDICAP MOVE_PASS
60 );
61
62 # marker types for each board position (ORed together)
63
64 sub MARK_TRIANGLE (){ 0x0001 }
65 sub MARK_SQUARE (){ 0x0002 }
66 sub MARK_CIRCLE (){ 0x0004 }
67 sub MARK_SMALL_B (){ 0x0008 } # small stone, used for scoring or marking
68 sub MARK_SMALL_W (){ 0x0010 } # small stone, used for scoring or marking
69 sub MARK_B (){ 0x0020 } # normal black stone
70 sub MARK_W (){ 0x0040 } # normal whit stone
71 sub MARK_GRAYED (){ 0x0080 } # in conjunction with MARK_[BW], grays the stone
72 sub MARK_LABEL (){ 0x0100 }
73 sub MARK_HOSHI (){ 0x0200 } # this is a hoshi point (not used much)
74 sub MARK_MOVE (){ 0x0400 } # this is a regular move
75 sub MARK_KO (){ 0x0800 } # this is a ko position
76 sub MARK_REDRAW (){ 0x8000 }
77
78 sub COLOUR_BLACK (){ 0 }
79 sub COLOUR_WHITE (){ 1 }
80
81 sub MOVE_PASS (){ undef }
82 sub MOVE_HANDICAP (){ -2 }
83
84 =item my $board = new $size
85
86 Creates a new empty board of the given size.
87
88 C<< $board->{size} >> stores the board size.
89
90 C<< $board->{max} >> stores the maximum board coordinate (size-1).
91
92 C<< $board->{captures}[COLOUR] >> stores the number of captured stones for
93 the given colour.
94
95 C<< $board->{last} >> stores the colour of the last move that was played.
96
97 C<< $board->{board} >> stores a two-dimensional array with board contents.
98
99 =cut
100
101 sub new {
102 my $class = shift;
103 my $size = shift;
104 bless {
105 max => $size - 1,
106 size => $size,
107 board => [map [(0) x $size], 1 .. $size],
108 captures => [0, 0], # captures
109 #timer => [],
110 #score => [],
111 #last => COLOUR_...,
112 @_
113 },
114 $class;
115 }
116
117 # inefficient and primitive, I hear you say?
118 # well... you are right :)
119 # use an extremely dumb floodfill algorithm to get rid of captured stones
120 sub capture {
121 my ($self, $mark, $x, $y) = @_;
122
123 my %seen;
124 my @found;
125 my @nodes = ([$x,$y]);
126 my $board = $self->{board};
127
128 my $max = $self->{max};
129
130 while (@nodes) {
131 my ($x, $y) = @{pop @nodes};
132 unless ($seen{$x,$y}++) {
133 if ($board->[$x][$y] & $mark) {
134 push @found, [$x, $y];
135
136 push @nodes, [$x-1, $y] unless $seen{$x-1, $y} || $x <= 0;
137 push @nodes, [$x+1, $y] unless $seen{$x+1, $y} || $x >= $max;
138 push @nodes, [$x, $y-1] unless $seen{$x, $y-1} || $y <= 0;
139 push @nodes, [$x, $y+1] unless $seen{$x, $y+1} || $y >= $max;
140 } elsif (!($board->[$x][$y] & (MARK_B | MARK_W))) {
141 return;
142 }
143 }
144 }
145
146 @found
147 }
148
149 =item $hint = $board->update ([update-structures...])
150
151 Each update-structure itself is also an array-ref:
152
153 [$x, $y, $clr, $set, $label, $hint] # update or move
154 [MOVE_HANDICAP, $handicap] # black move, set handicap
155 [MOVE_PASS] # pass
156 [] # also pass
157
158 It changes the board or executes a move, by first clearing the bits
159 specified in C<$clr>, then setting bits specified in C<$set>.
160
161 If C<$set> includes C<MARK_LABEL>, the label text must be given in
162 C<$label>.
163
164 If C<$set> contains C<MARK_MOVE>, then a circle symbol will be placed on
165 this coordinate only if this is the last move done (which is useful for a
166 move marker).
167
168 For handicap "moves", currently only board sizes 9, 13 and 19 are
169 supported and only handicap values from 2 to 9. The placement follows the
170 IGS rules, if you want other placements, you have to set it up yourself.
171
172 This function modifies the hint member of the specified path to speed up
173 repeated board generation and updates with the same update structures.
174
175 If the hint member is a reference the scalar pointed to by the reference
176 is updated instead.
177
178 If all this hint member thing is unclear, just ignore it and specify it
179 as C<undef> or leave it out of the array entirely. Do make sure that you
180 keep your update structures around, however, as regenerating a full board
181 position from hinted update structures is I<much> faster then recreating
182 it from fresh update structures.
183
184 Example, make two silly moves:
185
186 $board->update ([[0, 18, -1, MARK_B|MARK_MOVE],
187 [0, 17, -1, MARK_W|MARK_MOVE]]);
188
189 =cut
190
191 our %HANDICAP_COORD = (
192 9 => [2, 4, 6],
193 13 => [3, 6, 9],
194 19 => [3, 9, 15],
195 );
196 our %HANDICAP_XY = (
197 2 => [qw(0,2 2,0 )],
198 3 => [qw(0,2 2,0 2,2 )],
199 4 => [qw(0,2 2,0 2,2 0,0 )],
200 5 => [qw(0,2 2,0 2,2 0,0 1,1)],
201 6 => [qw(0,2 2,0 2,2 0,0 0,1 2,1 )],
202 7 => [qw(0,2 2,0 2,2 0,0 0,1 2,1 1,1)],
203 8 => [qw(0,2 2,0 2,2 0,0 0,1 2,1 1,0 1,2 )],
204 9 => [qw(0,2 2,0 2,2 0,0 0,1 2,1 1,0 1,2 1,1)],
205 );
206
207 sub update {
208 my ($self, $path) = @_;
209
210 my $board = $self->{board};
211
212 for (@$path) {
213 my ($x, $y, $clr, $set, $label) = @$_;
214
215 my $nodemask =
216 $_ == $path->[-1]
217 ? ~0
218 : ~(MARK_SQUARE | MARK_TRIANGLE | MARK_CIRCLE | MARK_LABEL | MARK_KO);
219
220 if (!defined $x) {
221 # pass
222 $self->{last} = $set & MARK_B ? COLOUR_BLACK : COLOUR_WHITE;
223
224 } elsif ($x == MOVE_HANDICAP) {
225 # $y = #handicap stones
226 my $c = $HANDICAP_COORD{$self->{size}}
227 or Carp::croak "$self->{size}: illegal board size for handicap";
228 my $h = $HANDICAP_XY{$y}
229 or Carp::croak "$y: illegal number of handicap stones";
230
231 for (@$h) {
232 my ($x, $y) = map $c->[$_], split /,/;
233 $board->[$x][$y] = MARK_B | MARK_MOVE;
234 }
235
236 } else {
237 $board->[$x][$y] =
238 $board->[$x][$y]
239 & ~$clr
240 | $set
241 & $nodemask;
242
243 $self->{label}[$x][$y] = $label if $set & MARK_LABEL;
244
245 if ($set & MARK_MOVE) {
246 $self->{last} = $set & MARK_B ? COLOUR_BLACK : COLOUR_WHITE;
247
248 unless (${ $_->[5] ||= \my $hint }) {
249 my ($own, $opp) =
250 $set & MARK_B
251 ? (MARK_B, MARK_W)
252 : (MARK_W, MARK_B);
253
254 my (@capture, $suicide);
255
256 push @capture, $self->capture ($opp, $x-1, $y) if $x > 0 && $board->[$x-1][$y] & $opp;
257 push @capture, $self->capture ($opp, $x+1, $y) if $x < $self->{max} && $board->[$x+1][$y] & $opp;
258 push @capture, $self->capture ($opp, $x, $y-1) if $y > 0 && $board->[$x][$y-1] & $opp;
259 push @capture, $self->capture ($opp, $x, $y+1) if $y < $self->{max} && $board->[$x][$y+1] & $opp;
260
261 # keep only unique coordinates
262 @capture = do { my %seen; grep !$seen{"$_->[0],$_->[1]"}++, @capture };
263
264 # remove captured stones
265 $self->{captures}[$self->{last}] += @capture;
266 $self->{board}[$_->[0]][$_->[1]] &= ~(MARK_B | MARK_W | MARK_MOVE)
267 for @capture;
268
269 $suicide += $self->capture ($own, $x, $y);
270
271 ${ $_->[5] } ||= !(@capture || $suicide);
272
273 if (!$suicide && @capture == 1) {
274 # possible ko. now check liberties on placed stone
275
276 my $libs;
277
278 $libs++ if $x > 0 && !($board->[$x-1][$y] & $opp);
279 $libs++ if $x < $self->{max} && !($board->[$x+1][$y] & $opp);
280 $libs++ if $y > 0 && !($board->[$x][$y-1] & $opp);
281 $libs++ if $y < $self->{max} && !($board->[$x][$y+1] & $opp);
282
283 if ($libs == 1) {
284 $board->[$x][$y] = $board->[$x][$y] & ~MARK_CIRCLE | (MARK_KO & $nodemask);
285 ($x, $y) = @{$capture[0]};
286 $board->[$x][$y] |= MARK_KO & $nodemask;
287 }
288 }
289 }
290 }
291 }
292 }
293 }
294
295 =item $board->is_valid_move ($colour, $x, $y[, $may_suicide])
296
297 Returns true if the move of the given colour on the given coordinates is
298 valid or not.
299
300 =cut
301
302 sub is_valid_move {
303 my ($self, $colour, $x, $y, $may_suicide) = @_;
304
305 my $board = $self->{board};
306
307 return if $board->[$x][$y] & (MARK_B | MARK_W | MARK_KO);
308
309 if ($may_suicide) {
310 return 1;
311 } else {
312 my ($own, $opp) = $colour == COLOUR_BLACK
313 ? (MARK_B, MARK_W)
314 : (MARK_W, MARK_B);
315
316 # try the move
317 local $board->[$x][$y] = $board->[$x][$y] | $own;
318
319 return 1 if $x > 0 && $board->[$x-1][$y] & $opp && $self->capture ($opp, $x-1, $y, 1);
320 return 1 if $x < $self->{max} && $board->[$x+1][$y] & $opp && $self->capture ($opp, $x+1, $y, 1);
321 return 1 if $y > 0 && $board->[$x][$y-1] & $opp && $self->capture ($opp, $x, $y-1, 1);
322 return 1 if $y < $self->{max} && $board->[$x][$y+1] & $opp && $self->capture ($opp, $x, $y+1, 1);
323
324 return !$self->capture ($own, $x, $y, 1);
325 }
326 }
327
328 1;
329
330 =back
331
332 =head2 AUTHOR
333
334 Marc Lehmann <schmorp@schmorp.de>
335
336 =head2 SEE ALSO
337
338 L<Gtk2::GoBoard>.
339
340 =cut
341