ViewVC Help
View File | Revision Log | Show Annotations | Download File
/cvs/Games-Go-SimpleBoard/SimpleBoard.pm
Revision: 1.18
Committed: Tue Jul 29 10:09:53 2008 UTC (15 years, 9 months ago) by root
Branch: MAIN
CVS Tags: rel-1_01, HEAD
Changes since 1.17: +1 -1 lines
Log Message:
1.01

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