ViewVC Help
View File | Revision Log | Show Annotations | Download File
/cvs/Games-Go-SimpleBoard/SimpleBoard.pm
Revision: 1.16
Committed: Tue Jul 29 10:03:52 2008 UTC (15 years, 9 months ago) by elmex
Branch: MAIN
Changes since 1.15: +39 -37 lines
Log Message:
added MARK_GRAYED

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