NORTH, 'NORTH' => NORTH, 'E' => EAST, 'EAST' => EAST, 'SOUTH' => SOUTH, 'S' => SOUTH, 'WEST' => WEST, 'W' => WEST, ]; // DIRECTION_NAMES for mapping direction indexes to descriptive direction // names. For example DIRECTION_NAMES[EAST] would be equal to the string // "east". const DIRECTION_NAMES = [ NORTH => 'north', EAST => 'east', SOUTH => 'south', WEST => 'west', ]; // location is used to define places in the world. The exits are an array // keyed by direction and pointing to a location reference. For example: // // [SOUTH=>'sidepath'] // // Indicates an exit south, leading to the location with a reference of // "sidepath". By using string references instead of location references we // avoid having to have a two pass setup: One to define the locations and the // second to link up the exits. It also means we can quickly and easily find // a location in the world array for any exit. See the loadWorld function for // some more details. class location { private $name; // Short location name private $description; // Longer location description private $exits; // Exits from this location to other locations public function __construct($name, $description, $exits) { $this->name = $name; $this->description = $description; $this->exits = $exits; } // describe returns a description of a location and exits available. public function describe() { $buf = sprintf("[%s]\n%s\n\n", $this->name, $this->description); $buf .= sprintf('You can see exits:'); // Examine the exits for the location foreach (DIRECTION_NAMES as $dir=>$name) { if (isset($this->exits[$dir]) === true) { $buf .= sprintf(' %s', $name); } } return $buf; } // move returns the location reference found by taking the exit specified // by the direction index. If the exit is not available from the current // location then false will be returned. If the exit leads to a location // that does not exist then null will be returned. public function move($dirIndex) { global $world; if (array_key_exists($dirIndex, $this->exits) === false) { return false; } $to = $this->exits[$dirIndex]; if (isset($world[$to]) === false) { return null; } return $to; } } // player represents the user in the game. class player { private $quit; // Are we asking to quit? private $input; // Current input from the user private $where; // Where we are, a reference to a location private $in; // Input stream from player private $out; // Output stream to player private $msgBuf; // A message buffer for output buffering // player constructor that initialises a new player and places them at the // given starting location. public function __construct($start) { $this->where = $start; $this->input = ''; $this->quit = false; $this->in = STDIN; $this->out = STDOUT; $this->buffer = ''; } // msg is used to write a message to be displayed to the player. msg may be // called multiple times and the data will be written to a buffer. The // buffer is not sent to the player until flush is called. Messages should // be composed using "\n" for line feed, flush will convert this to the // current platform's encoding using PHP_EOL automatically. public function msg($format, ...$a) { $this->msgBuf .= sprintf($format, ...$a); } // flush sends the content of the message buffer to the player. Once the // message buffer is sent it will be cleared ready for new data. Flush will // automatically convert "\n" line feeds to line feeds suitable for the // current platform using PHP_EOL. public function flush() { if (strlen($this->msgBuf) === 0) { return; } if (PHP_EOL !== "\n") { $this->buffer = str_replace("\n", PHP_EOL, $this->buffer); } fwrite($this->out, $this->msgBuf); fflush($this->out); $this->msgBuf = ''; } // getInput is used to read commands from the player. public function getInput() { $this->input = fgets($this->in, 80); } // processInput takes a player's input, preprocesses the command and passes // the command to processCommand for handling. public function processInput() { $cmd = strtoupper(trim($this->input)); $this->processCommand($cmd); } // processCommand handles the given command. This is separate from // processInput so that it can be called directly. See main, which calls // processCommand("LOOK") to display the initial location without any input // from the user. Also see move which uses processCommand("LOOK") to // display the location moved to. public function processCommand($cmd) { switch ($cmd) { case '': // no command, maybe player just pressed enter? break; case 'L': case 'LOOK': $this->look(); break; case 'N': case 'E': case 'S': case 'W': case 'NORTH': case 'EAST': case 'SOUTH': case 'WEST': $this->move($cmd); break; case 'SNEEZE': $this->sneeze(); break; case 'QUIT': $this->quit = true; $this->msg("\nBye bye!\n\n"); break; default: $this->msg("Eh?\n"); } } // isQuitting returns true is the player has requested to quit the game // else false. public function isQuitting() { return $this->quit; } // greeting displays a simple welcome message to the player. public function greeting() { $this->msg("\nWelcome to the cottage!\n\n"); } // sneeze makes the player sneeze! An example of a very simple command. private function sneeze() { $this->msg("Achoo! You sneeze.\n"); } // look describes the current location. As exits are stored in an array // their ordering is undetermined. We therefore loop over the direction // indexes for consistent ordering and test for each exit. private function look() { global $world; $where = $world[$this->where]; $this->msg($where->describe()); $this->msg("\n"); } // move attempts to follow an exit to a new location. The direction to take // is given as a short 'N' or long 'NORTH' direction name. If the direction // successfully leads to a new location the player's location will be // updated. private function move($direction) { global $world; $dIndex = DIRECTIONS[$direction]; $from = $world[$this->where]; $to = $from->move($dIndex); // If exit not available... if ($to === false) { $this->msg("You can't go %s!\n", DIRECTION_NAMES[$dIndex]); return; } // If new location does not actually exist... if ($to === null) { $this->msg( "Oops, you can't actually go %s.\n", DIRECTION_NAMES[$dIndex]); return; } // Move player and describe new location $this->where = $to; $this->processCommand('LOOK'); } } // loadWorld creates all of the locations in the world. Here we are // populating the world directly, the format matches the definition of // location: // // 'reference' = new location( // 'short name', // 'longer description', // [ // direction=>'to reference', // direction=>'to reference', // Repeat for each exit as needed // ] // }, // // For example: // // 'shed' = new location( // 'Shed', // 'You are standing in a small, dark, dingy garden shed.', // [ // SOUTH=>'sidepath', // ] // }, // // This defines a location with a reference of "shed". The short description // is "Shed" and the long description is "You are standing in a small, dark, // dingy garden shed.", there is a single exit south which leads to the // location with a reference of "sidepath". // // NOTE: If you use the same reference more than once, only the last one will // be useable and the others will be overwritten! function loadWorld() { return [ 'path'=> new location( 'Path outside a cottage', 'You are on a path leading north to a small, white cottage.', [ NORTH=>'frontdoor', SOUTH=>'bogusExit', // South leads to a non-existent location here WEST=>'frontlawn', ] ), 'frontlawn'=> new location( 'Front lawn', 'You are standing on a small, neat lawn outside the cottage.', [ NORTH=>'sidepath', EAST=>'path', ] ), 'frontdoor'=> new location( 'Front door', 'You at standing at the open front door of a small, white cottage.', [ NORTH=>'southhall', SOUTH=>'path', ] ), 'southhall'=> new location( 'Hallway', 'You are in a hallway by the front door.', [ NORTH=>'northhall', EAST=>'kitchen', SOUTH=>'frontdoor', WEST=>'study', ] ), 'northhall'=> new location( 'Hallway', 'You are in a hallway. Some pictures hang on the wall.', [ NORTH=>'bathroom', SOUTH=>'southhall', WEST=>'bedroom', ] ), 'kitchen'=> new location( 'Kitchen', 'You are standing in a small, very tidy kitchen.', [ NORTH=>'backlawn', WEST=>'southhall', ] ), 'study'=> new location( 'Study', 'This is a small, cosy study with a desk and many bookcases.', [ EAST=>'southhall', ] ), 'bedroom'=> new location( 'Bedroom', 'You are in a small bedroom with a bed and a large wardrobe.', [ EAST=>'northhall', ] ), 'bathroom'=> new location( 'Bathroom', 'You are in a small, very clean and bright bathroom.', [ SOUTH=>'northhall', ] ), 'backlawn'=> new location( 'Back lawn', 'You are standing on a small, neat lawn at the back of the cottage.', [ SOUTH=>'kitchen', WEST=>'sidepath', ] ), 'sidepath'=> new location( 'Side path', 'You are on a narrow path running along the side of the cottage.', [ NORTH=>'shed', EAST=>'backlawn', SOUTH=>'frontlawn', ] ), 'shed'=> new location( 'Shed', 'You are standing in a small, dark, dingy garden shed.', [ SOUTH=>'sidepath', ] ), ]; } // main performs initialisation and then runs the main game loop until the // player quits. function main() { global $world; $world = loadWorld(); $p = new player('path'); $p->greeting(); $p->processCommand('LOOK'); while ($p->isQuitting() === false) { $p->msg('?'); $p->flush(); $p->getInput(); $p->processInput(); } $p->flush(); } main(); ?>