Generador de tableros para una implementación de Food Chain Magnate

Aug 17 2020

Estoy implementando la lógica de un juego de mesa en Java para practicar mis habilidades de codificación. Elegí Food Chain Magnate porque es un juego bastante complejo que requiere diferentes estructuras de datos. Una de mis primeras tareas es generar la estructura de datos para el tablero de juego. En el juego, el tablero se construye usando algunas (de todas, dependiendo del número de jugadores) de las 20 fichas disponibles. Cada ficha es una cuadrícula de 5 x 5 cuadrados. La identidad de cada ficha no es importante durante el juego, solo si alguna casilla está en una ficha diferente a otra.

Creé una Boardclase que es básicamente una envoltura sobre una matriz de objetos 2D con métodos adicionales para el cálculo, y BoardGeneratorque crea Boardsy la inicializa con el contenido de los diferentes mosaicos.

Board.java

package com.lartkma.fcm.model.board;

public class Board {
    
    public static final int TILE_SIZE = 5;
    public static final Object OFF_LIMIT = new Object();
    
    private Object[][] boardSquares;
    
    public Board(int widthTiles, int heightTiles) {
        this.boardSquares = new Object[widthTiles * TILE_SIZE][heightTiles * TILE_SIZE];
    }
    
    public Object get(int x, int y) {
        if (x >= 0 && x < this.boardSquares.length && y >= 0 && y < this.boardSquares[0].length) {
            return this.boardSquares[x][y];
        } else {
            return OFF_LIMIT;
        }
    }
    
    public Object get(Point p) {
        return get(p.x(), p.y());
    }
    
    public void set(int x, int y, Object obj) {
        if (x >= 0 && x < this.boardSquares.length && y >= 0 && y < this.boardSquares[0].length) {
            this.boardSquares[x][y] = obj;
        } else {
            throw new IndexOutOfBoundsException("Point " + new Point(x, y) + " is out of the board");
        }
    }
    
    public void set(Point p, Object obj) {
        set(p.x(), p.y(), obj);
    }
    
    public int getWidth() {
        return this.boardSquares.length;
    }
    
    public int getHeight() {
        return this.boardSquares[0].length;
    }
    
    /**
     * Returns the tile where the square belongs, relative to this board. The value
     * is not related to the original tile used to build the board, only allows to
     * differentiate one tile from another.
     * @param p
     * @return
     */
    public int getTileNumber(Point p) {
        return (p.y() / TILE_SIZE) * (this.boardSquares.length / TILE_SIZE) + (p.x() / TILE_SIZE);
    }

}

La Pointclase es un simple, inmutable clase de punto 2D con un constructor Point(int x, int y), los métodos x(), y()para la recuperación y un add(int dx, int dy)método que devuelve el punto (x + dx, y + dy). No lo estoy escribiendo aquí para enfocarme en las otras clases.

BoardGenerator.java

package com.lartkma.fcm.model.board;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.stream.Stream;

public class BoardGenerator {

    public static Board fromRandom(int widthTiles, int heightTiles) {
        Random rnd = new Random();
        Map<Character, Object[][]> tileList = getTileList();
        List<Character> randomTiles = new LinkedList<>(tileList.keySet());
        Collections.shuffle(randomTiles);

        Board board = new Board(widthTiles, heightTiles);
        for (int i = 0; i < widthTiles; i++) {
            for (int j = 0; j < heightTiles; j++) {
                fillWithTile(board, tileList.get(randomTiles.get(i * heightTiles + j)), i * Board.TILE_SIZE,
                        j * Board.TILE_SIZE, rnd.nextInt(4));
            }
        }
        return board;
    }

    /**
     * Generates a board using the tiles and rotations indicated in the expression.
     * 
     * The expression is composed of (# tiles tall) subexpressions separated by
     * newlines or spaces, each subexpression made of (# tiles wide x 2) characters.
     * 
     * Each 2 characters of a subexpression describe a tile and the rotation of such
     * tile. The tile is indicated with one of the upper-case characters used in
     * <a href="https://boardgamehelpers.com/FoodChainMagnate/MapTileKey.aspx">this page</a>.
     * The rotation is described as a digit from 1 to 4, where 1 is the orientation shown in
     * the page mentioned above (with the identified in the bottom left), 2 rotates the
     * reference orientation rotated 90 degrees clockwise, and so on.
     * 
     * @param expression
     * @return
     */
    public static Board fromExpression(String expression) {
        String[] rows = expression.split("\n|\r\n| ");
        int heightTiles = rows.length;
        int widthTiles = Stream.of(rows).mapToInt(s -> s.length() / 2).max().orElse(0);
        Board board = new Board(widthTiles, heightTiles);
        Map<Character, Object[][]> tileList = getTileList();
        for (int i = 0; i < widthTiles; i++) {
            for (int j = 0; j < heightTiles; j++) {
                if (2 * i + 1 < rows[rows.length - 1 - j].length()) {
                    char tileId = rows[rows.length - 1 - j].charAt(2 * i);
                    char tileRotationFactor = rows[rows.length - 1 - j].charAt(2 * i + 1);
                    if (tileList.containsKey(tileId) && tileRotationFactor >= '1' && tileRotationFactor <= '4') {
                        // Number of rotations goes from 0 to 3
                        fillWithTile(board, tileList.get(tileId), i * Board.TILE_SIZE, j * Board.TILE_SIZE,
                                tileRotationFactor - '1');
                    } else {
                        throw new IllegalArgumentException(
                                "Board tile expression \"" + tileId + tileRotationFactor + "\" cannot be read");
                    }
                }
            }
        }
        return board;
    }

    private static Map<Character, Object[][]> getTileList() {
        Map<Character, Object[][]> outputMap = new HashMap<>();
        try (BufferedReader stream = new BufferedReader(
                new InputStreamReader(BoardGenerator.class.getResourceAsStream("tiles.txt")))) {
            int lineCount = 1;
            Object[][] currentTileContent = new Object[Board.TILE_SIZE][Board.TILE_SIZE];
            char currentTileIdentifier = 'A';
            String currentLine;
            while ((currentLine = stream.readLine()) != null) {
                for (int i = 0; i < Board.TILE_SIZE; i++) {
                    char lineChar = currentLine.charAt(i);
                    if (lineChar == 'O') {
                        currentTileContent[i][Board.TILE_SIZE - lineCount] = null;
                    } else if (lineChar == '-') {
                        currentTileContent[i][Board.TILE_SIZE - lineCount] = new Road(false, true, false, true);
                    } else if (lineChar == '|') {
                        currentTileContent[i][Board.TILE_SIZE - lineCount] = new Road(true, false, true, false);
                    } else if (lineChar == '/') {
                        // check the previous and next squares in the same line to check if this is
                        // a up-to-right turn or a right-to-up turn
                        char previous = (i == 0 ? 'O' : currentLine.charAt(i - 1));
                        char next = (i == Board.TILE_SIZE - 1 ? 'O' : currentLine.charAt(i + 1));
                        if ((isHorizontalRoad(previous) || i == 0) && !isHorizontalRoad(next)) {
                            currentTileContent[i][Board.TILE_SIZE - lineCount] = new Road(true, false, false,
                                    true);
                        } else if (!isHorizontalRoad(previous)
                                && (isHorizontalRoad(next) || i == Board.TILE_SIZE - 1)) {
                            currentTileContent[i][Board.TILE_SIZE - lineCount] = new Road(false, true, true,
                                    false);
                        } else {
                            throw new IllegalStateException("Unknown combination on ( " + currentLine + ")");
                        }
                    } else if (lineChar == '\\') {
                        // check the previous and next squares in the same line to check if this is
                        // a up-to-left turn or a left-to-up turn
                        char previous = (i == 0 ? 'O' : currentLine.charAt(i - 1));
                        char next = (i == Board.TILE_SIZE - 1 ? 'O' : currentLine.charAt(i + 1));
                        if ((isHorizontalRoad(previous) || i == 0) && !isHorizontalRoad(next)) {
                            currentTileContent[i][Board.TILE_SIZE - lineCount] = new Road(false, false, true,
                                    true);
                        } else if (!isHorizontalRoad(previous)
                                && (isHorizontalRoad(next) || i == Board.TILE_SIZE - 1)) {
                            currentTileContent[i][Board.TILE_SIZE - lineCount] = new Road(true, true, false,
                                    false);
                        } else {
                            throw new IllegalStateException("Unknown combination on ( " + currentLine + ")");
                        }
                    } else if (lineChar == '^') {
                        currentTileContent[i][Board.TILE_SIZE - lineCount] = new Road(true, true, false, true);
                    } else if (lineChar == '>') {
                        currentTileContent[i][Board.TILE_SIZE - lineCount] = new Road(true, true, true, false);
                    } else if (lineChar == 'V') {
                        currentTileContent[i][Board.TILE_SIZE - lineCount] = new Road(false, true, true, true);
                    } else if (lineChar == '<') {
                        currentTileContent[i][Board.TILE_SIZE - lineCount] = new Road(true, false, true, true);
                    } else if (lineChar == '+') {
                        currentTileContent[i][Board.TILE_SIZE - lineCount] = new Road(true, true, true, true);
                    } else if (lineChar == 'S') {
                        currentTileContent[i][Board.TILE_SIZE - lineCount] = GoodsSource.SODA;
                    } else if (lineChar == 'B') {
                        currentTileContent[i][Board.TILE_SIZE - lineCount] = GoodsSource.BEER;
                    } else if (lineChar == 'L') {
                        currentTileContent[i][Board.TILE_SIZE - lineCount] = GoodsSource.LEMONADE;
                    } else if (lineChar >= '0' && lineChar <= '9') {
                        Object previous = (i == 0 ? null : currentTileContent[i - 1][Board.TILE_SIZE - lineCount]);
                        if (previous instanceof House) {
                            // part of a two-digit house, same entity as left
                            currentTileContent[i][Board.TILE_SIZE
                                    - lineCount] = currentTileContent[i - 1][Board.TILE_SIZE - lineCount];
                        } else {
                            int houseOrder = (lineChar - '0'); // classic
                            char next = (i == Board.TILE_SIZE - 1 ? 'O' : currentLine.charAt(i + 1));
                            if (next >= '0' && next <= '9') { // two digit id
                                houseOrder = houseOrder * 10 + (next - '0');
                            }
                            currentTileContent[i][Board.TILE_SIZE - lineCount] = new House(houseOrder);
                        }
                    } else if (lineChar == 'H') {
                        Object previous = (i == 0 ? null : currentTileContent[i - 1][Board.TILE_SIZE - lineCount]);
                        if (previous instanceof House) {
                            // same entity as left
                            currentTileContent[i][Board.TILE_SIZE - lineCount] = previous;
                        } else {
                            previous = (lineCount == 1 ? null
                                    : currentTileContent[i][Board.TILE_SIZE - lineCount + 1]);
                            if (previous instanceof House) {
                                // same entity as up
                                currentTileContent[i][Board.TILE_SIZE - lineCount] = previous;
                            } else {
                                throw new IllegalStateException(
                                        "Unknown combination on ( " + currentLine + "): no house defined near H");
                            }
                        }
                    } else {
                        throw new IllegalStateException("Unknown symbol: " + lineChar);
                    }
                }

                lineCount += 1;
                if (lineCount > Board.TILE_SIZE) {
                    outputMap.put(currentTileIdentifier, currentTileContent);
                    lineCount = 1;
                    currentTileContent = new Object[Board.TILE_SIZE][Board.TILE_SIZE];
                    currentTileIdentifier += 1;
                }
            }

            return outputMap;
        } catch (IOException e) {
            throw new Error("tiles.txt not available", e);
        }
    }

    private static boolean isHorizontalRoad(char c) {
        return c == '-' || c == '/' || c == '\\' || c == '^' || c == 'V' || c == '+';
    }

    private static void fillWithTile(Board board, Object[][] tileArray, int xStart, int yStart, int numRotations) {
        for (int i = 0; i < Board.TILE_SIZE; i++) {
            for (int j = 0; j < Board.TILE_SIZE; j++) {
                Point boardPoint = new Point(xStart + i, yStart + j);
                Point tileCoords = toTileCoords(i, j, numRotations);
                Object inTile = tileArray[tileCoords.x()][tileCoords.y()];
                if (inTile instanceof House) {
                    Object prevHouse;
                    if ((prevHouse = board.get(boardPoint.add(-1, 0))) instanceof House
                            && ((House) prevHouse).getOrder() == ((House) inTile).getOrder()) {
                        // check house at the left
                        board.set(boardPoint, prevHouse);
                    } else if ((prevHouse = board.get(boardPoint.add(0, -1))) instanceof House
                            && ((House) prevHouse).getOrder() == ((House) inTile).getOrder()) {
                        // check house below
                        board.set(boardPoint, prevHouse);
                    } else {
                        board.set(boardPoint, new House(((House) inTile).getOrder()));
                    }
                } else if (inTile instanceof Road) {
                    board.set(boardPoint, ((Road) inTile).rotate(numRotations));
                } else if (inTile instanceof GoodsSource || inTile == null) {
                    board.set(boardPoint, inTile);
                } else {
                    throw new IllegalStateException("Unknown object: " + inTile.getClass());
                }
            }
        }
    }

    private static Point toTileCoords(int x, int y, int rotations) {
        switch (rotations) {
            case 0:
                return new Point(x, y);
            case 1:
                return new Point(Board.TILE_SIZE - 1 - y, x);
            case 2:
                return new Point(Board.TILE_SIZE - 1 - x, Board.TILE_SIZE - 1 - y);
            case 3:
                return new Point(y, Board.TILE_SIZE - 1 - x);
            default:
                throw new IllegalArgumentException("Should not happen");
        }
    }
}

El tiles.txtarchivo contiene la descripción de las 20 fichas. El contenido de cada mosaico se puede ver aquí:https://boardgamehelpers.com/FoodChainMagnate/MapTileKey.aspx(no incluye los mosaicos de expansión). Es un archivo de texto sin formato formado con líneas de 5 caracteres cada una. Cada 5 líneas describe un mosaico (5 x 5). A cada ficha se le asigna un carácter como se muestra en el enlace de referencia, siendo las primeras 5 líneas la ficha A, las siguientes 5 fichas B y así sucesivamente. Cada personaje (o grupo de personajes) representa un objeto. Por ejemplo, el mosaico E se describe como

/-/OO
|BOOO
/O8H/
OOHH|
OO/-/

(personajes /y \pueden representar uno de los dos posibles tipos de turnos, según el contexto)

Road.java

package com.lartkma.fcm.model.board;

import java.util.Arrays;
import java.util.StringJoiner;

public class Road {
    private boolean[] canMove;

    public Road(boolean canGoUp, boolean canGoRight, boolean canGoDown, boolean canGoLeft) {
        this.canMove = new boolean[] { canGoUp, canGoRight, canGoDown, canGoLeft };
    }

    public boolean canMove(Direction inDirection) {
        return this.canMove[inDirection.ordinal()];
    }

    public Road rotate(int amountRotations) {
        Road rotated = new Road(this.canMove[0], this.canMove[1], this.canMove[2], this.canMove[3]);
        if (amountRotations < 0) {
            // Java operator % returns a remainder, that is different from a mathematical
            // modulus
            // https://stackoverflow.com/questions/5385024/mod-in-java-produces-negative-numbers
            // https://docs.oracle.com/javase/specs/jls/se7/html/jls-15.html#jls-15.17.3
            amountRotations = amountRotations % rotated.canMove.length + rotated.canMove.length;
        } else {
            amountRotations = amountRotations % rotated.canMove.length;
        }
        boolean swapTemp;
        for (int k = 0; k < amountRotations; k++) {
            for (int i = 1; i < rotated.canMove.length; i++) { // it makes no sense for the first element
                swapTemp = rotated.canMove[0];
                rotated.canMove[0] = rotated.canMove[i];
                rotated.canMove[i] = swapTemp;
            }
        }
        return rotated;
    }

    @Override
    public boolean equals(Object obj) {
        if (obj instanceof Road) {
            return Arrays.equals(this.canMove, ((Road) obj).canMove);
        } else {
            return false;
        }
    }

    @Override
    public String toString() {
        StringJoiner name = new StringJoiner("-", "Road[", "]");
        for (Direction d : Direction.values()) {
            if (canMove(d)) {
                name.add(d.name());
            }
        }
        return name.toString();
    }
}

Directiones una enumeración con valores UP, RIGHT, DOWN, LEFT(en ese orden). Housees una clase de datos simple con una orderpropiedad, pero mutará otras propiedades durante el juego. GoodsSourcees una clase simple e inmutable que solo puede tener 3 instancias posibles.

BoardGeneratorTest.java (para una muestra de cómo se usa)

package com.lartkma.fcm.model.board;

import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.CoreMatchers.nullValue;
import static org.hamcrest.CoreMatchers.sameInstance;
import static org.junit.Assert.assertThat;
import static org.junit.jupiter.api.Assertions.assertAll;
import static org.junit.jupiter.api.Assertions.assertEquals;

import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;

import com.lartkma.fcm.model.board.Board;
import com.lartkma.fcm.model.board.GoodsSource;
import com.lartkma.fcm.model.board.House;
import com.lartkma.fcm.model.board.BoardGenerator;
import com.lartkma.fcm.model.board.Road;

public class BoardGeneratorTest {

    @Test
    @DisplayName("The board generator can receive specific tiles as parameters and should generate the correct amount of squares")
    public void testBoardGeneratorSizeFromExpression() {
        Board board = BoardGenerator.fromExpression("G1E2\nI3M4");
        assertAll("The board should be of 2 x 2 tiles (10 x 10 squares)",
                () -> assertEquals(10, board.getWidth(), "Board width"),
                () -> assertEquals(10, board.getHeight(), "Board height"));
    }

    @Test
    @DisplayName("The board generator can generate a random board and should generate the correct amount of squares")
    public void testBoardGeneratorSizeFromRandom() {
        Board board = BoardGenerator.fromRandom(3, 2);
        assertAll("The board should be of 3 x 2 tiles (15 x 10 squares)",
                () -> assertEquals(15, board.getWidth(), "Board width"),
                () -> assertEquals(10, board.getHeight(), "Board height"));
    }

    @Test
    @DisplayName("The board generator can create a 1-tile board with the correct contents")
    public void testBoardGeneratorContent() {
        Board board = BoardGenerator.fromExpression("E1");
        assertAll("The board should have the following contents",
                () -> assertThat("In 0, 0", board.get(0, 0), is(nullValue())),
                () -> assertThat("In 1, 0", board.get(1, 0), is(nullValue())),
                () -> assertThat("In 2, 0", board.get(2, 0), is(equalTo(new Road(false, true, true, false)))),
                () -> assertThat("In 3, 0", board.get(3, 0), is(equalTo(new Road(false, true, false, true)))),
                () -> assertThat("In 4, 0", board.get(4, 0), is(equalTo(new Road(true, false, false, true)))),
                () -> assertThat("In 0, 1", board.get(0, 1), is(nullValue())),
                () -> assertThat("In 1, 1", board.get(1, 1), is(nullValue())),
                () -> assertThat("In 2, 1", board.get(2, 1), is(equalTo(new House(8)))),
                () -> assertThat("In 3, 1", board.get(3, 1), is(sameInstance(board.get(2, 1)))),
                () -> assertThat("In 4, 1", board.get(4, 1), is(equalTo(new Road(true, false, true, false)))),
                () -> assertThat("In 0, 2", board.get(0, 2), is(equalTo(new Road(true, false, false, true)))),
                () -> assertThat("In 1, 2", board.get(1, 2), is(nullValue())),
                () -> assertThat("In 2, 2", board.get(2, 2), is(sameInstance(board.get(2, 1)))),
                () -> assertThat("In 3, 2", board.get(3, 2), is(sameInstance(board.get(2, 1)))),
                () -> assertThat("In 4, 2", board.get(4, 2), is(equalTo(new Road(false, true, true, false)))),
                () -> assertThat("In 0, 3", board.get(0, 3), is(equalTo(new Road(true, false, true, false)))),
                () -> assertThat("In 1, 3", board.get(1, 3), is(equalTo(GoodsSource.BEER))),
                () -> assertThat("In 2, 3", board.get(2, 3), is(nullValue())),
                () -> assertThat("In 3, 3", board.get(3, 3), is(nullValue())),
                () -> assertThat("In 4, 3", board.get(4, 3), is(nullValue())),
                () -> assertThat("In 0, 4", board.get(0, 4), is(equalTo(new Road(false, true, true, false)))),
                () -> assertThat("In 1, 4", board.get(1, 4), is(equalTo(new Road(false, true, false, true)))),
                () -> assertThat("In 2, 4", board.get(2, 4), is(equalTo(new Road(true, false, false, true)))),
                () -> assertThat("In 3, 4", board.get(3, 4), is(nullValue())),
                () -> assertThat("In 4, 4", board.get(4, 4), is(nullValue())));
    }

    @Test
    @DisplayName("The board generator can create a rotated 1-tile board with the correct contents")
    public void testBoardGeneratorContentRotated() {
        Board board = BoardGenerator.fromExpression("E2");
        assertAll("The board should have the following contents",
                () -> assertThat("In 0, 0", board.get(0, 0), is(equalTo(new Road(true, true, false, false)))),
                () -> assertThat("In 1, 0", board.get(1, 0), is(equalTo(new Road(false, true, false, true)))),
                () -> assertThat("In 2, 0", board.get(2, 0), is(equalTo(new Road(false, false, true, true)))),
                () -> assertThat("In 3, 0", board.get(3, 0), is(nullValue())),
                () -> assertThat("In 4, 0", board.get(4, 0), is(nullValue())),
                () -> assertThat("In 0, 1", board.get(0, 1), is(equalTo(new Road(true, false, true, false)))),
                () -> assertThat("In 1, 1", board.get(1, 1), is(equalTo(new House(8)))),
                () -> assertThat("In 2, 1", board.get(2, 1), is(sameInstance(board.get(1, 1)))),
                () -> assertThat("In 3, 1", board.get(3, 1), is(nullValue())),
                () -> assertThat("In 4, 1", board.get(4, 1), is(nullValue())),
                () -> assertThat("In 0, 2", board.get(0, 2), is(equalTo(new Road(false, false, true, true)))),
                () -> assertThat("In 1, 2", board.get(1, 2), is(sameInstance(board.get(1, 1)))),
                () -> assertThat("In 2, 2", board.get(2, 2), is(sameInstance(board.get(1, 1)))),
                () -> assertThat("In 3, 2", board.get(3, 2), is(nullValue())),
                () -> assertThat("In 4, 2", board.get(4, 2), is(equalTo(new Road(true, true, false, false)))),
                () -> assertThat("In 0, 3", board.get(0, 3), is(nullValue())),
                () -> assertThat("In 1, 3", board.get(1, 3), is(nullValue())),
                () -> assertThat("In 2, 3", board.get(2, 3), is(nullValue())),
                () -> assertThat("In 3, 3", board.get(3, 3), is(equalTo(GoodsSource.BEER))),
                () -> assertThat("In 4, 3", board.get(4, 3), is(equalTo(new Road(true, false, true, false)))),
                () -> assertThat("In 0, 4", board.get(0, 4), is(nullValue())),
                () -> assertThat("In 1, 4", board.get(1, 4), is(nullValue())),
                () -> assertThat("In 2, 4", board.get(2, 4), is(equalTo(new Road(true, true, false, false)))),
                () -> assertThat("In 3, 4", board.get(3, 4), is(equalTo(new Road(false, true, false, true)))),
                () -> assertThat("In 4, 4", board.get(4, 4), is(equalTo(new Road(false, false, true, true)))));
    }

}

Respuestas

2 MaartenBodewes Aug 20 2020 at 23:57
public static final int TILE_SIZE = 5;

Tal vez llame a esto TILE_EDGE_SIZEo algo similar, ya que una ficha no contiene 5 cuadrados.

public Object get(int x, int y) {

Elimina esto, solo úsalo get(Point p), no es más trabajo.

return OFF_LIMIT;

Este único valor mágico le impide usar la clase correcta para la matriz. Los valores mágicos son algo que quizás quieras evitar. Si realmente desea evitar una excepción, use Optional<Square>. Pero personalmente lanzaría una excepción y la usaría en Optionallugar de regresar null.

throw new IndexOutOfBoundsException("Point " + new Point(x, y) + " is out of the board");

Sabía que podía hacerlo, ahora haga que los métodos sean simétricos sobre cómo se manejan fuera de los límites.

public int getWidth() {
...
public int getHeight() {

Perfecto, no hay razón por la que la aplicación vuelva a aparecer en OFF_LIMITese momento.


Map<Character, Object[][]> tileList = getTileList();

Mezclar colecciones y matrices no es una buena idea; solo usa colecciones.

List<Character> randomTiles = new LinkedList<>(tileList.keySet());

Para ser precisos, usaría ArrayListaquí, no una lista vinculada.

Collections.shuffle(randomTiles);

Porque buscar índices aleatorios y luego moverlos en una lista enlazada no es una buena idea.

fillWithTile(board, tileList.get(randomTiles.get(i * heightTiles + j)), i * Board.TILE_SIZE, j * Board.TILE_SIZE, rnd.nextInt(4));

Demasiadas cosas están sucediendo en este método, divídalo. ¿Por qué sucede esto y qué está haciendo? ¿Por qué hay magia 4allí, 4 de qué?

if (2 * i + 1 < rows[rows.length - 1 - j].length()) {

Nuevamente, desde aquí podemos ver cómo se están haciendo las cosas, pero no qué ni por qué. El útil (aunque incompleto) JavaDoc ayuda un poco, pero se agradecería un comentario.

private static Map<Character, Object[][]> getTileList() {

Se está haciendo demasiado con este método, la cantidad de complejidad es asombrosa.

if (lineChar == 'O') { // ... endless else if's

Aquí, un interruptor haría maravillas, pero no olvides las break;declaraciones.

currentTileContent[i][Board.TILE_SIZE - lineCount] = null;

¿Qué pasa con la Object tileContent;declaración, luego configúrelo en el switch, y al final asígnelo a currentTileContent[i][Board.TILE_SIZE - lineCount]. Demasiado copiar / pegar si me preguntas.

 currentTileContent[i][Board.TILE_SIZE - lineCount] = new Road(false, true, false, true);

Ah, un camino que va false, true, false, true. Eso no es una truecarretera, use en enumlugar de parámetros booleanos. Está en Effective Java, que debería leer.

 EnumSet<Direction> possibleDirections = EnumSet.of(Direction.RIGHT, Direction.LEFT);

es mucho mejor, ¿no estás de acuerdo?

char previous = (i == 0 ? 'O' : currentLine.charAt(i - 1));
char next = (i == Board.TILE_SIZE - 1 ? 'O' : currentLine.charAt(i + 1));
if ((isHorizontalRoad(previous) || i == 0) && !isHorizontalRoad(next)) {
    currentTileContent[i][Board.TILE_SIZE - lineCount] = new Road(true, false, false,
            true);
} else if (!isHorizontalRoad(previous)
        && (isHorizontalRoad(next) || i == Board.TILE_SIZE - 1)) {
    currentTileContent[i][Board.TILE_SIZE - lineCount] = new Road(false, true, true,
            false);
} else {
    throw new IllegalStateException("Unknown combination on ( " + currentLine + ")");
}

Se devuelve un valor: a Road. ¿Método tal vez? Tan fácil de distinguir.

currentTileContent[i][Board.TILE_SIZE - lineCount] = GoodsSource.SODA;

Ah, ahora lo entiendo. A Road, GoodsSourcea Houseo no se espera nada. Aún así, cree una interfaz de marcador como TileContental menos, y téngala Roade GoodsSourceimpleméntela para que no la necesite Object, porque eso es demasiado feo.

throw new Error("tiles.txt not available", e);

No completamente legible es quizás una mejor excepción. RuntimeExceptiondebería preferirse sobre lo Errorque comúnmente no es recuperable en todo el sistema .

} else if ((prevHouse = board.get(boardPoint.add(0, -1))) instanceof House
        && ((House) prevHouse).getOrder() == ((House) inTile).getOrder()) {
    // check house below
    board.set(boardPoint, prevHouse);

Bien, entonces estás creando casas más grandes. Creo que puedo ser malo y crear una casa compuesta de partes separadas. Espero que sus casas sean cuadradas :) Pero realmente, nuevamente, brinde métodos.

Los valores de enumeración se pueden comparar directamente, sin necesidad de comparar orderpara la igualdad.

throw new IllegalArgumentException("Should not happen");

Estoy de acuerdo en que tal excepción no es aceptable.

    return this.canMove[inDirection.ordinal()];

O possibleDirections.contains(inDirection)(ver arriba?)