File Coverage

blib/lib/Games/Go/Cinderblock/Rulemap.pm
Criterion Covered Total %
statement 4 6 66.6
branch n/a
condition n/a
subroutine 2 2 100.0
pod n/a
total 6 8 75.0


line stmt bran cond sub pod time code
1             package Games::Go::Cinderblock::Rulemap;
2 6     6   148592 use 5.14.0;
  6         22  
  6         280  
3 6     6   15070 use Moose;
  0            
  0            
4              
5             use Games::Go::Cinderblock::NodeSet;
6             use Games::Go::Cinderblock::Delta;
7             use Games::Go::Cinderblock::State;
8             use Games::Go::Cinderblock::Scorable;
9             use Games::Go::Cinderblock::MoveAttempt;
10             use Games::Go::Cinderblock::MoveResult;
11              
12             use Games::Go::Cinderblock::Rulemap::Rect;
13             use List::MoreUtils qw/all/;
14              
15             # This class evaluates moves and determines new board positions.
16             # This class stores no board/position data.
17             # Also, will must be used to determine visible portions of the board if there's fog of war.
18              
19             # This class is basically here to define default behavior and
20             # to provide a mechanism to override it.
21             # However this class will not handle rendering.
22              
23             # Rulemaps are not stored in the database. They are derived from
24             # entries in the Ruleset and Extra_rule tables. Or wherever.
25             # Some extra rules could be assigned using Moose's roles.
26             # Example: 'fog of war', 'atom go' each could be assigned to several variants.
27              
28             # Also: This does not involve the ko rule. That requires a database search
29             # for a duplicate position. TODO:: B::History. Do this.
30              
31             # Note: I'm treating intersections (i.e. nodes) as scalars, which different rulemap
32             # subclasses may handle as they will. Rect nodes [$row,$col].
33              
34             #To support more than 2 players or sides, each game inherently has a sort of basis
35             # such as 'ffa', 'zen', 'team', 'perverse', or perhaps more
36              
37             # Classes to extract:
38             # Games::Go::Cinderblock::State, (DONE)
39             # Games::Go::Cinderblock::MoveResult, (DONE, + MoveAttempt)
40             # Games::Go::Cinderblock::Scorable, (DONE)
41             # Games::Go::Cinderblock::Delta,
42             # Games::Go::Cinderblock::History?
43             # Games::Go::Cinderblock::TurmDeturninator?
44             # Games::Go::Cinderblock::NodeSet, (DONE)
45              
46             has topology => (
47             is => 'ro',
48             isa => 'Str',
49             default => 'plane'
50             );
51             has phase_description => (
52             is => 'ro',
53             isa => 'Str',
54             default => '0b 1w'
55             );
56             has komi => (
57             is => 'ro',
58             isa => 'Num',
59             default => '6.5'
60             );
61             has ko_rule => (
62             is => 'ro',
63             isa => 'Str',
64             default => 'situational',
65             );
66              
67             # to be extended to fog, atom, etc
68             sub FOO_apply_rule_role{
69             my ($self, $rule, $param) = @_;
70             if ($rule =~ /^heisengo/){
71             Games::Go::Cinderblock::Rulemap::Heisengo::apply ($self, $param);
72             }
73             elsif ($rule =~ /^planckgo/){
74             Games::Go::Cinderblock::Rulemap::Planckgo::apply ($self, $param);
75             }
76             else {die $rule}
77             }
78              
79              
80             #These must be implemented in a subclass
81             my $blah = 'use a topo subclass such as ::Rect instead of Games::Go::Cinderblock::Rulemap';
82             sub move_is_valid{ die $blah}
83             sub node_to_string{ die $blah}
84             sub node_from_string{ die $blah}
85             sub node_liberties{ die $blah}
86             sub set_stone_at_node{ die $blah}
87             sub stone_at_node{ die $blah}
88             sub all_nodes{ die $blah}
89             sub copy_board{ die $blah}
90             sub empty_board{ die $blah}
91              
92              
93             sub initial_state{
94             my $self = shift;
95             my $state = Games::Go::Cinderblock::State->new(
96             rulemap => $self,
97             board => $self->empty_board,
98             turn => 'b',
99             );
100             return $state;
101             }
102              
103             sub normalize_board_to_string{ # to hash for ko collisions..
104             my ($self,$board) = @_;
105             my @all_nodes = $self->all_nodes;
106             my @all_stones = map {$self->stone_at_node($board, $_) || 0} @all_nodes;
107             return join '', @all_stones;
108             }
109             # use Carp::Always;
110              
111             sub evaluate_move_attempt{
112             my ($self, $move_attempt) = @_;
113             my $basis = $move_attempt->basis_state;
114             my $board = $basis->board;
115             my $node = $move_attempt->node;
116             my $color = $move_attempt->color;
117             # my ($self, $board, $node, $color) = @_;
118             die "bad color $color" unless $color=~ /^[bw]$/;
119             die "bad node @$node" unless $self->node_is_valid($node);
120            
121             my %failure_template = (
122             rulemap => $self,
123             basis_state => $basis,
124             move_attempt => $move_attempt,
125             succeeded => 0,
126             );
127             if ($self->stone_at_node ($board, $node)){
128             return Games::Go::Cinderblock::MoveResult->new(
129             %failure_template,
130             reason => "stone exists at ". $self->node_to_string($node)
131             );
132             }
133             if ($color ne $basis->turn){
134             return Games::Go::Cinderblock::MoveResult->new(
135             %failure_template,
136             reason => "color $color (not) played during turn " .$basis->turn,
137             );
138             }
139            
140             #produce copy of board for evaluation -> add stone at $node
141             my $newboard = $self->copy_board ($board);
142             $self->set_stone_at_node ($newboard, $node, $color);
143             # $chain is a list of strongly connected stones,
144             # and $foes=enemies,$libs=liberties adjacent to $chain
145             my ($chain, $libs, $foes) = $self->get_chain($newboard, $node);
146             my $caps = $self->find_captured ($newboard, $foes);
147             if (@$libs == 0 and @$caps == 0){
148             return Games::Go::Cinderblock::MoveResult->new(
149             rulemap => $self,
150             basis_state => $basis,
151             move_attempt => $move_attempt,
152             succeeded => 0,
153             reason => "suicide",
154             );
155             }
156             for my $cap(@$caps){ # just erase captured stones
157             $self->set_stone_at_node ($newboard, $cap, 0);
158             }
159             my $other_color = (($color eq 'b') ? 'w' : 'b');
160             my $res_stt = Games::Go::Cinderblock::State->new(
161             rulemap => $self,
162             board => $newboard,
163             turn => $other_color,
164             captures => {
165             $color => $basis->captures->{$color} + @$caps,
166             $other_color => $basis->captures->{$other_color},
167             },
168             );
169             #return ($newboard, '', $caps);#no err
170             return Games::Go::Cinderblock::MoveResult->new(
171             rulemap => $self,
172             basis_state => $basis,
173             move_attempt => $move_attempt,
174             succeeded => 1,
175             caps => $caps,
176             resulting_state => $res_stt,
177             );
178             #node is returned to make this method easier to override for heisenGo
179             }
180              
181             #uses a floodfill algorithm, #TODO: absorb. generic.
182             #returns (string, liberties, adjacent_foes)
183             sub get_chain { #for all board types
184             my ($self, $board, $node1) = @_; #start row/column
185            
186             my %seen; #indexed by stringified nodes
187             my @found;
188             my @libs; #liberties
189             my @foes; #enemy stones adjacent to string
190             my $string_side = $self->stone_at_node($board, $node1);
191             return unless defined $string_side; #empty
192             #0 has to mean empty, (b black, w white, ...)
193             my @nodes = ($node1); #array of adjacent intersections to consider
194            
195             while (@nodes) {
196             my $node = pop @nodes;
197             next if $seen {$self->node_to_string ($node)};
198             $seen {$self->node_to_string ($node)} = 1;
199            
200             my $here_side = $self->stone_at_node ($board, $node);
201            
202             unless ($here_side){ #empty
203             push @libs, $node;
204             next
205             }
206             if ($here_side eq $string_side){
207             push @found, $node;
208             push @nodes, $self->node_liberties ($node);
209             next
210             }
211             # else enemy
212             push @foes, $node;
213             }
214             return (\@found, \@libs, \@foes);
215             }
216              
217              
218             #chains are represented by a single 'delegate' node to identify chain
219             #returns chains, keyed by their delegates. a chain is a list of nodestrings
220             #also returns hash of {nodestring=>delegate}
221             #also returns hash of {delegate=>side}
222             sub FOO_all_chains{
223             my ($self, $board) = @_;
224             my %delegates;
225             my %delegate_of_stone;
226             my %delegate_side;
227             for my $n ($self->all_stones($board)){
228             my $s = $self->node_to_string($n);
229             next if $delegate_of_stone{$s};
230            
231             $delegate_side{$s} = $self->stone_at_node($board, $n);
232             my ($chain,$l,$f) = $self->get_chain($board, $n);
233             #push @chains, $chain;
234             #only deal with nodestrings here;
235             $delegates{$s} = [map {$self->node_to_string($_)} @$chain];
236             my @nodestrings;
237             #examine & to_string each node
238             for (@$chain){
239             my $nodestring =$self->node_to_string($_);
240             push @nodestrings, $nodestring;
241             $delegate_of_stone{$nodestring} = $s;
242             }
243             }
244             return (\%delegates, \%delegate_of_stone, \%delegate_side)
245             }
246              
247             sub nodeset{ # $rm->nodeset(@nodes)
248             my $self = shift;
249             my $ns = Games::Go::Cinderblock::NodeSet->new(rulemap => $self);
250             $ns->add($_) for @_;
251             return $ns;
252             }
253             # sub all_nodes, sub no_nodes.. TODO
254             sub all_nodes_nodeset{
255             my $self = shift;
256             return $self->nodeset($self->all_nodes);
257             }
258             sub FOO_floodfill{ #in state now..
259             my ($self, $cond, $progenitor) = @_;
260             my $set = $self->nodeset($progenitor);
261             my $seen = $self->nodeset($progenitor);
262             my @q = $self->adjacent_nodes($progenitor);
263             while(@q){
264             my $node = shift @q;
265             next if $seen->has($node);
266             $seen->add($node);
267             next unless $cond->($node);
268             $set->add($node);
269             push @q, $self->adjacent_nodes($node);
270             }
271             }
272              
273              
274             sub all_stones {
275             my ($self, $board) = @_;
276             return grep {$self->stone_at_node($board, $_)} ($self->all_nodes);
277             }
278              
279             #opposite of get_chain
280             sub get_empty_space{
281             my ($self, $board, $node1, $ignore_stones) = @_; #start row/column
282             return ([],[]) if $self->stone_at_node ($board, $node1);
283             $ignore_stones = {} unless $ignore_stones; #dead stones tend to be ignored when calculating territory
284            
285             my %seen; #indexed by stringified nodes
286             my @found;
287             my @adjacent_stones;
288             my @nodes = ($node1); #array of adjacent intersections to consider
289             while (@nodes) {
290             my $node = pop @nodes;
291             my $nodestring = $self->node_to_string ($node);
292             next if $seen {$nodestring};
293             $seen {$nodestring} = 1;
294            
295             my $here_color = $self->stone_at_node ($board, $node);
296             if (!$here_color or $ignore_stones->{$nodestring}){ #empty
297             push @found, $node;
298             push @nodes, $self->node_liberties ($node)
299             }
300             else{ #stone
301             push @adjacent_stones, $node;
302             }
303             }
304             return (\@found, \@adjacent_stones);
305             }
306              
307             # TODO: absorb into generic flood fill
308             #take a list of stones, returns those which have no libs, as chains
309             sub find_captured{
310             my ($self, $board, $nodes) = @_;
311             my @nodes = @$nodes; #list
312             my %seen; #indexed by stringified node
313             my @caps; #list
314             while (@nodes){
315             my $node = pop @nodes;
316             next if $seen {$self->node_to_string($node)};
317             my ($chain, $libs, $foes) = $self->get_chain ($board, $node);
318             my $capture_these = scalar @$libs ? '0' : '1';
319             for my $n (@$chain){
320             $seen {$self->node_to_string($n)} = 1;
321             push @caps, $n if $capture_these;
322             }
323             }
324             return \@caps
325             }
326              
327              
328              
329             sub side_of_entity{
330             my ($self, $entity) = @_;
331             die 'wrong score mode' unless $self->detect_basis eq 'ffa';
332             for my $phase (split ' ', $self->phase_description) {
333             if ($phase =~ m/$entity([wbr])/){
334             return $1;
335             }
336             }
337             }
338             sub all_entities{
339             my $self = shift;
340             my $pd = $self->phase_description;
341             my %e;
342             while($pd=~/(\d)/g){
343             $e{$1}=1
344             }
345             return keys %e;
346             }
347             sub all_sides{
348             my $self = shift;
349             my $pd = $self->phase_description;
350             my %s;
351             while($pd=~/([bw])/g){
352             $s{$1}=1
353             }
354             return keys %s;
355             }
356              
357             sub default_captures {#for before move 1
358             my $self = shift;
359             my @phases = split ' ', $self->phase_description;
360             return join ' ', map {0} (1..@phases) #'0 0'
361             }
362              
363              
364             #Necessary to decide how to describe game in /game.
365             #Score & game objectives depend.
366             #reads the phase description and
367             # returns 'ffa', 'team', 'zen', or 'perverse'? or 'other'?
368             sub detect_basis{
369             my $self = shift; #is it a pd or a rulemap?
370             my $pd = ref $self ? $self->phase_description : $self;
371            
372             #assume that this is well-formed
373             #and no entity numbers are skipped
374             my @phases = map {[split'',$_]} split ' ', $pd;
375             my %ents;
376             my %sides;
377             for (@phases){
378             $ents{$_->[0]}{$_->[1]} = 1;
379             $sides{$_->[1]}{$_->[0]} = 1;
380             }
381             return 'ffa' if @phases == keys %ents
382             and @phases == keys %sides;
383             return 'zen' if all {keys %{$ents{$_}} == keys%sides} (keys%ents);
384            
385             return 'other';
386             }
387              
388             sub compute_score{
389             my ($self, $board, $caps, $death_mask) = @_;
390             my ($terr_mask, $terr_points) = $self->find_territory_mask($board, $death_mask);
391            
392             my $type = $self->detect_basis;
393             my $pd = $self->phase_description;
394             my @phases = split ' ', $pd;
395             @phases = map {[split '', $_]} @phases;
396            
397             my @sides = $self->all_sides;
398             my %side_score = map {$_=>0} @sides;
399            
400             { #add up captures of each team.
401             my @caps = split ' ', $caps; # from latest move
402             for my $phase (@phases){
403             my $phase_caps = shift @caps;
404             $side_score{$phase->[1]} += $phase_caps;
405             }
406             #add up territory of each team.
407             for my $side (@sides){
408             $side_score{$side} += $terr_points->{$side};
409             }
410             #and count dead things in death_mask
411             #points in death_mask go to territory owner in terr_mask
412             for my $d (keys %$death_mask){
413             my $capturer = $terr_mask->{$d};
414             if ($capturer){
415             $side_score{$capturer}++;
416             }
417             }
418             }
419            
420             if ($self->phase_description eq '0b 1w'){
421             $side_score{w} += $self->komi;
422             }
423            
424             if ($type eq 'ffa' or $type eq 'zen' or $type eq 'team'){
425             return \%side_score
426             }
427             return 'perverse or other modes not scoring...'
428             }
429              
430             sub num_phases{
431             my ($self) = @_;
432             my @phases = split ' ', $self->phase_description;
433             return scalar @phases;
434             }
435              
436             sub determine_next_phase{
437             my ($self, $phase, $choice_phases) = @_;
438             my $np = $self->num_phases;
439             my $next = $phase;
440             for (1..$np){
441             $next = ($next + 1) % $np;
442             return $next if grep {$next==$_} @$choice_phases;
443             }
444             die "I was given a bad list of choice phases: " . join',',@$choice_phases;
445             }
446              
447              
448             #compare earlier state to later state.
449             # package the things which actually change.
450             # among board, turn, & captures.
451              
452             sub delta{
453             my ($self,$state1,$state2) = @_;
454             my %deltargs;
455              
456             my $board_changeset = $self->_compare_boards( $state1->board, $state2->board );
457             $deltargs{board} = $board_changeset if %$board_changeset;
458             if($state1->turn ne $state2->turn){
459             $deltargs{turn} = {before=>$state1->turn,after=>$state2->turn};
460             }
461             if($state1->captures('w') != $state2->captures('w')){
462             $deltargs{captures}{w} = {before=>$state1->captures('w'),after=>$state2->captures('w')};
463             }
464             if($state1->captures('b') != $state2->captures('b')){
465             $deltargs{captures}{b} = {before=>$state1->captures('b'),after=>$state2->captures('b')};
466             }
467              
468             my $delta = Games::Go::Cinderblock::Delta->new(
469             rulemap => $self,
470             %deltargs
471             );
472             return $delta;
473             }
474              
475              
476             #compare earlier board to later board.
477             sub _compare_boards{
478             my ($self, $board1, $board2) = @_;
479            
480             my %changeset;
481             for my $node ($self->all_nodes){
482             my $fore = $self->stone_at_node($board1, $node); #0,w,b,etc
483             my $afte = $self->stone_at_node($board2, $node);
484             next if ($fore eq $afte);
485             if($fore){
486             # autovivify.
487             push @{$changeset{remove}{$fore}}, $node;
488             }
489             if($afte){
490             push @{$changeset{add}{$afte}}, $node;
491             }
492             }
493             return \%changeset;
494             }
495              
496             1;
497              
498             __END__
499              
500             =head1 NAME
501              
502             Games::Go::Cinderblock::Rulemap - The beating heart of cinderblock.
503              
504             =head1 SYNOPSIS
505              
506             my $rulemap = Games::Go::Cinderblock::Rulemap::Rect->new(
507             w => 11,
508             h => 8,
509             wrap_h => 1,
510             wrap_v => 1,
511             );
512             my $state = $rulemap->initial_state;
513             my $move_result = $state->attempt_move(
514             color => 'b',
515             node => [3,3],
516             );
517             say $move_result->succeeded ? 'success!' : ('failed? ' . $move_result->reason);
518             $state = $move_result->resulting_state;
519             # do something with $move_result->delta.
520              
521             =head1 DESCRIPTION
522              
523             This module is basically basilisk::Rulemap, now mostly split
524             into a bunch of helper modules. The intention is still to use
525             Moose's metaclass capabilities & method modifiers to override
526             aspects of the default behavior.
527              
528             This class still uses subclasses to define topology, and still
529             only one topology is in a usable state:
530             L<Games::Go::Cinderblock::Rulemap::Rect>.
531              
532             =head1 METHODS
533              
534             =head2 initial_state
535              
536             =head2 nodeset
537              
538             =head2 empty_board
539              
540             =head2 all_nodes
541              
542             =cut