<?php
  // Copyright 2019 Andrew 'Diddymus' Rolfe. All rights reserved.
  //
  // Released under: Free Public License 1.0.0
  //
  // Permission to use, copy, modify, and/or distribute this software for any
  // purpose with or without fee is hereby granted.
  //
  // THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
  // WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
  // MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
  // ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
  // WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
  // ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR
  // IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.

  // This is a small, simple, single player, plain text, 'toy' adventure game.
  // It is intended to be used for learning, experimenting, general hacking and
  // having fun while doing so.
  //
  // Commands available to the player are:
  //
  //    N or NORTH, S or SOUTH, E or EAST, W or WEST - for movement
  //    L or LOOK - describe the current location again
  //    SNEEZE - makes the player sneeze
  //    QUIT - exit the game
  //
  // To run the game simply open a terminal or command window, change to the
  // directory/folder where this file is saved, then type following:
  //
  //    php -f cottage.php
  //
  // This assumes you have PHP installed, the full path to the 'php' binary may
  // need to be qualified. E.G. c:\php-7.3.5\php -f cottage.php
  //
  // NOTE: THIS IS NOT INTENDED TO BE RUN USING A WEB SERVER. IT IS A CLI,
  // COMMAND LINE, PROGRAM ONLY!
  //
  // Have fun, and hack the code :)
  //
  // Tested on:
  //   Linux (Debian) - PHP v7.3.4 and v5.6.26
  //   Windows 10 Pro - PHP v7.3.5
	//
	// Additional information and background can be found here:
	//
	//   http://www.wolfmud.org/journal/2019/5/9.html
	//

  // Turn on full error reporting
  error_reporting(E_ALL);
  ini_set('display_errors', 'stderr');

  // world contains all of the locations in the game world. Each location is
  // keyed by a reference. This means each location reference must be unique.
  // This is the same reference that is used to map exits to other locations.
  $world = null;

  // Constants for direction indexes. These are used to define the DIRECTIONS
  // and DIRECTION_NAMES. They also let us use NORTH, EAST, SOUTH and WEST
  // instead of the cryptic numbers 0, 1, 2 and 3 in the code.
  const NORTH = 0;
  const EAST  = 1;
  const SOUTH = 2;
  const WEST  = 3;

  // DIRECTIONS for mapping short or long direction names to direction indexes.
  // For example DIRECTIONS["E"] will be equal to the constant EAST (which is
  // 1).
  const DIRECTIONS = [
    'N'     => 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();
?>
