class Grid implements \JsonSerializable, \ArrayAccess
{
public $rows, $columns, $cells, $width, $height;
/**
* Create a new Grid from a block of Scalar
*
* @param Scalar|string|array|Set $from
* @param string|int $y_delimiter String delimiter or number of characters per cell
* @param string|int $x_delimiter String delimiter for the rows
*/
public function __construct($from = [], $x_delimiter = 1, $y_delimiter = "\n")
{
$from = is_string($from) ? scalar($from) : $from;
if ($from instanceof Scalar) {
$this->rows = $from->split($y_delimiter)->map(function($row) use ($x_delimiter) {
return is_int($x_delimiter) ? $row->chunk($x_delimiter) : $row->split($x_delimiter);
});
} else {
$this->rows = set($from);
}
}
/**
* When debugging, only output the value
*
* @return mixed
*/
public function jsonSerialize(): mixed
{
return $this->fillVoid(" ")->rows->callEach()->join("")->values();
}
/**
* Display the grid as a string
*
* @return string
*/
public function __toString()
{
return $this->fillVoid(" ")->rows->callEach()->join("")->join("\n");
}
/**
* Keep value but create a new instance for each row
*/
public function __clone()
{
$this->rows = $this->rows->mapAssoc(fn ($y, $row) => [$y => set((array) $row)]);
}
/**
* Dump the result of a model with all its fields loaded
*
* @return void
*/
public function json()
{
\Syltaen\Log::json($this);
}
// =============================================================================
// > ARRAY ACCESS
// =============================================================================
/** Impementation of ArrayAccess::offsetExists */
public function offsetExists(mixed $offset): bool
{
return isset($this->rows[$offset]);
}
/** Impementation of ArrayAccess::offsetGet */
public function offsetGet(mixed $offset): mixed
{
return $this->rows[$offset];
}
/** Impementation of ArrayAccess::offsetSet */
public function offsetSet(mixed $offset, mixed $value): void
{
$this->rows[$offset] = $value;
}
/** Impementation of ArrayAccess::offsetUnset */
public function offsetUnset(mixed $offset): void
{
unset($this->rows[$offset]);
}
// =============================================================================
// > DYNAMIC PROPERTIES
// =============================================================================
/**
* Get the rows of the grid
*
* @return Set
*/
public function rows()
{
return $this->rows;
}
/**
* Get the columns of the grid by transposing the rows
*
* @return Set
*/
public function columns()
{
return $this->columns ??= $this->rows()->callEach()->keys()->merge()->unique()->sort()
->mapAssoc(fn ($i, $x) => [$x => $this->rows()->column($x)->filter(fn ($v) => $v !== null)]);
}
/**
* List of all the cells of the grid
*
* @param Set $xys List of cells to get
* @return Set
*/
public function cells($xys = false)
{
// Get specific cells
if (!empty($xys)) {
$cells = set();
foreach ($xys as $xy) $cells[implode(";", $xy)] = $this->cell($xy);
return $cells;
}
// Cache all cells values
if (empty($this->cells)) {
$this->cells = set();
foreach ($this->rows as $y=>$row) {
foreach ($row as $x=>$cell) {
$this->cells["$x;$y"] = $cell;
}
}
}
return $this->cells;
}
/**
* List of all defined cells keys
*
* @return Set of [x, y]
*/
public function keys()
{
$keys = [];
foreach ($this->rows as $y=>$row) {
foreach ($row as $x=>$cell) $keys[] = [$x, $y];
}
return set($keys);
}
/**
* Width of the grid
*
* @return int
*/
public function width()
{
return $this->width ??= $this->columns()->keys()->max() - $this->columns()->keys()->min() + 1;
}
/**
* Height of the grid
*
* @return int
*/
public function height()
{
return $this->height ??= $this->rows()->keys()->max() - $this->rows()->keys()->min() + 1;
}
/**
* Update the grid with a new set of rows
* and clear cached data
*
* @return self
*/
public function update($rows)
{
$this->rows = $rows;
$this->clearCache();
return $this;
}
/**
* Clear all cached values
*
* @return self
*/
public function clearCache()
{
unset($this->width, $this->height, $this->columns, $this->cells);
return $this;
}
// =============================================================================
// > PROPERTIES SHORTCUTS
// =============================================================================
/**
* Get a specific row
*
* @param int $y
* @return Set
*/
public function row($y)
{
return $this->rows[$y];
}
/**
* Get a specific column
*
* @param int $y
* @return Set
*/
public function column($x)
{
return $this->columns()[$x];
}
/**
* Get a specific cell
*
* @param array|GridPointer $xy [x, y]
* @return mixed
*/
public function cell($xy)
{
return $this->rows[$xy[1]][$xy[0]] ?? null;
}
/**
* Get one or several cells values
*
* @param array|GridPionter xy or array of xy
* @return mixed
*/
public function get($xy)
{
return $this->cell($xy);
}
/**
* Check that a cell exists
*
* @param array|GridPointer $xy
* @return boolean
*/
public function has($xy)
{
return isset($this->rows[$xy[1]][$xy[0]]);
}
/**
* Get the neighbors values of the given cell
*
* @param array|GridPointer $xy The given cell
* @param boolean $diagonals
* @return Set
*/
public function getNeighbors($xy, $diagonals = false)
{
return $this->cells(xy($xy)->getNeighbors($diagonals));
}
/**
* Wrap a xy position that would be outside the grid
*
* @param array $xy [x, y]
* @param string $direction Wrap in a specific direction
* @return array [x, y]
*/
public function wrap($xy, $direction)
{
switch ($direction) {
case "up":
$column = $this->columns()[$xy[0]];
$xy[1] = $xy[1] < $column->keys()->min() ? $column->keys()->max() : $xy[1];
break;
case "right":
$row = $this[$xy[1]];
$xy[0] = $xy[0] > $row->keys()->max() ? $row->keys()->min() : $xy[0];
break;
case "down":
$column = $this->columns()[$xy[0]];
$xy[1] = $xy[1] > $column->keys()->max() ? $column->keys()->min() : $xy[1];
break;
case "left":
$row = $this[$xy[1]];
$xy[0] = $xy[0] < $row->keys()->min() ? $row->keys()->max() : $xy[0];
break;
default:
die("Direction not supported : {$direction}");
}
return $xy;
}
// =============================================================================
// > PARTS
// =============================================================================
/**
* Slice of a row/column from a specific point
*
* @param GridPointer $xy
* @param string $direction up|right|down|left
* @return Set
*/
public function slice($xy, $direction)
{
switch ($direction) {
case "up":
$col = $this->columns()[$xy[0]];
return $col->slice(0, $col->keys()->search($xy[1]))->reverse();
case "right":
$row = $this->rows[$xy[1]];
return $row->slice($row->keys()->search($xy[0]) + 1);
case "down":
$col = $this->columns()[$xy[0]];
return $col->slice($col->keys()->search($xy[1]) + 1);
case "left":
$row = $this->rows[$xy[1]];
return $row->slice(0, $row->keys()->search($xy[0]))->reverse();
default:
die("Direction not supported : $direction");
}
}
/**
* Get a subgrid of this grid
*
* @param array $from XY top left
* @param array $to XY bottom right
* @return Grid
*/
public function sub($from, $to)
{
$rows = $this->rows->slice($from[1], $to[1]);
$rows = $rows->map(fn ($row) => $row->slice($from[0], $to[0]));
return new Grid($rows);
}
// =============================================================================
// > TOOLS
// =============================================================================
/**
* Map the cells of the grid
*
* @param callable $callback
* @return Grid
*/
public function map($callback)
{
return new static($this->rows->map(function($row, $y) use ($callback) {
return $row->map(function ($cell, $x) use ($callback, $y) {
return $callback($cell, [$x, $y]);
}, true);
}, true));
}
/**
* Return a CallableSet that allows to use a specific method on each element of this set.
*
* @return CallableMap
*/
public function callEach($deepness = 1)
{
$cc = new CallableMap($this);
if ($deepness == 1) {
return $cc;
}
return $cc->callEach($deepness - 1)->callEach();
}
/**
* Filter the cells of the grid
*
* @param callable $callback
* @return Grid
*/
public function filter($callback = "\Scalar::isNotEmpty")
{
return new static($this->rows->map(function($row, $y) use ($callback) {
return $row->filter(function ($cell, $x) use ($callback, $y) {
return $callback($cell, [$x, $y]);
});
}, true));
}
/**
* Keep only the specified values
*
* @param array $array
* @return Grid
*/
public function keep($array)
{
return $this->filter(fn ($cell) => in_array($cell, (array) $array));
}
/**
* Remove the specified values
*
* @param array $array
* @return Grid
*/
public function remove($array)
{
return $this->filter(fn ($cell) => !in_array($cell, (array) $array));
}
/**
* Filter the cells of the grid
*
* @param callable $callback
* @return mixed
*/
public function reduce($callback, $init)
{
return $this->rows->reduce(function($carry, $row, $y) use ($callback) {
return $row->reduce(function($carry, $cell, $x) use ($callback, $y) {
return $callback($carry, $cell, [$x, $y]);
}, $carry, true);
}, $init, true);
}
/**
* Repeat the grid in the x axis
*
* @return Grid
*/
public function repeatX($times = 2)
{
return new static($this->rows->callEach()->repeat($times));
}
/**
* Repeat the grid in the y axis
*
* @return Grid
*/
public function repeatY($times = 2)
{
return new static($this->rows->repeat($times));
}
/**
* Create all the missing cells with a specific value
*
* @return self
*/
public function fillVoid($content = " ")
{
$xs = $this->rows()->callEach()->keys()->merge();
$ys = $this->rows()->keys();
for ($x = $xs->min(); $x <= $xs->max(); $x++) {
for ($y = $ys->min(); $y <= $ys->max(); $y++) {
if (!isset($this->rows[$y][$x])) {
$this->set([$x, $y], $content);
}
$this->rows[$y]->sortKeys();
}
$this->rows->sortKeys();
}
return $this;
}
/**
* Draw a line between two points in the same row or column
*
* @param GridPointer $from
* @param array $to
* @return self
*/
public function drawLine($from, $to, $content = "#")
{
foreach (xy($from)->pathTo($to) as $cell) {
$this->set($cell, $content);
}
return $this;
}
/**
* Draw the content of another grid inside this one
*
* @param array|GridPointer $xy
* @param Grid $grid
* @return self
*/
public function insert($xy, $grid)
{
foreach ($grid->rows as $y=>$row) {
foreach ($row as $x=>$cell) {
$this->set([$x + $xy[0], $y + $xy[1]], $cell);
}
}
return $this;
}
/**
* Draw a circle in the grid
*
* @param GridPointer $center
* @param int $radius
* @return self
*/
public function drawCircle($center, $radius, $content = "#")
{
foreach ((new GridCircle($center, $radius))->getPoints() as $xy) {
$this->set($xy, $content);
}
return $this;
}
/**
* Return the index of the first match
*
* @param mixed $search
* @return array
*/
public function search($search)
{
foreach ($this->rows as $y => $row) {
if (($x = $row->search($search)) !== false) {
return [$x, $y];
}
}
return false;
}
/**
* Return the index of all the matches
*
* @param mixed $search
* @return array
*/
public function searchAll($search)
{
$indexes = [];
foreach ($this->rows as $y => $row) {
foreach ($row as $x=>$value) {
if ($value == $search) {
$indexes[] = [$x, $y];
}
}
}
return $indexes;
}
/**
* Set the value of a cell
*
* @param array|GridPointer $xy
* @param mixed $value
* @return self
*/
public function set($xy, $value)
{
if (!isset($this->rows[$xy[1]])) {
$this->rows[$xy[1]] = set([]);
}
$this->rows[$xy[1]][$xy[0]] = $value;
// $this->clearCache();
return $this;
}
/**
* Set all values of a specific row
*
* @param $int $y
* @param Set|array $row
* @return self
*/
public function setRow($y, $row)
{
$this->rows[$y] = set($row);
return $this;
}
/**
* Set all values of a specific column
*
* @param $int $x
* @param Set|array $row
* @return self
*/
public function setColumn($x, $column)
{
foreach ($column as $y => $value) {
$this->set([$x, $y], $value);
}
return $this;
}
/**
* Delete a cell
*
* @param GridPointer $xy
* @return self
*/
public function unset($xy)
{
if (isset($this->rows[$xy[1]][$xy[0]])) {
unset($this->rows[$xy[1]][$xy[0]]);
}
$this->clearCache();
return $this;
}
/**
* Offset the whole grid
*
* @param GridPonter $xy
* @return self
*/
public function move($xy)
{
$this->rows = $this->rows->mapAssoc(function ($y, $row) use ($xy) {
return [$y + $xy[1] => $row->mapAssoc(fn ($x, $cell) => [$x + $xy[0] => $cell])];
});
return $this;
}
/**
* Reverse on the X axis
*
* @return self
*/
public function reverseX()
{
$this->rows = $this->rows->map(fn ($r) => $r->reverse());
return $this;
}
/**
* Reverse on the Y axis
*
* @return self
*/
public function reverseY()
{
$this->rows = $this->rows->reverse();
return $this;
}
/**
* Rotate the grid X times by 90° clockwise
*
* @param integer $times
* @return self
*/
public function rotate($times = 1)
{
for ($i = 0; $i < $times; $i++) {
$this->rows = $this->columns();
$this->reverseX();
$this->clearCache();
}
return $this;
}
/**
* Check if the grid has collisions with another
*
* @return boolean|int The number of collisons
*/
public function hasCollision($grid)
{
return $this->cells()->keys()->keep($grid->cells()->keys())->count();
}
/**
* Create a graph of the grid, with each item as a node
*
* @param callback $edge_selector
* @return Graph
*/
public function getGraph($edge_selector = false)
{
// Default selection : direct neighbors that exist in the grid
if (!$edge_selector) {
$edge_selector = function ($cell, $xy, $grid) {
return $xy->getNeighbors()->filter(fn ($xy) => $grid->get($xy))->mapAssoc(function ($i, $xy) {
return [(string) $xy => 1];
});
};
}
$nodes = $this->map(function ($cell, $xy) use ($edge_selector) {
return $edge_selector($cell, $xy, $this);
})->cells()->filter();
return new Graph($nodes);
}
}