로그인
KO
games/open-lemmings / src / Sim / Terrain.php · commit 04e2e2a
<?phpnamespace App\Sim;/** * The destructible ground of a level: one byte per pixel (EMPTY, EARTH, STEEL) in a string. * Pure PHP (no SDL): the simulation and its tests run without a window. Every change is also logged in * $changes as row spans, which the view (TerrainView) replays on its picture. */final class Terrain{    public const EMPTY = 0;    public const EARTH = 1;    public const STEEL = 2;    public const BRICK = 3;    public string $mask;    /** @var array<int, array{0:int,1:int,2:int,3:int}> [y, x0, x1, material] spans changed since the view read them */    public array $changes = [];    public function __construct(public readonly int $width, public readonly int $height)    {        $this->mask = str_repeat("\0", $width * $height);    }    /** Solid at this pixel? The left / right sides of the level are walls, above and below are open. */    public function solid(int $x, int $y): bool    {        if ($x < 0 || $x >= $this->width) {            return true;        }        if ($y < 0 || $y >= $this->height) {            return false;        }        return $this->mask[$y * $this->width + $x] !== "\0";    }    public function at(int $x, int $y): int    {        if ($x < 0 || $x >= $this->width) {            return self::STEEL;        }        if ($y < 0 || $y >= $this->height) {            return self::EMPTY;        }        return ord($this->mask[$y * $this->width + $x]);    }    /** Fills a row span with a material (level building: no change log). */    public function fillSpan(int $y, int $x0, int $x1, int $material): void    {        if ($y < 0 || $y >= $this->height) {            return;        }        $x0 = max(0, $x0);        $x1 = min($this->width - 1, $x1);        if ($x1 < $x0) {            return;        }        $this->mask = substr_replace($this->mask, str_repeat(chr($material), $x1 - $x0 + 1), $y * $this->width + $x0, $x1 - $x0 + 1);    }    public function fillRect(int $x, int $y, int $w, int $h, int $material): void    {        for ($row = $y; $row < $y + $h; $row++) {            $this->fillSpan($row, $x, $x + $w - 1, $material);        }    }    /**     * Digs a row span: earth and bricks go, steel stays. Returns true when steel was in the way.     */    public function digSpan(int $y, int $x0, int $x1): bool    {        if ($y < 0 || $y >= $this->height) {            return false;        }        $hitSteel = $x0 < 0 || $x1 >= $this->width;        $x0 = max(0, $x0);        $x1 = min($this->width - 1, $x1);        if ($x1 < $x0) {            return $hitSteel;        }        $offset = $y * $this->width + $x0;        $length = $x1 - $x0 + 1;        $part = substr($this->mask, $offset, $length);        if (trim($part, "\0") === '') {            return $hitSteel;        }        if (!str_contains($part, "\2")) {            $this->mask = substr_replace($this->mask, str_repeat("\0", $length), $offset, $length);            $this->changes[] = [$y, $x0, $x1, self::EMPTY];            return $hitSteel;        }        // steel in the span: dig around it, run by run        $start = null;        for ($i = 0; $i <= $length; $i++) {            $c = $i < $length ? $part[$i] : "\2";            if ($c !== "\2") {                $start ??= $i;                continue;            }            if ($start !== null) {                $n = $i - $start;                $this->mask = substr_replace($this->mask, str_repeat("\0", $n), $offset + $start, $n);                $this->changes[] = [$y, $x0 + $start, $x0 + $i - 1, self::EMPTY];                $start = null;            }        }        return true;    }    /** Digs a rectangle; true when steel was in the way. */    public function digRect(int $x, int $y, int $w, int $h): bool    {        $steel = false;        for ($row = $y; $row < $y + $h; $row++) {            $steel = $this->digSpan($row, $x, $x + $w - 1) || $steel;        }        return $steel;    }    public function digCircle(int $cx, int $cy, int $radius): void    {        for ($dy = -$radius; $dy <= $radius; $dy++) {            $dx = (int)floor(sqrt($radius * $radius - $dy * $dy));            $this->digSpan($cy + $dy, $cx - $dx, $cx + $dx);        }    }    /** Lays bricks on the empty pixels of a row span (what is solid stays as it is). */    public function buildSpan(int $y, int $x0, int $x1): void    {        if ($y < 0 || $y >= $this->height) {            return;        }        $x0 = max(0, $x0);        $x1 = min($this->width - 1, $x1);        for ($x = $x0; $x <= $x1; $x++) {            $i = $y * $this->width + $x;            if ($this->mask[$i] === "\0") {                $this->mask[$i] = "\3";                $this->changes[] = [$y, $x, $x, self::BRICK];            }        }    }    /** Any solid pixel in this rectangle? */    public function anySolid(int $x, int $y, int $w, int $h): bool    {        $x0 = max(0, $x);        $x1 = min($this->width - 1, $x + $w - 1);        if ($x1 < $x0) {            return false;        }        for ($row = max(0, $y); $row < min($this->height, $y + $h); $row++) {            if (trim(substr($this->mask, $row * $this->width + $x0, $x1 - $x0 + 1), "\0") !== '') {                return true;            }        }        return false;    }    /** Any steel pixel in this rectangle (the level sides count as steel)? */    public function anySteel(int $x, int $y, int $w, int $h): bool    {        if ($x < 0 || $x + $w > $this->width) {            return true;        }        for ($row = max(0, $y); $row < min($this->height, $y + $h); $row++) {            if (str_contains(substr($this->mask, $row * $this->width + $x, $w), "\2")) {                return true;            }        }        return false;    }    /** The level as text, one character per $cell x $cell block (debugging, level design). */    public function ascii(int $cell = 8): string    {        $out = '';        for ($y = 0; $y < $this->height; $y += $cell) {            for ($x = 0; $x < $this->width; $x += $cell) {                $m = $this->at($x + intdiv($cell, 2), $y + intdiv($cell, 2));                $out .= $m === self::EMPTY ? '.' : ($m === self::STEEL ? 'S' : '#');            }            $out .= "\n";        }        return $out;    }}