3. Triominos Placing a Tile - AgileBitFlipper/triominos GitHub Wiki

Placing a tile on the board requires us to determine if the tile will fit on the board at the location in any orientation and rotation. In the following paragraphs I hope to show you how we can establish a set of simple rules and procedures for automating the placement of a tile at various stages of the Round.

First Tile

Once the correct player to start is determined, based on the set of rules described in Triominos, a tile is chosen from the Player's tray. This tile is placed at location (56,56) in the downward orientation. The first tile placement doesn't need to follow any placement rules other then the location. However, all other tile placements depend on the open faces of tiles already played on the board.

Orientation

Internally, the orientation of a tile when placed on the board is determined by adding the row and column values together; if the sum is an even value, the tile is oriented downward. Conversely, if the sum is an odd value then the tile is oriented upward.

Default Board Sizing

The choice of (56,56) as the starting location is solely based on the possible growth of the board as gameplay progresses. Since there are 56 tiles, growth in a linear fashion in any direction will still result in all the tiles fitting on the board. I'm sure an optimization can occur here if needed, but for now I see no need to spend any time shrinking or growing the board dynamically.

Face Comparison

As indicated earlier, the next tile to be placed is determined by matching the face of a tile in a player's tray to any open face on a tile on the board. The following diagram demonstrates how faces are examined to find a match. A face match is determined by comparing the matching corners from two adjacent tiles. Every open face on the board is tried against every face on each tile in the Player's tray. If a match is found, a score is determined for the placement of that tile, and that choice is kept in a list of all the other matches. The match with the highest score is chosen and played.

Open Face Matching

Corner Comparison

Even when a matching face is found, it doesn't guarantee a fit on the board. A corner matching should also be performed to make sure that the every corner of the choice also matches any adjacent tiles when placed on the board. The diagram below shows how each corner is examined to determine if it matches each of matching corners of the adjacent tiles. This comes into play most importantly when the third corner completes a bridge.

Corner Comparison

Scoring

The scoring of a tile is based on a few factors. The first is the base value of the tile. This is determined by adding up the values of each corner of the tile being placed. In the case of Tile '2-3-4' the value would be '2+3+4' or '9'. Next, there is a check to see if the player earned bonus points because of the placement of the new tile. Bonuses can occur when the tile played creates a Hexagon, a Bridge, or it is the last tile played from the Player's tray.

Hexagon

When a tile is placed on the board, and that placement results in the completion of a hexagon, a bonus of 50 points is awarded to the player. Determining if a placement completes a hexagon is challenging since each tile has the potential of completing up to three hexagons.
The following diagram shows the three possibilities. The placement of 'Tile 10' (green) can complete the hexagon to the left (orange), to the right (purple), and the hexagon to the top (red). Internally, the pattern to test for the creation of a hexagon is straight forward, but it is lengthy. Since the orientation of a played piece impacts the set of checks that need to be made, it can get a bit unwieldy at times. But, I'll endeavor to make it seem simple. We'll discuss more of this later.

Possible Hexagons

Bridge

When a tile is placed on the board, and that placement results in the completion of a bridge, a bonus of 40 points is awarded to the player. Determining if a piece that is played completes a bridge can be seen in the image below. Since a piece requires at least a single face to match, the easiest bridge would be composed of a single face and the opposite corner matching like 'Tile 13'. Face BC on 'Tile 16' matches Face BC on 'Tile 13', and Corner A matches both Corner A on 'Tile 5' and Corner B on 'Tile 4'. Since there is no adjacent tile on Face CA on 'Tile 13' this placement completes a bridge.
The calculations to determine if a bridge is created is a bit more complicated than that of a hexagon. But it is just about a lengthy. Again, we'll discuss more about the logic to determine if a bridge is created later.

Hexagon and Bridge

End of Round

As stated earlier, the end of a round is triggered if no player can play a tile, or is a player plays all of the tiles in their tray. The winning player is chosen based on an empty tray, or the fewest tiles in their tray in the case of an impasse. The winner by empty tray earns a bonus as well as the spoils of all the points for each tile in the other players' trays. The winner by impasse only receives the points from tiles in other players' trays.

End of Game

The first player to earn more than 400 points wins. If more than one player is above 400 points, the player that won the round wins the game. If the player that won the round is NOT one of the players over 400 points, the player with the highest number of points wins.

Board Size and Display

After each play, a calculation is performed to size the board so that we only need to display the portion of the board that actually contains tiles. This calculation is brute force, and involves spinning trough each of the 112x112 spaces looking to see if they contain a tile. If they do, the minimum row and column, as well as the maximum row and column is adjusted appropriately. Then, the board is displayed starting from the minimum row and column until it reaches the maximum row and column. Optimization - Once again, there is room for optimization here. At each play, the checks could be performed based on the tiles placement and the values of min and max could be updated on the fly. This would entirely eliminate the need to perform the calculations brute force for each time the board is displayed.

Bonus Calculations

The logic to determine if a hexagon or a bridge is created is straight forward, but it can seem lengthy. The way I approached it was to break each check down to a simple boolean value; true or false. This permits me to combine the boolean values to produce a single result; is it a bridge or is it a hexagon or is it nothing.

Is It A Bridge

Determining if a bridge has occurred is a bit tricky, but do-able. The diagram below should help understand what is needed.

A bridge is completed for placing Tile 10 in an upward orientation if at least one of the following is found to be true:

  • (( Tile 11 or Tile 12 or Tile 13 ) and ( Tile 2 or Tile 3 or Tile 4 ) and ( Face CA is empty ))
  • (( Tile 2 or Tile 3 or Tile 4 ) and ( Tile 16 or Tile 15 or Tile 14 ) and ( Face AB is empty ))
  • (( Tile 16 or Tile 15 or Tile 14 ) and ( Tile 11 or Tile 12 or Tile 13 ) and ( Face BC is empty ))

Bridge In Upward Orientation

Likewise, a bridge is completed for placing Tile 10 in a downward orientation if at least one of the following is found to be true:

  • (( Tile 12 or Tile 11 or Tile 2 ) and ( Tile 4 or Tile 16 or Tile 15 ) and ( Face BC is empty ))
  • (( Tile 4 or Tile 16 or Tile 15 ) and ( Tile 14 or Tile 13 or Tile 9 ) and ( Face CA is empty ))
  • (( Tile 14 or Tile 13 or Tile 9 ) and ( Tile 12 or Tile 11 or Tile 2 ) and ( Face AB is empty ))

In the case of a downward played piece competing a hexagon, the image looks like the following with the code to follow shortly after.

Bridge In Downward Orientation

Code-wise, when you take into consideration the possibility of the edges of the board impacting the placement of the tiles, this looks like the following:

boolean bMiddleFaceEmpty = (row > 0 ) && (pieceAtLocation(row - 1, col) == null);
boolean bAnchorBelow =
  ((( col > 0 )          && ( row < num_rows-1 ) && ( pieceAtLocation(row+1, col-1) != null ) ) ||
    (                       ( row < num_rows-1 ) && ( pieceAtLocation(row+1,col) != null ) ) ||
   (( col < num_cols-1 ) && ( row < num_rows-1 ) && ( pieceAtLocation(row+1,col+1) != null ) )) ;
boolean bAnchorLeft =
  ((( col > 1 )                                  && ( pieceAtLocation(row, col-2) != null ) ) ||
   (( col > 1 )          && ( row > 0 )          && ( pieceAtLocation(row-1,col-2 ) != null ) ) ||
   (( col > 0 )          && ( row > 0 )          && ( pieceAtLocation(row-1,col-1 ) != null ) )) ;
boolean bAnchorRight =
  ((( col < num_cols-2 )                         && ( pieceAtLocation(row,col+2) != null ) ) ||
   (( col < num_cols-2 ) && ( row > 0 )          && ( pieceAtLocation(row-1,col+2) != null ) ) ||
   (( col < num_cols-1 ) && ( row > 0 )          && ( pieceAtLocation(row-1,col+1) != null ) )) ;

if ( ( bLeftFaceEmpty   && bAnchorBelow && bAnchorLeft  ) ||
     ( bRightFaceEmpty  && bAnchorBelow && bAnchorRight ) ||
     ( bMiddleFaceEmpty && bAnchorLeft  && bAnchorRight ) ) {
    bCreatesABridge = true ;
}

The checks for when the placed tile is in the upward orientation is similar, and looks something like the following: Possible Bridges - Up Orientation

boolean bMiddleFaceEmpty = (row < num_rows-1) && (pieceAtLocation(row + 1, col) == null);
boolean bAnchorAbove =
  ((( col > 0 )          && ( row > 0 )          && ( pieceAtLocation(row-1, col-1) != null ) ) ||
   (                        ( row > 0 )          && ( pieceAtLocation(row-1,col) != null ) ) ||
   (( col < num_cols-1 ) && ( row > 0 )          && ( pieceAtLocation(row-1,col+1) != null ) ) ) ;
boolean bAnchorLeft =
  ((( col > 1 )                                  && ( pieceAtLocation(row, col-2) != null ) ) ||
   (( col > 1 )          && ( row < num_rows-1 ) && ( pieceAtLocation(row+1,col-2 ) != null ) ) ||
   (( col > 0 )          && ( row < num_rows-1 ) && ( pieceAtLocation(row+1,col-1 ) != null ) )) ;
boolean bAnchorRight =
  ((( col < num_cols-2 )                         && ( pieceAtLocation(row,col+2) != null ) ) ||
   (( row < num_rows-1 ) && ( col < num_cols-2 ) && ( pieceAtLocation(row+1,col+2) != null ) ) ||
   (( row < num_rows-1 ) && ( col < num_cols-1 ) && ( pieceAtLocation(row+1,col+1) != null ) )) ;

if ( ( bLeftFaceEmpty   && bAnchorAbove && bAnchorLeft  ) ||
     ( bRightFaceEmpty  && bAnchorAbove && bAnchorRight ) ||
     ( bMiddleFaceEmpty && bAnchorLeft  && bAnchorRight ) ) {
    bCreatesABridge = true ;
}

Is It A Hexagon

The logic for determining if a hexagon is complete appears to be straightforward, but lengthy. For this type of check, I thought it would be easier to think of each check in terms of the row and column offset from the tile placed. The logic involved can be broken down as follows:

  • (Tile 9 and Tile 8 and Tile 13 and Tile 14 and Tile 15) or
  • (Tile 11 and Tile 12 and Tile 17 and Tile 16 and Tile 15) or
  • (Tile 9 and Tile 2 and Tile 3 and Tile 4 and Tile 11

Possible Hexagons - Upward Orientation

For the tile being placed in an upward orientation, the code looks like the following:

int hexLRD[] = {  0,  0,  1,  1, 1 } ;
int hexLCD[] = { -1, -2, -2, -1, 0 } ;
int hexRRD[] = {  1,  1,  1,  0, 0 } ;
int hexRCD[] = {  0,  1,  2,  2, 1 } ;
int hexARD[] = {  0, -1, -1, -1, 0 } ;
int hexACD[] = { -1, -1,  0,  1, 1 } ;

if ( row < num_rows-1 ) {
  if ( col > 1 ) {
    bULHexagon = true ;
    // hexagon left
    for ( int i=0; i<5; i++ ) {
      bULHexagon &= ( pieceAtLocation(row + hexLRD[i],col + hexLCD[i]) != null ) ;
    }
    if ( bULHexagon )
      Log.Info("  Completed hexagon with Up-Left orientation!");
  }
  if ( col < num_cols-2 ) {
    URHexagon = true ;
    // hexagon right
    for ( int i=0; i<5; i++ ) {
      bURHexagon &= ( pieceAtLocation(row + hexRRD[i],col + hexRCD[i]) != null ) ;
    }
    if ( bURHexagon )
      Log.Info("  Completed hexagon with Up-Right orientation!");
  }
}
if ( row > 1 ) {
  if ( col > 1 && col < num_cols - 1 ) {
    bUMHexagon = true ;
    // hexagon above
    for ( int i=0; i<5; i++ ) {
      bUMHexagon &= ( pieceAtLocation(row + hexARD[i],col + hexACD[i]) != null ) ;
    }
  }
  if ( bUMHexagon )
     Log.Info("  Completed hexagon with Up-Middle orientation!");
}

A very similar set of checks is performed if the tile is placed in the downward orientation.

Possible Hexagons - Downward Orientation

A snippet of the code that checks for hexagon completions for a tile placed in a downward orientation can be seen below:

int hexLRD[] = {  0,  0, -1, -1, -1 } ;
int hexLCD[] = { -1, -2, -2, -1,  0 } ;
int hexRRD[] = { -1, -1, -1,  0,  0 } ;
int hexRCD[] = {  0,  1,  2,  2,  1 } ;
int hexARD[] = {  0, +1, +1, +1,  0 } ;
int hexACD[] = { -1, -1,  0,  1,  1 } ;
if ( row > 0 ) {
  if ( col > 1 ) {
    bDLHexagon = true ;
    // hexagon left
    for ( int i=0; i<5; i++ ) {
      bDLHexagon &= ( pieceAtLocation(row + hexLRD[i],col + hexLCD[i]) != null ) ;
    }
    if ( bDLHexagon )
        Log.Info("  Completed hexagon with Down-Left orientation!");
  }
  if ( col < num_cols-2 ) {
    bDRHexagon = true ;
    // hexagon right
    for ( int i=0; i<5; i++ ) {
      bDRHexagon &= ( pieceAtLocation(row + hexRRD[i],col + hexRCD[i]) != null ) ;
    }
    if ( bDRHexagon )
      Log.Info("  Completed hexagon with Down-Right orientation!");
  }
}
if ( row < num_rows-1 ) {
  if ( col > 1 && col < num_cols - 1 ) {
    bDMHexagon = true ;
    // hexagon below
    for ( int i=0; i<5; i++ ) {
      bDMHexagon &= ( pieceAtLocation(row + hexARD[i],col + hexACD[i]) != null ) ;
    }
    if ( bDMHexagon )
      Log.Info("  Completed hexagon with Down-Middle orientation!");
  }
}