File Coverage

blib/lib/Games/Cards/Undo.pm
Criterion Covered Total %
statement 5 73 6.8
branch 1 32 3.1
condition 1 13 7.6
subroutine 3 11 27.2
pod 4 5 80.0
total 14 134 10.4


line stmt bran cond sub pod time code
1             package Games::Cards::Undo;
2              
3             # part of Games::Cards by Amir Karger (See Cards.pm for details)
4              
5             =pod
6              
7             =head1 NAME
8              
9             Games::Cards::Undo -- undoing/redoing moves in Games::Cards games
10              
11             =head1 SYNOPSIS
12              
13             use Games::Cards::Undo;
14             $Undo = new Games::Cards::Undo(100); # Make undo engine to save 100 moves
15             $Undo->undo; # undo last move
16             $Undo->redo; # redo last undone move
17             $Undo->end_move; # tell undo engine we're done with a move
18              
19             =head1 DESCRIPTION
20              
21             This is the package for methods to undo & redo moves. The GC::Undo object has
22             no publicly accessible fields. But it stores an array of the
23             preceding moves. Note that a "move" is made up of several "atoms" (objects of
24             the private class GC::Undo::Atom and its subclassess). For example, moving a
25             card from one column to another in solitaire involves one or more Splice atoms
26             (removing or adding card(s) to a CardSet) and possibly a Face atom (turning a
27             card over).
28              
29             Many of the GC::Undo methods (and all of the GC::Undo::Atom methods) will be
30             called by other Games::Cards methods, but not by the actual games. Here are
31             the publicly accesssible methods:
32              
33             =over 4
34              
35             =cut
36              
37             # TODO write Undo::Sort?
38              
39              
40             # sub-packages
41             {
42             package Games::Cards::Undo;
43             package Games::Cards::Undo::Atom;
44             package Games::Cards::Undo::Splice;
45             package Games::Cards::Undo::Face;
46             package Games::Cards::Undo::End_Move;
47             }
48              
49             # How does Games::Cards handle undo?
50             #
51             # Undo_List is just an array of (objects from derived classes of) Undo::Atoms.
52             # E.g. in solitaire one "move" might include moving cards from one column to
53             # another (two Undo::Splice objects) and turning a card over (a Undo::Face
54             # object) The undo list will store those Atoms as well as an End_Move object,
55             # which is just a placeholder saying that move is over.
56              
57             # Global private variables
58             # Can't keep this info in an object, because private GC subroutines
59             # (like CardSet::splice) need access to the Undo list, and I shouldn't have
60             # to pass the undo object around to every sub.
61             # GC::Undo::Undo_List holds all previous moves in GC::Undo::Atom objects
62             # GC::Undo::Current_Atom is the index of the current Atom in @Undo_List
63             # GC::Undo::Max_Size is the maximum size (moves, not Atoms!) of the undo list
64             # GC::Undo::In_Undo says that we're currently doing (or undoing) an Undo, so we
65             # shouldn't store undo information when we move cards around
66             my (@Undo_List, $Current_Atom, $Max_Size, $In_Undo);
67              
68             =item new(MOVES)
69              
70             Initialize the Undo engine. MOVES is the number of atoms to save.
71             0 (or no argument) allows infinite undo.
72              
73             This method must be called before any undo-able moves are made (i.e., it can be
74             called after the hands are dealt). This method will also re-initialize the
75             engine for a new game.
76              
77             =cut
78              
79             sub new {
80 0     0 1 0 my $class = shift;
81             # (re)set global private variables
82 0   0     0 $Max_Size = shift || 0;
83 0         0 $Current_Atom = -1;
84 0         0 @Undo_List = ();
85 0         0 $In_Undo = 0;
86              
87             # Make the (dummy) object to give a "handle" for methods
88 0         0 my $thing = {};
89 0         0 bless $thing, $class;
90 0         0 return $thing;
91             }
92              
93             =item end_move
94              
95             End the current move. Everything between the last call to end_move and now
96             is considered one move. This tells undo how much to undo.
97              
98             =cut
99              
100             sub end_move {
101             # Don't store anything if no atoms have been stored since the
102             # last End_Move atom. This could happen e.g. if someone does
103             # an illegal move & then wants to undo it.
104 0 0 0 0 1 0 if (! defined $Current_Atom ||
      0        
105             $Current_Atom == -1 ||
106             ref($Undo_List[$Current_Atom]) eq "Games::Cards::Undo::End_Move") {
107 0         0 return;
108             }
109              
110             # calling with just "store(foo)" there aren't enough args!
111 0         0 my $atom = new Games::Cards::Undo::End_Move;
112 0         0 $atom->store;
113             } # end sub Games::Cards::Undo::end_move
114              
115             sub store {
116             # Stores a move in the undo list, which can later be undone or redone. The
117             # first argument is the type of move to store, other args give details about
118             # the move depending on the move type.
119             #
120             # arg1 is a subclass of Undo::Atom
121             # Don't store moves if the undo engine hasn't been initialized
122 1288 50   1288 0 9441 return unless defined $Current_Atom;
123              
124             # don't store undo moves when we're currently implementing an undo/redo
125 0 0       0 return if $In_Undo;
126              
127 0         0 shift; # ignore class
128 0         0 my $atom = shift; # the Undo::Atom to store
129              
130             # If we undid some moves & then do a new move instead of redoing,
131             # then erase the moves we undid
132 0         0 $#Undo_List = $Current_Atom;
133              
134             # Now add the move to the undo list
135 0         0 push @Undo_List, $atom;
136              
137             # If the list is too big, remove a whole move (not just an Atom)
138             # from the beginning of the list (oldest undos)
139 0         0 my $end_class = "Games::Cards::Undo::End_Move";
140 0 0 0     0 if ($Max_Size && grep {ref eq $end_class} @Undo_List > $Max_Size) {
  0         0  
141 0         0 $atom = shift @Undo_List until ref($atom) eq $end_class;
142             }
143              
144 0         0 $Current_Atom = $#Undo_List;
145              
146 0         0 return 1;
147             } # end sub Games::Cards::Undo::store
148              
149             =item undo
150              
151             Undo a move.
152              
153             =cut
154              
155             sub undo {
156             # undoing a move means undoing all the Atoms since the last
157             # End_Move Atom
158             # Note that this sub can (?) also undo from the middle of a move
159             # If called w/ class instead of object, and we never called new(),
160             # then return. This shouldn't happen.
161 0 0   0 1 0 return unless defined $Current_Atom; # never called new
162 0 0       0 return if $Current_Atom == -1;
163 0         0 $In_Undo = 1; # Don't store info when moving cards around
164              
165             # Loop until the next End_Move Atom or until we exhaust the undo list
166 0         0 my $end_class= "Games::Cards::Undo::End_Move";
167 0 0       0 $Current_Atom-- if ref($Undo_List[$Current_Atom]) eq $end_class;
168 0         0 for (;$Current_Atom > -1; $Current_Atom--) {
169 0         0 my $atom = $Undo_List[$Current_Atom];
170 0 0       0 last if ref($atom) eq $end_class;
171 0         0 $atom->undo;
172             }
173             # now $Current_Atom is on the End_Move at the end of the last move
174              
175 0         0 $In_Undo = 0; # done undoing. Allowed to store again.
176 0         0 return 1;
177             } # end sub Games::Cards::Undo::undo
178              
179              
180             =item redo
181              
182             Redo a move that had been undone with undo.
183              
184             =cut
185              
186             sub redo {
187             # redoing a move means redoing every Atom from the current atom
188             # (which should be an End_Move) until the next End_Move atom
189             # If called w/ class instead of object, and we never called new(),
190             # then return. This shouldn't happen.
191 0 0   0 1 0 return unless defined $Current_Atom;
192 0 0       0 return if $Current_Atom == $#Undo_List;
193 0         0 $In_Undo = 1; # Don't store info when moving cards around
194              
195             # Loop until the next End_Move Atom or until we exhaust the undo list
196 0         0 my $atom;
197 0         0 my $end_class = "Games::Cards::Undo::End_Move";
198 0 0       0 $Current_Atom++ if ref($Undo_List[$Current_Atom]) eq $end_class;
199 0         0 for (;$Current_Atom <= $#Undo_List; $Current_Atom++) {
200 0         0 my $atom = $Undo_List[$Current_Atom];
201 0 0       0 last if ref($atom) eq $end_class;
202 0         0 $atom->redo;
203             }
204             # now $Current_Atom is on the End_Move at the end of this move
205              
206 0         0 $In_Undo = 0; # done redoing. Allowed to store again.
207 0         0 return 1;
208             } # end sub Games::Cards::Undo::redo
209              
210             =back
211              
212             =cut
213              
214             {
215             package Games::Cards::Undo::Atom;
216             # A CG::Undo::Atom object stores the smallest indivisible amount of undo
217             # information. The subclasses of this class implement different kinds of atoms,
218             # as well as the way to undo and redo them.
219              
220             sub new {
221             # This new will be used by subclasses
222             # arg0 is the class. arg1 is a hashref containing various fields. Just
223             # store 'em.
224 1288     1288   1696 my $class = shift;
225 1288   50     2590 my $atom = shift || {};
226              
227             # turn it into an undo move
228 1288         4440 bless $atom, $class;
229             } # end sub Games::Cards::Undo::Atom::new
230              
231             sub store {
232             # Store this Atom in the Undo List
233 1288     1288   2649 Games::Cards::Undo->store(shift);
234             } # end sub Games::Cards::Undo::Atom::store
235              
236             } # end package Games::Cards::Undo::Atom
237              
238             {
239             package Games::Cards::Undo::End_Move;
240             # An Undo::End_Move is just a marker. Everything in the Undo_List from just
241             # after the last End_Move until this one is one "move".
242              
243             @Games::Cards::Undo::End_Move::ISA = qw(Games::Cards::Undo::Atom);
244              
245             # inherit SUPER::new
246             # No other methods necessary!
247              
248             } # end package Games::Cards::Undo::End_Move
249              
250             {
251             package Games::Cards::Undo::Face;
252             # This object stores the act of turning a card over
253              
254             @Games::Cards::Undo::Face::ISA = qw(Games::Cards::Undo::Atom);
255              
256             # inherit SUPER::new
257              
258             sub undo {
259 0     0     my $face = shift;
260 0           my ($card, $direction) = ($face->{"card"}, $face->{"direction"});
261 0 0         if ($direction eq "up") {
    0          
262 0           $card->face_down;
263             } elsif ($direction eq "down") {
264 0           $card->face_up;
265             } else {
266 0           my $func = (caller(0))[3];
267 0           die ("$func called with unknown direction $direction\n");
268             }
269             } # end sub Games::Cards::Undo::Face::undo
270              
271             sub redo {
272 0     0     my $face = shift;
273 0           my ($card, $direction) = ($face->{"card"}, $face->{"direction"});
274 0 0         if ($direction eq "up") {
    0          
275 0           $card->face_up;
276             } elsif ($direction eq "down") {
277 0           $card->face_down;
278             } else {
279 0           my $func = (caller(0))[3];
280 0           die ("$func called with unknown direction $direction\n");
281             }
282             } # end sub Games::Cards::Undo::Face::redo
283              
284             } # end package Games::Cards::Undo::Face
285              
286             {
287             package Games::Cards::Undo::Splice;
288             # This object stores the act of adding or removing cards from a CardSet, i.e.
289             # one of these objects gets created each time GC::CardSet::splice is called.
290             # This stores most of the actions in a card game.
291              
292             @Games::Cards::Undo::Splice::ISA = qw(Games::Cards::Undo::Atom);
293              
294             # inherit SUPER::new
295              
296             sub undo {
297             # If we changed ARRAY by doing:
298             # RESULT = splice(ARRAY, OFFSET, LENGTH, LIST);
299             # then we can return ARRAY to its original form by
300             # splice(ARRAY, OFFSET, scalar(LIST), RESULT);
301             #
302             # (sub splice also made sure that for calls to splice without
303             # all the arguments, the missing arguments were added, and that OFFSET
304             # would be >= 0)
305              
306 0     0     my $splice = shift;
307             # Could do this quicket with no strict refs :)
308 0           my ($set, $offset, $in_cards, $out_cards) =
309 0           map {$splice->{$_}} qw(set offset in_cards out_cards);
310              
311             # Do the anti-splice and return its return value
312             # (Return will actually be in_cards!)
313 0           $set->splice ($offset, scalar(@$in_cards), $out_cards);
314             } # end sub Cards::Games::Undo::Splice::undo
315              
316             sub redo {
317             # we changed ARRAY by doing:
318             # RESULT = splice(ARRAY, OFFSET, LENGTH, LIST);
319             # Just redo the splice.
320             # (sub splice also made sure that for calls to splice without
321             # all the arguments, the missing arguments were added, and that OFFSET
322             # would be >= 0)
323              
324 0     0     my $splice = shift;
325             # Could do this quicket with no strict refs :)
326 0           my ($set, $offset, $in_cards, $length) =
327 0           map {$splice->{$_}} qw(set offset in_cards length);
328              
329             # Do the splice and return its return value
330             # (Return will actually be out_cards!)
331 0           $set->splice ($offset, $length, $in_cards);
332             } # end sub Cards::Games::Undo::Splice::redo
333              
334             } # end package Games::Cards::Undo::Splice
335              
336             1; # end package Games::Cards::Undo