Skip to content
/ urru Public

Three puzzles - Flow, Fold, Fill - with Undo/Redo and more, lanterna or scalaFX UI

License

Notifications You must be signed in to change notification settings

sjbiaga/urru

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

15 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Undo-Redo-Redo-Undo

A Game is composed of colored "items" that correspond to:

  • lines between pairs of points, in the case of game-flow;
  • folding blocks in the case of game-fold, and
  • compact blocks in the case of game-fill.

Besides the intuitive UI that allows the player to use the mouse, there is also a keyboard interface: for a short description of (some of) the keys, press ESCAPE. In order to play a certain level, there is a file format per game described in, respectively, the files "flow.txt", "fold.txt" and "fill.txt": these files should be copied and edited to provide a level. Then, the file names must be programmatically specified in one of the two Main.scala source files, either in the package ui.lanterna (for playing in a limited ANSI terminal) or ui.scalafx (full JavaFX graphics).

Paths

Per game, each item holds a "linked list" that grows by each move: it is called a "path", because the case class - named Path for a game - has a parent field through which the "list" is linked back to None - the "empty" start of the game. (In fact, there is a Path per item with each restart of a game.)

An instance of Path has two fields of type Option[?] named "undo" and "redo": following back through "undo" is just as following back through "parent"; however, upon multiple "undos", not only does the path update to the parent, but also a "redo" list is composed such that, following it through to the end, the reverse list of "lost" undos can be traversed (and so, "redone").

                   _
   ,---------------|-.
   |       _         |
   |       | next    |
   |    ,--|---.     |
   |  A | undo |     |
   |    `------'     |
   |       ^         |
   /-------|---------'
  /        |       ^
 Path      |       | parent
  \        |       |
   \-------|-------|-.
   |       | next    |
   |    ,--|---.     |
   |  B | undo |<............
   |    `------'     |      |
   | ,------.        |      | next
   | |      |        |   ,--|---.
   | | redo ------------>| undo | C
   | |      |        |   `------'
   | `--|---'        |      ^
   |    | next       |      |
   `----|------------'      |
        v                   |
     ,------.               | next
     |      |            ,--|---.
   E | redo ------------>| undo | D
     |      |   undo     `------'
     `--|---'
        | next
        _

An "undo" has a move field that holds the corresponding move, i.e., that can be "undone". This field is preserved even in a "redo", because the latter holds the "undo" in its undo field, and so the move - via undo.move. For game-fill, a move in the opposite direction as the previous, it is intercepted also an undo.

A "redo" is much like a move. Following a "redo", a new instance of Path is created with the undo field taken from its now-parent's redo, and this "redo" is of no further use, although kept as such in the now-parent Path.

                   _
   ,---------------|-.
   |       _         |
   |       | next    |
   |    ,--|---.     |
   |  A | undo |     |
   |    `------'     |
   |       ^         |
   `-------|---------'
           |       ^
           |       | parent
           |       |
   ,-------|-------|-.
   |       |         |
   |       | next    |
   |    ,--|---.     |
   |  B | undo |     | (<- unused redo)
   |    `------'     |
   |       ^         |
   /-------|---------'
  /        |       ^
 Path      |       | parent
  \        |       |
   \-------|-------|-.
   |       | next    |
   |    ,--|---.     |
   |  C | undo |<............
   |    `------'     |      |
   | ,------.        |      | next
   | |      | E      |   ,--|---.
   | | redo ------------>| undo | D
   | |      |   undo |   `------'
   | `--|---'        |
   |    | next       |
   `----|------------'
        _

When a move is performed, the "redo" list vanishes, unless the move is the same with that of the "redo". Here, not only the two moves must be the same, but they must also be performed with the same intensity (from the part of the player). This amounts to the local context of the move, that can be quantified with regard to the status of the grid: clues and other items. In the simplest case, the move is free absolutely, when the intensity of the redo move (kept in its undo) is that this undo would be just a backtrack (without "clashes") from the part of the player.

The state of a game is a mutable list, with an index for each item. The color is a negative number, calculated by negating the index (divided by 2, for the game-flow) and subtracting one. The index is a positive number, obtained from the color by the same calculation: for the game-flow, further multiplied by 2 and added either 0 or 1.

Just - The Past Simple

The "undos" are essential because they are added in a mutable list in the parent, alternating with the corresponding "redo": this list becomes (part of) the "past" (simple) for an instance of Path created upon a (new) move. It can be accessed from the current instance of the Path (from the game state) through the parent field. It holds objects of type Just, but in reality, the Undo and Redo case classes only, inherit this trait.

Consider a move which is undone: this undo is added at the end of the mutable list - of the instance of Path that has now become current (and was former the parent). Next, consider a redo. But this performs the move prior to the undo: as well, this is also added at the end of the mutable list. Now, the "same" undo again - added at the end of the mutable list. In general, cycling the undo/redo prior to a new distinct move, will correspond to successive elements, either undo or redo, for the same move, at the end of the mutable list that is (becomes) the "simple past" - for any new instance of Path.

An element of the "simple past" holds an instance of its Path as well. So, consider a new distinct move: this move will be performed also for all the "just" mutable lists encountered up through the parent field, on each element's own Path. A quick inductive thinking reveals that there is an invariant for all these "just" elements, which is that they correspond at all times to the actual just one grid, and thus need not store a grid, resulting in a low memory footprint.

This is not all the advantage, though. After a - say, an - undo is added as an element of the "just" mutable list, a new distinct move which is undone, will not only create a subsequent undo added after this mentioned one, but also in the "just" list of the current Path of this one. Hence, there appear to be three combinations: the first undo by itself, the second undo by itself, and the first undo together with the second undo. Of course, these correspond to the same item, at the same depth, but distinct moves.

In this manner, rather than complicatedly generating combinations from a "just" mutable list, a tree-like structure is obtained, whose simple traversal (albeit not quite so direct) allows to apply a callback on the collected path of multiple undos (and redos), and have these analysed in conjunction. Moreover, these can be (de)serialized as case classes, thus storing actually the tree. However, in memory, the tree will fast grow exponentially with each undo/redo, because it really captures all possible combinations.

The memory grows exponentially, because when the immutable undos/redos are added, the sequence of moves, undos and redos, is replayed (or reflected, but not from the very beginning, as a further undo would remove a Path together with its " just") commencing with a fresh instance of Path, in each case: it is this copy of an undo or redo case class that is added at the end of the "just" mutable list. The rest of the fields (i.e., besides the path: Mutable[?] and identifier: Mutable[Long] fields) are ignored when an undo or redo becomes "part of the past simple".

Since the methods invoked on the Path case class are the same even for replaying, an Option[Long] parameter id tells whether to avoid replaying. The invariant of there existing only a grid is preserved as follows.

For an undo, replaying (when reverting from a current child to its parent) means performing

  1. the child's undo field's move with a fresh (empty) Path;
  2. the moves from the undo for each redo "forward";
  3. the undos for each redo "backward"; and,
  4. because replaying occurs on the child, also just one more undo (reflecting the pass from child to parent).

[The resulting undo-as-just is added at the end to the parent's just mutable list (to be such simple past for further child Paths).]

For a redo, replaying (when creating a child from its current parent) means performing

  1. the undo field move with a fresh (empty) Path;
  2. the moves from the undo for each redo "forward";
  3. the undos for each redo "backward"; and,
  4. because replaying occurs on the parent, also just one redo (reflecting the pass from parent to child).

[The resulting redo-as-just is added at the end to the current just mutable list, but which becomes a parent Path.]

In either case, the resulting Path of the replayed and copied undo or redo, is from now on bi-similar with the returned Path; or at most until the instance of Path to whose just mutable list they are added, it is "gone" - if undone. This is when "repeal" occurs: TODO.

Pending Batch(es)

With the contingent exception of game-fill, a move may require undo's on other items, until the move is possible.

For the game-flow, a move is always possible, i.e. other line(s) "snap" until reducing to at most the pair of clue points (of course, a move into any of the pair of clue points is impossible); however, when the two halves of the same color would (snap and) join at ninety degrees upon a bridge clue, this move is prohibited: not that it couldn't make a pending, but otherwise there might be two pendings (one "collinear", one "not collinear"), whereas at most one pending is assumed.

For the game-fold, a move may not be possible, because a block can at most fold to its initial shape, that cannot be eliminated.

For the game-fill, although drag (move into the pad) and drop (move out of the pad) are also accounted for in the undo/redo lists, a cascade of undos of other blocks leading to a cascade of undos of yet other moves, may become circular before the blocks can be "dragged" out from the grid onto the pad, so a move is not always possible.

[The condition when a move is abandoned (not when it is an undo) occurs in a breadth-first-search manner. To an item ("visited node") there correspond several blocks that are the moves that need to be undone. For each such block in turn, there are searched the items that require several undos (until the former block does not collide with a latter undone block). However, if for any two different items attempted to be undone (one that has been visited and one that is being visited), there are any blocks to be undone that have points in common - collide -, this is the condition for the initial move to be abandoned.

Initially, for the item that is to be moved, there are two blocks that must not collide (with other items' undone blocks): the block before the move and the block after the move (as with the rest, actually). When dropping a block onto the grid from the pad, the former block is empty - thus, it never collides.

It is true that a move may be possible, but not with this algorithm.

The drawback is, of course, that the requirement for any two undone blocks, corresponding to two different items, not to collide, is too strong (because any of these do not occur both at once). Nevertheless, it integrates seamlessly with the undo-redo mechanism, and can always be ameliorated by previously manually dragging out items that otherwise "stuck" the move onto the pad.

The advantage is that impossible moves are not possible.]

For the cases when undos must precede a possible move in order to make it occur, it is resorted to the very methods of undo (and redo), only which do not alter the past simple, nor are considered from the point of view of intensities: they are called batch undo and batch redo.

Because a batch may be independent of other batches, there is a stack-like structure - the "pending" - that is cleared under certain conditions (which obviously interfere with some "untouchable" batch); otherwise, the pending batch undos may be batch redone, if only in the reverse order in which they occurred.

A (pending) batch associates an item with other items that are undone upon the item's move. If possible ("batchable", depending on the game), the "top" (of the stack-list) "pending" batch accumulates in a HashMap the index of each undone item, mapped to how many undos (a batch will redo).

Thus, an element of the pending mutable stack-list, is a pair:

  • the current move's item's index;
  • a mapping from the indexes of the (batch) undone items to an undo count.

The conditions under which the "pending" is cleared are as follows:

  • the current move's item's index already exists in the "pending" list, as the first of a pair - and there can be but one;

  • the current move's item's index appears in the "pending" list, contained in the second HashMap of a pair - an interference that cancels some batch(es);

  • the "pending" stack-list contains one or more elements, but the current undo's is not on the top of the stack; otherwise, a batch redo occurs.

A batch redo iterates over the set of indexes (of the items that were undone), and for each index, performs so many redos as mapped at the index.

For the game-flow, if the current move's line actually "snaps" the same-color paired line, this is not considered a batch undo, but the join of the two lines (of the same color).

Have - The Past Perfect

There is a single case class (per game) that inherits the Have (past perfect) trait: a Board is the grid without any rules or player, which anything can pile up on, in no matter what order or how many times: inconsistencies are extensional. The grid of a Have (Board) is reached by the player, and upon an undo, it is added to the parent Path's have mutable list: it "has been", so there is no longer any intensity.

Whenever there is a move by the player, this move piles up on every Board in the have mutable list. Nothing extensional corresponds to undo/redo.

About

Three puzzles - Flow, Fold, Fill - with Undo/Redo and more, lanterna or scalaFX UI

Topics

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages