amercier/rectangular-mozaic

View on GitHub
src/Grid.php

Summary

Maintainability
A
2 hrs
Test Coverage
<?php

namespace RectangularMozaic;

use InvalidArgumentException;

/**
 * Grid.
 */
class Grid
{
    /**
     * Number of rows.
     *
     * @var int
     */
    public $rows;

    /**
     * Number of columns.
     *
     * @var int
     */
    public $columns;

    /**
     * Number of small tiles.
     *
     * @var int
     */
    public $small;

    /**
     * Number of tall tiles.
     *
     * @var int
     */
    public $tall;

    /**
     * Number of wide tiles.
     *
     * @var int
     */
    public $wide;

    /**
     * Cell values.
     *
     * @var Cell?[][]
     */
    protected $cells;

    /**
     * Create a new Grid instance and initialize all cells to `null`.
     *
     * @param int $rows Number of rows
     * @param int $columns Number of column
     */
    public function __construct(int $rows, int $columns)
    {
        Assert::assertPositiveInteger($rows, 'rows');
        Assert::assertPositiveInteger($columns, 'columns');

        $this->small = 0;
        $this->tall = 0;
        $this->wide = 0;

        $this->rows = $rows;
        $this->columns = $columns;
        $this->cells = array($rows);

        for ($i = 0; $i < $rows; $i += 1) {
            $this->cells[$i] = array_fill(0, $columns, null);
        }
    }

    /**
     * Get a string representation of the grid.
     *
     * @return string A string representation of the grid.
     */
    public function __toString()
    {
        return "{$this->rows}x{$this->columns} [{$this->small},{$this->tall},{$this->wide}]";
    }

    /**
     * Get the value at a given row and column.
     *
     * @param int $row Row index.
     * @param int $column Column index.
     * @return Cell The value at [$row, $column].
     * @throws InvalidArgumentException If row or column are either too low (negative) or too high.
     */
    public function get(int $row, int $column)
    {
        $this->assertIndexesAreValid($row, $column);
        return $this->cells[$row][$column];
    }

    /**
     * Determine whether a cell is empty.
     *
     * @param int $row Row index.
     * @param int $column Column index.
     * @return boolean `true` is the cell at [$row, $column] is empty, `false` otherwise.
     */
    public function isEmpty(int $row, int $column)
    {
        $this->assertIndexesAreValid($row, $column);
        return $this->get($row, $column) === null;
    }

    /**
     * Determine whether all required cells for a given tile are empty at [$row, $column].
     *
     * @param int $row Row index.
     * @param int $column Column index.
     * @param Tile $tile The tile.
     * @return void
     */
    public function isEmptyForTile($row, $column, $tile)
    {
        for ($x = 0; $x < $tile->width; $x += 1) {
            for ($y = 0; $y < $tile->height; $y += 1) {
                if (!$this->isEmpty($row + $y, $column + $x)) {
                    return false;
                }
            }
        }
        return true;
    }

    /**
     * Assert a given [$width, $height] rectangle fits within the grid at [$row, $column] position.
     *
     * @param int $row Row index.
     * @param int $column Column index.
     * @param int $height Height of the rectangle.
     * @param int $width Width of the rectangle.
     * @return void
     */
    protected function assertIndexesAreValid($row, $column, $height = 1, $width = 1)
    {
        Assert::assertIntegerBetween(0, $this->rows - $height, $row, 'row');
        Assert::assertIntegerBetween(0, $this->columns - $width, $column, 'column');
    }

    /**
     * Assert a given tile fits within the grid at [$row, $column] position.
     *
     * @param int $row Row index.
     * @param int $column Column index.
     * @param Tile $tile The tile.
     * @return void
     */
    protected function assertIndexesAreValidForTile($row, $column, Tile $tile)
    {
        return $this->assertIndexesAreValid($row, $column, $tile->height, $tile->width);
    }

    /**
     * Assert all required cells for a given tile are empty at [$row, $column].
     *
     * @param int $row Row index.
     * @param int $column Column index.
     * @param Tile $tile The tile.
     * @return void
     */
    protected function assertEmptyTileSlot($row, $column, Tile $tile)
    {
        // Check indexes
        $this->assertIndexesAreValidForTile($row, $column, $tile);

        // Check all cells are empty
        if (!$this->isEmptyForTile($row, $column, $tile)) {
            throw new InvalidArgumentException(
                "Cannot set tile {$tile} at [$row, $column]: not empty."
            );
        }
    }

    /**
     * Set a cell values at a given row and column for a given tile.
     *
     * @param int $row Row index.
     * @param int $column Column index.
     * @param Tile $tile Tile to set.
     * @return Grid This object to allow chaining.
     * @throws InvalidArgumentException If row or column are either too low (negative) or too high.
     * @throws InvalidArgumentException If the cell is not empty.
     * @return void
     */
    public function set(int $row, int $column, Tile $tile)
    {
        $this->assertEmptyTileSlot($row, $column, $tile);
        switch ($tile->getValue()) {
            case Tile::SMALL:
                $this->cells[$row][$column] = Cell::SMALL();
                $this->small += 1;
                break;
            case Tile::TALL:
                $this->cells[$row][$column] = Cell::TALL_TOP();
                $this->cells[$row + 1][$column] = Cell::TALL_BOTTOM();
                $this->tall += 1;
                break;
            case Tile::WIDE:
                $this->cells[$row][$column] = Cell::WIDE_LEFT();
                $this->cells[$row][$column + 1] = Cell::WIDE_RIGHT();
                $this->wide += 1;
                break;
        }
    }

    /**
     * Reset the grid to an empty state (ie. remove all cell values).
     *
     * @return void
     */
    public function empty()
    {
        if ($this->small > 0 || $this->tall > 0 || $this->wide > 0) {
            for ($i = 0; $i < $this->rows; $i += 1) {
                for ($j = 0; $j < $this->columns; $j += 1) {
                    $this->cells[$i][$j] = null;
                }
            }
            $this->small = 0;
            $this->tall = 0;
            $this->wide = 0;
        }
    }

    /**
     * Get a copy of the cells array.
     *
     * @param boolean $flatten Whether to flatten the two-dimensional array to one, or not.
     * @param function? $cellToValue A function that takes an instance of Cell and return any value,
     * or `false` to omit it from the flattened array. If `null` is given, returns the original value.
     * @return mixed[] A one-dimensional of values, when `$flatten` is `true`, or
     * @return mixed[][] A two-dimensional of values, when `$flatten` is `false`.
     */
    public function getData(bool $flatten = false, callable $cellToValue = null)
    {
        $copy = array();
        for ($row = 0; $row < $this->rows; $row++) {
            if (!$flatten) {
                $copy[$row] = array();
            }
            foreach ($this->cells[$row] as $cell) {
                $value = $cellToValue ? $cellToValue($cell) : $cell;
                if ($flatten && $value === false) {
                    // skip
                } elseif ($flatten) {
                    $copy[] = $value;
                } else {
                    $copy[$row][] = $value;
                }
            }
        }
        return $copy;
    }

    /**
     * Get a copy of the grid cells.
     *
     * @param boolean $flatten Whether to flatten the two-dimensional array to one, or not.
     * @param boolean $raw Whether to return Cell instances or cell values (int).
     * @return int[] A one-dimensional of Cell values, when `$raw` is `false` and `$flatten` is `true`, or
     * @return int[][] A two-dimensional of Cell values, when `$raw` is `false` and `$flatten` is `false`.
     * @return Cell[] A one-dimensional of Cell instances, when `$raw` is `true` and `$flatten` is `true`, or
     * @return Cell[][] A two-dimensional of Cell instances, when `$raw` is `true` and `$flatten` is `false`.
     */
    public function getCells(bool $flatten = false, bool $raw = false)
    {
        return $this->getData($flatten, $raw ? null : function ($cell) {
            return Cell::toValue($cell);
        });
    }

    /**
     * Get a copy of the grid cells as an array of tiles.
     *
     * @param boolean $flatten Whether to flatten the two-dimensional array to one, or not.
     * @param boolean $raw Whether to return Tile instances or tile values (int).
     * @return int[] A one-dimensional of Tile values, when `$raw` is `false` and `$flatten` is `true`, or
     * @return int[][] A two-dimensional of Tile values, when `$raw` is `false` and `$flatten` is `false`.
     * @return Tile[] A one-dimensional of Tile instances, when `$raw` is `true` and `$flatten` is `true`, or
     * @return Tile[][] A two-dimensional of Tile instances, when `$raw` is `true` and `$flatten` is `false`.
     */
    public function getTiles(bool $flatten = false, bool $raw = false)
    {
        return $this->getData(
            $flatten,
            $raw
                ? function ($cell) {
                    return Tile::fromCell($cell);
                } : function ($cell) {
                    return Cell::toValue(Tile::fromCell($cell));
                }
        );
    }
}