From b2588f5f5415f3ea61a0d8a9fb1c5afe74bde39c Mon Sep 17 00:00:00 2001 From: Mikko Ahlroth Date: Thu, 18 Mar 2021 21:58:26 +0200 Subject: [PATCH] Initial commit --- class_abstractbeing.php | 158 ++++++++++++++++ class_being.php | 9 + class_cannotequipexception.php | 9 + class_coord.php | 40 +++++ class_display.php | 82 +++++++++ class_displayfactory.php | 19 ++ class_dude.php | 30 ++++ class_dudestore.php | 16 ++ class_dungeon.php | 50 ++++++ class_emptyfloor.php | 7 + class_entity.php | 113 ++++++++++++ class_human.php | 26 +++ class_item.php | 15 ++ class_level.php | 298 +++++++++++++++++++++++++++++++ class_leveldrawer.php | 50 ++++++ class_place.php | 23 +++ class_placeexception.php | 6 + class_placeoccupiedexception.php | 6 + class_randomitemfactory.php | 9 + class_room.php | 7 + class_sdgexception.php | 6 + class_settings.php | 24 +++ class_settingsman.php | 19 ++ class_wall.php | 7 + inc_ansi.php | 4 + inc_stuffz.php | 10 ++ sdg.php | 54 ++++++ 27 files changed, 1097 insertions(+) create mode 100644 class_abstractbeing.php create mode 100644 class_being.php create mode 100644 class_cannotequipexception.php create mode 100644 class_coord.php create mode 100644 class_display.php create mode 100644 class_displayfactory.php create mode 100644 class_dude.php create mode 100644 class_dudestore.php create mode 100644 class_dungeon.php create mode 100644 class_emptyfloor.php create mode 100644 class_entity.php create mode 100644 class_human.php create mode 100644 class_item.php create mode 100644 class_level.php create mode 100644 class_leveldrawer.php create mode 100644 class_place.php create mode 100644 class_placeexception.php create mode 100644 class_placeoccupiedexception.php create mode 100644 class_randomitemfactory.php create mode 100644 class_room.php create mode 100644 class_sdgexception.php create mode 100644 class_settings.php create mode 100644 class_settingsman.php create mode 100644 class_wall.php create mode 100644 inc_ansi.php create mode 100644 inc_stuffz.php create mode 100644 sdg.php diff --git a/class_abstractbeing.php b/class_abstractbeing.php new file mode 100644 index 0000000..49148a9 --- /dev/null +++ b/class_abstractbeing.php @@ -0,0 +1,158 @@ +name = 'FAIL_' . uniqid(); + } + + static public function giveRandomItems(AbstractBeing $being) + { + /*$amount = rand(0, static::MAXITEMS); + for ($i = 0; $i < $amount; ++$i) + { + $being->items[] = RandomItemFactory::getItem($being); + }*/ + } + + static public function equipRandomItems(AbstractBeing $being) + { + /*$amount = rand(0, count($being->items)); + for ($i = 0; $i < $amount; ++$i) + { + try + { + $being->equip($being->items[$i]); + } catch (CannotEquipException $e) + { } + }*/ + } + + static public function setRandomStats(AbstractBeing $being) + { + + } + + + + public function __construct($name = null, $exp = 0, array $items = null, array $equips = null, $speed = null, $sight = null, $hp = 0, $mp = 0, $money = null, Place $place = null) + { + $this->name = $name; + + if ($items === null) + static::giveRandomItems($this); + + if ($equips === null) + static::equipRandomItems($this); + + static::setRandomStats($this); + if ($speed !== null) + $this->speed = $speed; + if ($sight !== null) + $this->sight = $sight; + if ($hp > 0) + $this->hp = $hp; + if ($mp > 0) + $this->mp = $mp; + if ($money !== null) + $this->money = $money; + if ($exp > 0) + $this->exp = $exp; + + if ($place != null) + $this->setPlace($place); + } + + public function setRandomPlace(Level $level = null) + { + $coord = $level->getEmptyPlace(); + $this->setPlace(new Place($level, $coord)); + } + + public function move($dir) + { + $newX = $this->place->getCoord()->x; + $newY = $this->place->getCoord()->y; + if ($dir & DIR_DOWN) + ++$newY; + elseif ($dir & DIR_UP) + --$newY; + if ($dir & DIR_RIGHT) + ++$newX; + elseif ($dir & DIR_LEFT) + --$newX; + + $newCoord = new Coord($newX, $newY); + if ($this->place->getLevel()->whatsAt($newCoord) instanceof EmptyFloor) + $this->setPlace(new Place($this->place->getLevel(), $newCoord)); + + // Clear sight data + $this->seenCells = array(); + } + + public function getName() + { + return $this->name; + } + + public function getItems() + { + return $this->items; + } + + public function getEquips() + { + return $this->equips; + } + + public function getSpeed() + { + return $this->speed; + } + + public function getSight() + { + return $this->sight; + } + + public function getHP() + { + return $this->hp; + } + + public function getMP() + { + return $this->mp; + } + + public function getMoney() + { + return $this->money; + } + + public function getExp() + { + return $this->exp; + } + + public function __toString() + { + if (DudeStore::getDude()->getBase() == $this) + return '@'; + return parent::__toString(); + } +} diff --git a/class_being.php b/class_being.php new file mode 100644 index 0000000..4e8c692 --- /dev/null +++ b/class_being.php @@ -0,0 +1,9 @@ +x = $x; + $this->y = $y; + } + + public function getDistance(Coord $coord) + { + return round(sqrt(pow($coord->x - $this->x, 2) + pow($coord->y - $this->y, 2))); + } + + public function __get($name) + { + if ($name == 'x' || $name == 'y') + { + return $this->$name; + } + else + throw new SDGException('Can only access X and Y in ' . __CLASS__); + } + + public function __toString() + { + return $this->x . ',' . $this->y; + } +} diff --git a/class_display.php b/class_display.php new file mode 100644 index 0000000..c36a3f2 --- /dev/null +++ b/class_display.php @@ -0,0 +1,82 @@ +xSize = $xSize; + $this->ySize = $ySize; + $this->stream = $stream; + $this->color = self::COLOR_NONE; + } + + public function write($text, $style = self::STYLE_NONE, $color = self::COLOR_NONE) + { + if ($style != self::STYLE_NONE) + $this->setStyle($style); + if ($color != self::COLOR_NONE) + $this->setColor($color); + fwrite($this->stream, $text); + } + + public function writeTo($x, $y, $text, $style = self::STYLE_NONE, $color = self::COLOR_NONE) + { + $this->write(ANSI_CSI . $y . ';' . $x . 'H' . $text, $style, $color); + } + + public function setCursor($x, $y) + { + $this->writeTo($x, $y, ''); + } + + public function setColor($color = self::COLOR_NONE) + { + $this->write(ANSI_CSI . (30 + $color) . 'm'); + } + + public function setStyle($style = self::STYLE_NONE) + { + if ($style == self::STYLE_NONE) + $this->write(ANSI_CSI . 0 . 'm'); + else + { + if ($style & self::STYLE_BOLD) + $this->write(ANSI_CSI . 1 . 'm'); + if ($style & self::STYLE_ITALIC) + $this->write(ANSI_CSI . 3 . 'm'); + if ($style & self::STYLE_UNDERLINE) + $this->write(ANSI_CSI . 4 . 'm'); + if ($style & self::STYLE_BLINK) + $this->write(ANSI_CSI . 5 . 'm'); + if ($style & self::STYLE_INVERSE) + $this->write(ANSI_CSI . 7 . 'm'); + } + } + + public function clear() + { + $this->write(ANSI_CSI . 2 . 'J'); + $this->setCursor(null, null); + } +} diff --git a/class_displayfactory.php b/class_displayfactory.php new file mode 100644 index 0000000..12cb20c --- /dev/null +++ b/class_displayfactory.php @@ -0,0 +1,19 @@ +base = $base; + DudeStore::add($this); + } + + public function getBase() + { + return clone $this->base; + } + + public function move($dir) + { + $this->base->move($dir); + } + + public function __call($method, $arguments) + { + if (is_callable('parent::' . $method)) + call_user_func_array('parent::' . $method, $arguments); + else + throw new SDGException('Cannot call nonexistant method ' . $method); + } +} diff --git a/class_dudestore.php b/class_dudestore.php new file mode 100644 index 0000000..698451a --- /dev/null +++ b/class_dudestore.php @@ -0,0 +1,16 @@ +lowest = 0; + $this->highest = 0; + $this->levels = array(); + } + + public function addLevelAbove(Level $level) + { + if (!empty($this->levels)) + { + ++$this->highest; + } + $this->levels[$this->highest] = $level; + } + + public function addLevelBelow(Level $level) + { + if (empty($this->levels)) + { + --$this->lowest; + } + $this->levels[$this->lowest] = $level; + } + + public function getLevels() + { + return $this->levels; + } + + public function getLevel($number) + { + return (isset($this->levels[$number]))? $this->levels[$number] : null; + } + + public function getLevelNumber(Level $level) + { + foreach ($levels as $number => $savedLevel) + if ($savedLevel == $level) + return $number; + } +} diff --git a/class_emptyfloor.php b/class_emptyfloor.php new file mode 100644 index 0000000..eb09098 --- /dev/null +++ b/class_emptyfloor.php @@ -0,0 +1,7 @@ +place !== null) + $this->place->getLevel()->removeEntity($this->place->getCoord()); + + $this->place = $place; + + if ($this->place !== null) + $this->place->getLevel()->insertEntity($this->place->getCoord(), $this); + } + + public function getPlace() + { + return $this->place; + } + + public function drawPath(Entity $target, $callback) + { + $targetX = $target->getPlace()->getCoord()->x; + $targetY = $target->getPlace()->getCoord()->y; + $startX = $this->getPlace()->getCoord()->x; + $startY = $this->getPlace()->getCoord()->y; + + if ($startX == $targetX && $startY == $targetY) + return; + + $diffX = $targetX - $startX; + $diffY = $targetY - $startY; + + $step = max(abs($diffX), abs($diffY)); + $stepX = $diffX / $step; + $stepY = $diffY / $step; + if ($stepX != 0) + $steps = floor($diffX / $stepX); + else + $steps = floor($diffY / $stepY); + + $x = $startX; + $y = $startY; + $i = 0; + do + { + $x += $stepX; + $y += $stepY; + ++$i; + if (!$callback(new Coord(floor($x), floor($y)))) + return; + + } while ((floor($x) != $targetX || floor($y) != $targetY) + && $i < $steps); + } + + public function canSee(Entity $target) + { + // Check if already marked as seen + if (isset($this->seenCells[$target->getPlace()->getCoord()->x][$target->getPlace()->getCoord()->y])) + return true; + + // Check distance + if ($this->getSight() < $this->getPlace()->getCoord()->getDistance($target->getPlace()->getCoord())) + return false; + + // Check each cell on the way for opaque objects + $level = $this->getPlace()->getLevel(); + $return = true; + $seenCells =& $this->seenCells; + $this->drawPath($target, function(Coord $coord) use (&$return, $level, $target, + &$seenCells) + { + $obj = $level->whatsAt($coord); + if ($obj !== null && $obj != $target && $obj->getVisibility() == Entity::VSB_VISIBLE) + { + $return = false; + return false; + } + $seenCells[$coord->x][$coord->y] = true; + return true; + }); + + return $return; + } +} diff --git a/class_human.php b/class_human.php new file mode 100644 index 0000000..d8433b3 --- /dev/null +++ b/class_human.php @@ -0,0 +1,26 @@ +sex = rand(0, 1); + parent::__construct($name, $exp, $items, $equips, $speed, $sight, $hp, $mp, $money, $place); + } + + public function setSex($sex) + { + $this->sex = $sex; + } + + public function getSex() + { + if ($this->sex == 0) + return 'male'; + return 'female'; + } +} diff --git a/class_item.php b/class_item.php new file mode 100644 index 0000000..aa74acc --- /dev/null +++ b/class_item.php @@ -0,0 +1,15 @@ +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)); + } + } +} diff --git a/class_leveldrawer.php b/class_leveldrawer.php new file mode 100644 index 0000000..52f03fa --- /dev/null +++ b/class_leveldrawer.php @@ -0,0 +1,50 @@ +dude = DudeStore::getDude(); + $this->changeDisplay($disp); + } + + public function changeDisplay(Display $disp) + { + $this->disp = $disp; + } + + public function drawEverything() + { + $this->dude->getPlace()->getLevel()->printLevel(); + } + + public function drawDudeView() + { + $this->drawView($this->dude->getBase()); + } + + public function drawView(AbstractBeing $being) + { + $radius = $being->getSight(); + $beingX = $being->getPlace()->getCoord()->x; + $beingY = $being->getPlace()->getCoord()->y; + + // Only print the characters inside the being's sight radius + for ($x = $beingX - $radius; $x <= $beingX + $radius; ++$x) + { + for ($y = $beingY - $radius; $y <= $beingY + $radius; ++$y) + { + $obj = $being->getPlace()->getLevel()->whatsAt(new Coord($x, $y)); + if ($obj !== null) + { + // Draw path to object and only display those that can be seen + if ($being->canSee($obj)) + $this->disp->writeTo($x + 1, $y + 1, $obj); + } + } + } + } +} diff --git a/class_place.php b/class_place.php new file mode 100644 index 0000000..3a5f342 --- /dev/null +++ b/class_place.php @@ -0,0 +1,23 @@ +level = $level; + $this->coord = $coord; + } + + public function getCoord() + { + return $this->coord; + } + + public function getLevel() + { + return $this->level; + } +} diff --git a/class_placeexception.php b/class_placeexception.php new file mode 100644 index 0000000..8f52e5f --- /dev/null +++ b/class_placeexception.php @@ -0,0 +1,6 @@ +settings = array(); + } + + public function set($setting, $value) + { + $this->settings[$setting] = $value; + } + + public function get($setting) + { + if (is_object($this->settings[$setting])) + return clone $this->settings[$setting]; + else + return $this->settings[$setting]; + } +} diff --git a/class_settingsman.php b/class_settingsman.php new file mode 100644 index 0000000..0a6292c --- /dev/null +++ b/class_settingsman.php @@ -0,0 +1,19 @@ +clear(); +$disp->setColor(Display::COLOR_RED); + +$dungeon = new Dungeon(); + +$level = new Level($dungeon); +$dungeon->addLevelBelow($level); + +$human = new Human('', 0, null, null, null, 40); +$enemy = new Human(); +$enemy->setRandomPlace($level); +$dude = new Dude($human); +$human->setRandomPlace($level); + +$draw = new LevelDrawer($disp); + +DisplayFactory::terminalSetRaw(); + +$disp->clear(); +$draw->drawDudeView(); + +$input = ''; +do +{ + $input = fread(STDIN, 1); + if (ctype_digit($input)) + { + $dude->move($input); + $disp->clear(); + $draw->drawDudeView(); + } + elseif ($input == 'd') + { + $enemy->drawPath($disp, $dude->getBase()); + } +} while($input != 'q'); + +$disp->setStyle(); +DisplayFactory::terminalSetCooked();