Group Abstract Group Abstract

Message Boards Message Boards

Project L board game analysis

Posted 3 years ago

In recent investigations of multiway games, we generated more content than was really necessary to get the point across. In addition to RandomSierpinskiMaze (the subject of another post) the list of omissions also includes a few insightful results about the Project L board game. The purpose of this memo is to pose a problem about polyomino tilings in which summer school students have a chance of joining our efforts by reproducing our findings. If verifying reproducibility is not enough for the terribly ambitious, there is also chance for innovation and originality. My personal solution of the following problem did not use an optimal time algorithm, and more comprehensive analysis remains to be done if we can improve efficiency of our methodology.

From a mathematical viewpoint the board game "Project L" simply asks players to solve exact cover problems given limited resources and time constraints. It is an "engine building" game in the sense that, as time progresses, players build up a collection of tiles, which are used as resources for completing puzzles. So let's start there, with a complete depiction of all available resources, the polyomino pieces which are used to solve tiling puzzles:

project L pieces

What we see here is a particularly nice, almost-symmetrical tiling comprised of yellow monominos, green dominos, orange and blue trominos, and five colors of tetrominos (yes, the tetris pieces). No one player ever has control over all pieces, so such an aggregation could never be created in game. In fact, the rules state that each player starts with one yellow monomino and one green domino, and it takes time and puzzles solved to build up a larger hand of tiles (probably not exceeding 20 pieces).

Just to give you readers an idea how small and relatively easy the puzzles are, here is a sample of five taken from the entire set of 52 (digitized by yours truly):

puzzles

When a player solves such a puzzle by filling it in with polyomino pieces, they are awarded a new polyomino for their collection (top right) and possibly points toward their total score (top left). At the end of the game the player with the highest score wins (but the winning player can also be expected to have collected a pretty nice and diverse protfolio of tiles).

If you look closely, each puzzle is characterized by three weight metrics: The number of points awarded in the top left corner, the grid size (or weight) of the polyomino reward piece, and the total "cost" to solve the puzzle, which is counted by the grid size of the recessed, white, inner regions. There is definitely a cost ~= reward balance in each of these puzzles, which we will return to shortly.

Now before going any farther into game mechanics, let's talk about classical mechanics. When someone gives you a finite, spendable resource, the first physical quantity you should think of is energy. Indeed, during the game, tiles are a form of energy, which are spent toward completing patterns at variable rates of power. When considering points taken per turn, and gauging against power, it also becomes possible to define efficiency. Luck aside, we generally assume that the most efficient players are the ones who should win the engine building games.

Now, let us get to the games basic per turn mechanic. Each player's turn consists of three consecutive actions, which are chosen from a list of energy-rated alternatives:

  1. A puzzle is drawn from the bank at no cost.
  2. A tile of weight N is played into a puzzle.
  3. A tile of weight 1 is drawn from the bank.
  4. A tile of weight N is traded to the bank for another of max weight N+1.
  5. Multiple tiles of total weight X can be played into consecutive puzzles.

The fourth alternative is the odd one out, and may only be used once per turn. Since total weight is a summation of up to four individual tile weights (players can have at max four puzzles on board), the play of X per one time interval is usually the player's maximum power action. If used properly, "master actions" should lead to advantageous gains. But dominant play also requires one to know which are the best pieces to have in hand, and which are the best puzzles to have on board.

Now for some insight toward winning the game, let's define efficiency with regard to gaining points.

Assume that every point value P is in fact an expected time T = P to completion. The weight W of the interior puzzle can be considered an energy E = W, such that the nominal power rating of a puzzle is E / T = W / P. Now let's call the actual time to completion T' and the actual completion power W / T', such that the efficiency of solving is simply P / T'. In this definition, efficiency can also be thought of as a scale factor: actual completion power = efficiency * nominal completion power [W/T' = (P/T')*W/P ].

According to my comprehensive data analysis, most puzzles in the game are balanced such that P / T' is less than or equal to one. However, all 5-point puzzles (and a few more at 3, 4 points) allow that P / T' can be greater than one. Quite obviously, such puzzles are desirable to have on board during the end game when "engine building" is no longer as important as "engine spending".

So finally, we're done with a practical, real-life example set up, and we are motivated to find out all the possible ways that five point puzzles from Project L can be solved with a relative efficiency greater than one. Here they are, the particular five point puzzles:

enter image description here

rawData={{{1, 0, 0, 1, 1}, {1, 0, 0, 0, 0}, {1, 0, 0, 0, 0}, {0, 0, 0, 0, 
   1}, {1, 1, 0, 0, 1}}, {{0, 0, 0, 0, 1}, {0, 0, 0, 0, 1}, {1, 0, 0, 
   0, 0}, {1, 0, 0, 0, 0}, {1, 1, 1, 1, 1}}, {{1, 0, 0, 0, 1}, {0, 0, 
   0, 0, 0}, {0, 0, 0, 0, 0}, {1, 0, 0, 0, 1}, {1, 1, 1, 1, 1}}, {{0, 
   0, 0, 0, 1}, {1, 0, 0, 0, 0}, {1, 0, 0, 0, 0}, {0, 0, 0, 0, 1}, {1,
    1, 1, 1, 1}}, {{0, 0, 0, 0, 0}, {0, 0, 0, 0, 0}, {0, 0, 0, 0, 
   0}, {1, 1, 0, 1, 1}, {1, 1, 1, 1, 1}}};

But the challenge is not only to solve these puzzles in all possible ways (assuming maximum efficiency), but also to give a quantitative comparison of the most useful tetrominos when attempting to do so! Which tetromino is most useful in the end game of Project L?

HINT: it is possible to modify MultiwayDeletionsGraph to solve this problem (what I did), but it would probably be faster to implement D. Knuth's Dancing Links. Extra Credit: If it is possible to implement DLX, what is the graph structure on which the DLX backtracker operates?

POSTED BY: Brad Klee
10 Replies
Posted 3 years ago

Hi Victor, Thanks for your interest!

We are slowly but surely moving ahead with this project, and FindExactCover should go through WFR pretty soon. The final write-up will be more interesting than these basically public-facing notes I have here, because we're starting to understand why timing statistics look the way they do. Unfortunately, we still haven't succeeded in compiling Knuth's Dancing Links X, which is the main outstanding "To do" item on this mini-project.

That's nice that you figured out how to go faster making the cover matrices. I also improved this code while I was running more tests on the actual solvers (where the bottleneck really is):

PieceRotations[True, True][piece_] := Union[
  ResourceFunction["ArrayRotations"][piece]]

PieceRotations[True, False][piece_] := Union[
  ResourceFunction["ArrayRotations"][piece, False]]

PieceRotations[_, _][piece_] := {piece}

PieceTranslates[piece_, shift_, dims_
  ] := With[{inset = PadRight[PadRight[#,
        dims[[2]], 0] & /@ piece,
     dims[[1]], {ConstantArray[0, dims[[2]] ]}]},
  Catenate[Outer[RotateRight[inset, {#1, #2}] &,
    Range[0, shift[[1]] ], Range[0, shift[[2]]], 1]]]

PartialCoverRows[tfRot_, tfRef_
   ][template_, piece_] := Module[{
   tot = Total[piece, 2],
   zeroes = Position[template, 0],
   rot = PieceRotations[tfRot, tfRef][piece],
   dims = Dimensions[template], shift, candidates},
  shift = Map[dims - Dimensions[#] &, rot];
  candidates = Catenate[MapThread[
     If[MemberQ[Sign[#2], -1],
       Nothing, PieceTranslates[#1, #2, dims]] &,
     {rot, shift}]];
  Select[Outer[#1[[Sequence @@ #2]] &,
    candidates, zeroes, 1],
   Total[#] == tot &]]

MatricesToCoverMatrix[template_, pieces_,
  OptionsPattern[{
    "Rotations" -> True,
    "Reflections" -> True,
    "PlaceOne" -> False
    }]] := If[TrueQ[OptionValue["PlaceOne"]],
  Catenate[MapThread[Function[{piece, ind},
     Join[ind, #] & /@ PartialCoverRows[
        OptionValue["Rotations"], OptionValue["Reflections"]
        ][template, piece]],
    {pieces, IdentityMatrix[Length[pieces]]}]],
  Catenate[PartialCoverRows[
       OptionValue["Rotations"], OptionValue["Reflections"]
       ][template, #] & /@ pieces]] 

Would you want to see this as a WFR? If yes, can you think of a better name for it?

It should be about as fast as what you have, but it's also keyed in with some options and better free space recognition. The design is so that it's easy to make the cover matrix for "Scott's pentomino problem" from Knuth's arxiv article:

pentominoProblem = MatricesToCoverMatrix[
   ArrayPad[ConstantArray[1, {2, 2}], 3, 0],
   upToPentominos[[5]], "PlaceOne" -> True];

Dimensions@pentominoProblem

Out[]= {1568, 72} 

We can find all solutions of this matrix, but our times are still a little slow compared to industry standard (due I think to not having compiled code). Assuming you can make this matrix, what's your fastest time for solving it with Mathematica code?

POSTED BY: Brad Klee

Hi Brad, what a great article! It was a pleasure to read. And as much as I enjoyed understanding the magic in constructing the PolyominoCoverMatrix (and I learned quite a few valuable techniques in the process!) - we can simplify this code and remove a 13.1 dependency.

The construction of the cover matrix is pretty complicated. First, we start from a 2d binary array describing the form and then follow a series of transformations: binary array -> (via ArrayMesh and MeshPrimitives) polyomino graph -> (via FindIsomorphicSubgraph and FindCycle) sorted list of 4-cycles, which we use to create the cover matrix. However, since every four-cycle essentially represents an original cell on the form, we made all this journey only to return to a binary list representing the form.

We can accomplish the same more directly:

f = {{1, 0, 1, 1}, {1, 1, 1, 0}, {0, 0, 1, 1}};

embeds[f_, {n_, m_}] := 
 Module[{d1 = Dimensions@f, board = ConstantArray[0, {n, m}], e},
  e = ConstantArray[board, {n, m} - d1 + {1, 1}];
  Table[e[[i, j]][[i ;; i + d1[[1]] - 1, j ;; j + d1[[2]] - 1]] = f, {i, n - d1[[1]] + 1}, {j, m - d1[[2]] + 1}]; 
  Flatten[e, 1]]

Grid@Partition[ArrayPlot[#, ImageSize -> Tiny] & /@ embeds[f, {5, 5}] , 2]

embeds of the form in 5x5 matrix

allEmbeds[f_, board_] := 
 DeleteDuplicates@Flatten[embeds[#, board] & /@ ResourceFunction["ArrayRotations"][f],1]

Grid@Partition[ArrayPlot[#, ImageSize -> Tiny] & /@ allEmbeds[f, {5, 5}], 6]

enter image description here

PolyominoCoverMatrixFast[forms_, {m_, n_}] := 
 Flatten[Function[{f}, Flatten /@ allEmbeds[f, {m, n}]] /@ forms, 1]

In[85]:= IsomorphicGraphQ[
 CoverGraph[
  PolyominoCoverMatrix[{form}, PolyominoGraph[ArrayMesh[ConstantArray[1, {6, 6}]]]]],
 CoverGraph[PolyominoCoverMatrixFast[{f}, {6, 6}]]]

Out[85]= True

The new method is also faster, which justifies the name :)

In[86]:= PolyominoCoverMatrix[{form}, PolyominoGraph[ArrayMesh[ConstantArray[1, {6, 6}]]]]; // AbsoluteTiming

Out[86]= {0.151714, Null}

In[88]:= PolyominoCoverMatrixFast[{f}, {6, 6}]; // AbsoluteTiming

Out[88]= {0.001137, Null}

And while we are on the topic of simplification, I will pedantically point out that !MemberQ is called FreeQ, and that

In[92]:= IsomorphicGraphQ[PolyominoGraph[ArrayMesh[ConstantArray[1, {5, 5}]]], GridGraph[{6, 6}]]
Out[92]= True

(although I understand while you define emptiness in terms of PolyominoGraph for consistency).

POSTED BY: Victor Kryukov

enter image description here -- you have earned Featured Contributor Badge enter image description here Your exceptional post has been selected for our editorial column Staff Picks http://wolfr.am/StaffPicks and Your Profile is now distinguished by a Featured Contributor Badge and is displayed on the Featured Contributor Board. Thank you!

POSTED BY: EDITORIAL BOARD
Posted 3 years ago

You might want to learn to use the block quote mechanism, for example:

How do we define units for time? Can we really measure the tile's usefulness by the proportion of puzzle solutions that display the tile?

Just highlight relevant text and push the " icon, or you can manually add ">" at the beginning of each line. It would be helpful to readers if you went back and edited your post to make it more clear what is C+P from mine and what else you added.

Here are answers to your questions, which I will quote again, just to emphasize important functionality of the BB, and how much it improves readability.

How do we define units for time?

Local time coordinates can have units of turns or actions. The conversion is three actions per turn. Since the point of the game is to complete puzzles, the global time coordinate should count down (from a maximum of 52) the number of puzzles remaining to be solved. Although "How many puzzles have been solved" is a valid and useful way to measure in-game time, obviously there is not a linear conversion factor to actions. Solving any one puzzle involves finding a path through the multiway graph of all possible exact covers. Different solutions take different amounts of time.

Can we really measure the tile's usefulness by the proportion of puzzle solutions that display the tile?

Instead of "display" perhaps "include"?

Utility principle: "A tile's utility is (at least partially) a function of how many times it is included in the solutions of a set of puzzles".

Non-utility principle: "A tile has no utility if it can't be used to solve any puzzle".

(In the question asked above, we are only concerned with tetrominos. As these are the most powerful to play, they are generally more useful, especially in the end game. However, they are not useful for small puzzles with fewer than four slots.)

Actual measurement is a more nuanced question, because even if the utility principle is correct, getting an actual number for the utility would involve extra arbitrary choices. Similar for fairness metrics. The end game will not be fair if one player has a much better tile set than the others.

POSTED BY: Brad Klee
Posted 3 years ago

Now, Stephen Wolfram has asked to also solve tiling problems in terms of System` function SatisfiabilityInstances, and to that end we already have a couple of WFR's: GenerateTiling and FindMinimalTilings. I used these functions to get some basic results, and they seemed slow to me. Applying the SAT solver to the exact cover problem gives us a benchmark, which then causes us to pause and consider if our methodology in searching tiling space is really reaching optimal times.

In addition to code above, we need another function, which allows input of a cover matrix to the SAT Solver:

OverlapMatrix[covMat_] := With[{len = Length[covMat]},
  Transpose[covMat[[All, len + 1 ;; -1]]]  ]

SATCoverGraph[overlap_] := 
 With[{len = Dimensions[overlap][[2]]}, 
  ResourceFunction["ToDirectedAcyclicGraph"][NearestNeighborGraph[
    SatisfiabilityInstances[And @@ (
        Or[
           BooleanCountingFunction[{1}, len] @@ 
            PadLeft[(x /@ Position[#, 1][[All, 1]]), len, False],
           Nor @@ (x /@ Position[#, 1][[All, 1]])]
          & /@ overlap), x /@ Range[len], All] /. {True -> 1, 
      False -> 0}],
   ConstantArray[0, len],
   GraphLayout -> "LayeredDigraphEmbedding",
   VertexCoordinates -> Automatic,
   AspectRatio -> 1/2]]

As written, the output of SATCoverGraph should be isomorphic to the output of CoverGraph (and the faster computation via augmented MultiwayDeletionsGraph), so we can run a timing test.

The test is similar to what's above, but this time we are trying to fit roto-reflect-translated copies of a chair tromino onto an $n \times 3$ grid.

form = PolyominoGraph[ArrayMesh[{{1, 1}, {1, 0}}]];

times0 = AbsoluteTiming[
     With[{mdgAll = MultiwayDeletionsGraph[
          PairwiseOverlapGraph[PolyominoCoverMatrix[{form},
            PolyominoGraph[ArrayMesh[ConstantArray[1, {#, 3}]]]
            ]],
          {}, "ExactCover"]},
       g0 = 
        EdgeAdd[mdgAll, {} -> # & /@ 
          Select[VertexList[mdgAll], Length[#] == 1 &]]
       ];
     ][[1]] & /@ Range[2, 7]

Out[]= {0.022514, 0.021699, 0.032995, 0.051815, 0.204917, 1.76713}

times1 = AbsoluteTiming[
     g1 = CoverGraph[PolyominoCoverMatrix[{form},
         PolyominoGraph[ArrayMesh[ConstantArray[1, {#, 3}]]]
         ]]; 
     ][[1]] & /@ Range[2, 7]

Out[]= {0.016607, 0.023163, 0.049301, 0.132063, 0.711254, 4.83718}

times2 = AbsoluteTiming[
     g2 = SATCoverGraph[OverlapMatrix[PolyominoCoverMatrix[{form},
          PolyominoGraph[ArrayMesh[ConstantArray[1, {#, 3}]]]
          ]]];
     ][[1]] & /@ Range[2, 7]

Out[]= {0.013869, 0.029674, 0.069643, 0.303353, 2.12466, 28.7976}

IsomorphicGraphQ[g0, g1]
IsomorphicGraphQ[g0, g2]

 Out[28]= True, True

The time statistics do not look good for System`SatsifiabilityInstances:

ListLogPlot[{times0, times1, times2}, PlotRange -> All]

log times

ListPlot[times2/times0]

time ratio

If we adapt the code to try and solve the previous summer school problem (solved minimal graph construct in 2s), what we find is that the SAT solver hangs and can not complete the task! Caveat Emptor!

POSTED BY: Brad Klee
Posted 3 years ago
POSTED BY: Brad Klee
POSTED BY: Zsombor Méder
Posted 2 years ago
POSTED BY: Brad Klee
POSTED BY: Zsombor Méder
POSTED BY: Dean Gladish