299 lines
8.6 KiB
PHP
299 lines
8.6 KiB
PHP
|
<?php
|
||
|
|
||
|
class Level
|
||
|
{
|
||
|
const MIN_STROKES = 5;
|
||
|
const MAX_STROKES = 25;
|
||
|
|
||
|
const BLOOM_MAX = 1, BLOOM_MIN = 0;
|
||
|
|
||
|
// This needs its own definitions of these so we can use some stupid array tricks
|
||
|
const DIR_NONE = -1, DIR_UP = 0, DIR_RIGHT = 1, DIR_DOWN = 2, DIR_LEFT = 3;
|
||
|
|
||
|
private $coords;
|
||
|
private $width;
|
||
|
private $height;
|
||
|
private $dungeon;
|
||
|
private $changes;
|
||
|
|
||
|
public function __construct(Dungeon $dungeon, $createRandom = true)
|
||
|
{
|
||
|
$this->coords = array();
|
||
|
$this->width = 0;
|
||
|
$this->height = 0;
|
||
|
|
||
|
if ($createRandom)
|
||
|
$this->createRandom();
|
||
|
|
||
|
$this->dungeon = $dungeon;
|
||
|
}
|
||
|
|
||
|
public function whatsAt(Coord $place)
|
||
|
{
|
||
|
return (isset($this->coords[(string) $place]))? $this->coords[(string) $place] : null;
|
||
|
}
|
||
|
|
||
|
public function insertEntity(Coord $place, Entity $entity)
|
||
|
{
|
||
|
if (isset($this->coords[(string) $place])
|
||
|
&& !($this->coords[(string) $place] instanceof EmptyFloor))
|
||
|
throw new PlaceOccupiedException('Cannot insert, place already taken.');
|
||
|
|
||
|
$this->coords[(string) $place] = $entity;
|
||
|
|
||
|
$this->width = max($this->width, $place->x);
|
||
|
$this->height = max($this->height, $place->y);
|
||
|
}
|
||
|
|
||
|
public function removeEntity(Coord $place)
|
||
|
{
|
||
|
if (!isset($this->coords[(string) $place])
|
||
|
|| $this->coords[(string) $place] instanceof EmptyFloor)
|
||
|
throw new PlaceException('Cannot remove, nothing there.');
|
||
|
|
||
|
unset($this->coords[(string) $place]);
|
||
|
|
||
|
$floor = new EmptyFloor();
|
||
|
$floor->setPlace(new Place($this, $place));
|
||
|
}
|
||
|
|
||
|
public function printLevel(Display $disp)
|
||
|
{
|
||
|
foreach ($this->coords as $coord => $content)
|
||
|
{
|
||
|
// +1 to X and Y since in the terminal they start from 1,1
|
||
|
$expCoord = Coord::createFromString($coord);
|
||
|
$disp->writeTo($expCoord->x + 1, $expCoord->y + 1, $this->whatsAt($expCoord));
|
||
|
}
|
||
|
}
|
||
|
|
||
|
public function getWidth()
|
||
|
{
|
||
|
return $this->width;
|
||
|
}
|
||
|
|
||
|
public function getHeight()
|
||
|
{
|
||
|
return $this->height;
|
||
|
}
|
||
|
|
||
|
public function getDungeon()
|
||
|
{
|
||
|
return $dungeon;
|
||
|
}
|
||
|
|
||
|
public function getEmptyPlace()
|
||
|
{
|
||
|
$keys = array_keys($this->coords);
|
||
|
shuffle($keys);
|
||
|
do
|
||
|
{
|
||
|
$coord = array_pop($keys);
|
||
|
|
||
|
if ($coord === null)
|
||
|
throw new PlaceException('No empty place found for entity.');
|
||
|
|
||
|
$content = $this->coords[$coord];
|
||
|
} while (!($content instanceof EmptyFloor));
|
||
|
return Coord::createFromString($coord);
|
||
|
}
|
||
|
|
||
|
|
||
|
|
||
|
|
||
|
|
||
|
|
||
|
|
||
|
private function createRandom()
|
||
|
{
|
||
|
$strokes = array();
|
||
|
$strokeAmount = rand(static::MIN_STROKES, static::MAX_STROKES);
|
||
|
|
||
|
$ends = array();
|
||
|
|
||
|
for ($i = 0; $i < $strokeAmount; ++$i)
|
||
|
{
|
||
|
$start = null; $dir = 0;
|
||
|
if (empty($ends)) // If empty, start somewhere
|
||
|
{
|
||
|
$start = new Coord(rand(2, TERM_X - 3), rand(2, TERM_Y - 3));
|
||
|
$freeDir = rand(self::DIR_UP, self::DIR_LEFT);
|
||
|
$ends[] = $start;
|
||
|
$strokes[(string) $start] = true;
|
||
|
}
|
||
|
else // Else find a previous intersection and start there
|
||
|
{
|
||
|
$freeDir = self::DIR_NONE;
|
||
|
shuffle($ends);
|
||
|
$endAmnt = count($ends);
|
||
|
for ($r = 0; $r < $endAmnt && $freeDir == self::DIR_NONE; ++$r)
|
||
|
{
|
||
|
$start = $ends[$r];
|
||
|
$freeDir = $this->getFreeDir($start, $strokes);
|
||
|
}
|
||
|
|
||
|
// No free spots could be found, end algorithm
|
||
|
if ($freeDir == self::DIR_NONE)
|
||
|
break;
|
||
|
}
|
||
|
|
||
|
// Now create strokes from that place onwards
|
||
|
$strokeLines = rand(1, ceil($strokeAmount / 2));
|
||
|
|
||
|
for ($r = 0; $r < $strokeLines; ++$r)
|
||
|
{
|
||
|
$newStart = $this->strokeLine($start, $strokes, $ends);
|
||
|
if ($newStart === null) // Couldn't find a free direction
|
||
|
break;
|
||
|
$start = $newStart;
|
||
|
$ends[] = $start;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Widen created strokes
|
||
|
$strokes = $this->bloom($strokes);
|
||
|
|
||
|
// Build walls
|
||
|
$this->buildWalls($strokes);
|
||
|
}
|
||
|
|
||
|
private function getFreeDir(Coord $coord, array $reserved)
|
||
|
{
|
||
|
$dirs = range(self::DIR_UP, self::DIR_LEFT);
|
||
|
shuffle($dirs);
|
||
|
foreach ($dirs as $dir)
|
||
|
{
|
||
|
if ($dir == self::DIR_UP && $coord->y > 1 && !isset($reserved[(string) (new Coord($coord->x, $coord->y - 1))]))
|
||
|
return $dir;
|
||
|
|
||
|
elseif ($dir == self::DIR_DOWN && $coord->y < TERM_Y - 2 && !isset($reserved[(string) (new Coord($coord->x, $coord->y + 1))]))
|
||
|
return $dir;
|
||
|
|
||
|
elseif ($dir == self::DIR_RIGHT && $coord->x < TERM_X - 2 && !isset($reserved[(string) (new Coord($coord->x + 1, $coord->y))]))
|
||
|
return $dir;
|
||
|
|
||
|
elseif ($dir == self::DIR_LEFT && $coord->x > 1 && !isset($reserved[(string) (new Coord($coord->x - 1, $coord->y))]))
|
||
|
return $dir;
|
||
|
}
|
||
|
|
||
|
return self::DIR_NONE;
|
||
|
}
|
||
|
|
||
|
private function strokeLine(Coord $start, array &$strokes, array &$ends)
|
||
|
{
|
||
|
// Get direction
|
||
|
$dir = $this->getFreeDir($start, $strokes);
|
||
|
|
||
|
if ($dir == self::DIR_NONE)
|
||
|
return null;
|
||
|
|
||
|
switch ($dir)
|
||
|
{
|
||
|
case self::DIR_UP:
|
||
|
$maxLen = ceil(($start->y - 1) / 3);
|
||
|
break;
|
||
|
case self::DIR_DOWN:
|
||
|
$maxLen = ceil((TERM_Y - 2 - $start->y) / 3);
|
||
|
break;
|
||
|
case self::DIR_RIGHT:
|
||
|
$maxLen = ceil((TERM_X - 2 - $start->x) / 3);
|
||
|
break;
|
||
|
case self::DIR_LEFT:
|
||
|
$maxLen = ceil(($start->x - 1) / 3);
|
||
|
}
|
||
|
|
||
|
$length = rand(ceil($maxLen / 2), $maxLen);
|
||
|
|
||
|
$nextCoord = clone $start;
|
||
|
// Now add line to $strokes
|
||
|
for ($i = 1; $i <= $length; ++$i)
|
||
|
{
|
||
|
switch ($dir)
|
||
|
{
|
||
|
case self::DIR_UP:
|
||
|
$nextCoord = new Coord($nextCoord->x, $nextCoord->y - 1);
|
||
|
break;
|
||
|
case self::DIR_DOWN:
|
||
|
$nextCoord = new Coord($nextCoord->x, $nextCoord->y + 1);
|
||
|
break;
|
||
|
case self::DIR_RIGHT:
|
||
|
$nextCoord = new Coord($nextCoord->x + 1, $nextCoord->y);
|
||
|
break;
|
||
|
case self::DIR_LEFT:
|
||
|
$nextCoord = new Coord($nextCoord->x - 1, $nextCoord->y);
|
||
|
}
|
||
|
$strokes[(string) $nextCoord] = true;
|
||
|
$ends[] = $nextCoord;
|
||
|
}
|
||
|
return $nextCoord;
|
||
|
}
|
||
|
|
||
|
private function bloom(array $reserved)
|
||
|
{
|
||
|
$bloom = rand(self::BLOOM_MIN, self::BLOOM_MAX);
|
||
|
$added = array();
|
||
|
foreach ($reserved as $key => $coord)
|
||
|
{
|
||
|
$coord = Coord::createFromString($key);
|
||
|
for ($x = ($coord->x - $bloom); $x <= ($coord->x + $bloom); ++$x)
|
||
|
{
|
||
|
for ($y = ($coord->y - $bloom); $y <= ($coord->y + $bloom); ++$y)
|
||
|
{
|
||
|
if ($x > 0 && $x < TERM_X - 1 && $y > 0 && $y < TERM_Y - 1)
|
||
|
{
|
||
|
$added[(string) (new Coord($x, $y))] = true;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Calculate next bloom size
|
||
|
$action = rand(1, 6);
|
||
|
switch ($action)
|
||
|
{
|
||
|
case 1:
|
||
|
// Completely new bloom 1/6 of the time
|
||
|
$bloom = rand(self::BLOOM_MIN, self::BLOOM_MAX);
|
||
|
break;
|
||
|
case 2:
|
||
|
// Slightly new bloom 1/6 of the time
|
||
|
$vals = array(max($bloom - 1, self::BLOOM_MIN), min($bloom + 1, self::BLOOM_MAX));
|
||
|
$bloom = $vals[array_rand($vals)];
|
||
|
break;
|
||
|
// Else no change to bloom 2/3 of the time
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return array_merge($reserved, $added);
|
||
|
}
|
||
|
|
||
|
private function buildWalls(array $reserved)
|
||
|
{
|
||
|
foreach ($reserved as $coord => $dummy)
|
||
|
{
|
||
|
$coord = Coord::createFromString($coord);
|
||
|
// Loop around each cell
|
||
|
for ($x = ($coord->x - 1); $x <= ($coord->x + 1); ++$x)
|
||
|
{
|
||
|
for ($y = ($coord->y - 1); $y <= ($coord->y + 1); ++$y)
|
||
|
{
|
||
|
if ($x < 0 || $y < 0 || $x >= TERM_X || $y >= TERM_Y)
|
||
|
throw new SDGException();
|
||
|
if ($x >= 0 && $x < TERM_X && $y >= 0 && $y < TERM_Y && !isset($reserved[$x . ',' . $y])) // This cell is empty
|
||
|
{
|
||
|
try
|
||
|
{
|
||
|
$wall = new Wall();
|
||
|
$wall->setPlace(new Place($this, new Coord($x, $y)));
|
||
|
} catch (PlaceOccupiedException $e)
|
||
|
{ }
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Create empty floor tile for reserved place
|
||
|
$floor = new EmptyFloor();
|
||
|
$floor->setPlace(new Place($this, $coord));
|
||
|
}
|
||
|
}
|
||
|
}
|