How’d They Do It? PHPSnake: Detecting Keypresses

Bruno Skvorc
Share

Vector image of an old school joystick

At a recent conference in Bulgaria, there was a hackathon for which Andrew Carter created a PHP console version of the popular “snake” game.

I thought it was a really interesting concept, and since Andrew has a history of using PHP for weird things, I figured I’d demystify and explain how it was done.

The original repository is here, but we’ll build a version of it from scratch in this series so no need to clone it.

Screenshot of the game

Prerequisites and Rules

As usual, I’ll be using my Homestead Improved box to get up and running instantly. You can use your own environment, just make sure it has a decent version of PHP runnable from the command line.

The snake game we’re replicating has the following features:

  • a snake starts as a single on-screen character, and gets longer by one character every time it eats a piece of food.
  • food is spawned randomly anywhere on the map.
  • in single player mode, the snake is controlled by the arrow keys.
  • in two player mode, one snake is controlled with the WSAD keys, while the other is controlled with the arrow keys.
  • in single player mode, the walls are obstacles and cause a collision. Running into a wall or into yourself ends the game.
  • in multi player mode, only your own snake or the enemy’s snake is an obstacle – the walls wrap around the world. Colliding will reset your snake’s length to 0. The player with the longest snake after 100 seconds have elapsed is the winner.
  • it’s CLI, so does not run in the browser – it runs in the terminal window

Note that the game doesn’t work in native Windows – to run it on a Windows platform, use a good VM like Homestead Improved.

Bootstrapping

To launch a CLI (console) game, we need something similar to an index.php file in traditional websites – a “front controller” which reads our command line input, parses it, and then launches the required classes, just like in a traditional web app. We’ll call this file play.php.

<?php

$param = ($argc > 1) ? $argv[1] : '';

echo "Hello, you said: " . $param;

Okay, so if we launch this file with php play.php something, we’ll get:

Hello, you said: something

Let us now make a bogus SnakeGame.php in the subfolder classes class which gets invoked by this front controller.

// classes/Snake.php

<?php

namespace PHPSnake;

class SnakeGame
{
    public function __construct()
    {
        echo "Hello, I am snake!";
    }
}

Let’s also update the front controller to load this class and invoke it:

<?php

use PHPSnake\SnakeGame;

require_once 'classes/SnakeGame.php';

$param = ($argc > 1) ? $argv[1] : '';

$snake = new SnakeGame();

Okay, we should be seeing a snakey greeting now if we re-run php play.php.

Frames

The way that traditional games work is by re-checking the system’s state every frame, and a higher frame rate means more frequent checking. For example, when the screen is rendered (a single frame), the system can tell us that the letter A is pressed, and that one of the players is in the process of colliding with food so a snake growth needs to happen. All this happens in a single frame, and “FPS” or frames per second means exactly that – how many of these system observations happen per second.

Programming languages intended for this have built-in loops for checking state. PHP… not so much. We can hack our way around this, but let’s go through it all step by step. Let’s modify Snake’s constructor like so:

    public function __construct()
    {
        echo "Hello, I am snake!";

        $stdin = fopen('php://stdin', 'r');

        while (1) {
            $key = fgetc($stdin);
            echo $key;
        }
    }

First, we open an “stdin” stream, meaning we’re creating a way for PHP to get “standard input” from the command line, treating it as if it was a file (hence, fopen). The fgetc function is used to get a single character from a file pointer (as opposed to fgets which gets a whole line) and then the key is printed on screen. The while loop is there so PHP keeps waiting for more input, and doesn’t end the script after a single key is pressed.

If we try running our app, though, we’ll notice that the key is only echoed out after we press enter – so after a new line. What’s more – everything we wrote gets echoed. What we want instead, is for PHP to echo out each key we press, as we press it.

Here are two ways to accomplish this.

stty

The first way is via a tool called stty which comes native with terminal applications on *nix systems (so no Windows, unless you’re using a VM like Homestead Improved). It’s used to modify and configure terminal input and output through the use of flags – when prefixed with -, these often mean “deactivation” and vice versa.

What we want is stty’s cbreak flag. As per the docs:

Normally, the tty driver buffers typed characters until a newline or carriage return is typed. The cbreak routine disables line buffering and erase/kill character-processing (interrupt and flow control characters are unaffected), making characters typed by the user immediately available to the program.

In layman’s terms, we’re no longer waiting for the enter key to be fired to send the inputs.

    public function __construct()
    {
        echo "Hello, I am snake!";

        system('stty cbreak');
        $stdin = fopen('php://stdin', 'r');

        while (1) {
            $c = ord(fgetc($stdin));
            echo "Char read: $c\n";
        }

We invoke the system function which basically acts as a shell proxy and forwards the command provided as an argument to the terminal we’re running the PHP app from. After this, running the app with php play.php should let you write characters and echo them immediately after every keypress.

Note that we’re getting the key code because we wrapped the character into ord – this function returns the ASCII code for a given character.

stty solution working

readline callback

The second way is by using the surprisingly mystical and underdocumented readline_callback_handler_install function in combination with stream_select (also *nix only because stream_select calls the system select command, which is not available in Windows).

readline_callback_handler_install takes a prompt message as its first argument (so, what to “ask” the user), and a callback as the second. In our case, we leave it as an empty function because we don’t really need it – we’re reading characters by parsing STDIN, a constant which is actually just a shortcut for fopen('php://stdin', 'r');. Our code for this part looks like this:

    public function __construct()
    {
        echo "Hello, I am snake!";

        readline_callback_handler_install('', function() { });
        while (true) {
            $r = array(STDIN);
            $w = NULL;
            $e = NULL;
            $n = stream_select($r, $w, $e, null);
            if ($n) {
                $c = ord(stream_get_contents(STDIN, 1));
                echo "Char read: $c\n";

            }
        }
    }

The stream select accepts several streams, and acts as an event listener for when something changes on any of them. Given that we’re only looking for “read” (i.e. input), we define that as an array format of STDIN. The others are set to NULL, we don’t need them. Since stream_select accepts only values by reference, we cannot pass NULL in directly, they must be defined as variables beforehand.

The if block checks if $n is positive ($n is the number of updated streams), and if so, it extracts the first character from STDIN, which is our input key press:

readline solution working

Which is better?

I prefer the stty method because both are *nix only, both invoke system commands, but the latter is arguably more complex, and its effect can vary depending on current terminal settings in a given OS.

Notice a crucial difference between the two gifs above – the stty method also echoes out the character being pressed before outputting its keycode. To completely remove all auto-output and have PHP process it all, we need another stty flag: -echo.

    public function __construct()
    {
        echo "Hello, I am snake!";

        system('stty cbreak -echo');
        $stdin = fopen('php://stdin', 'r');

        while (1) {
            $c = ord(fgetc($stdin));
            echo "Char read: $c\n";
        }

    }

As per the docs, -echo disables the output of input characters.

Mapping Snakes to Directions

It goes without saying we’ll need a way to tell each snake which direction to move in. First, let’s make a new Snake class in Snake.php to represent an instance of a player and hold their state.

<?php

namespace PHPSnake;

class Snake
{

    /** @var string */
    private $name;

    /** @var string */
    private $direction;

    /** @var int */
    private $size = 0;

    const DIRECTIONS = ['UP', 'DOWN', 'LEFT', 'RIGHT'];

    public function __construct(string $name = null)
    {
        if ($name === null) {
            $this->name = $this->generateRandomName();
        } else {
            $this->name = $name;
        }
    }

    public function getName() : string
    {
        return $this->name;
    }

    public function setDirection(string $direction) : Snake
    {
        $direction = strtoupper($direction);
        if (!in_array($direction, Snake::DIRECTIONS)) {
            throw new \InvalidArgumentException(
                'Invalid direction. Up, down, left, and right supported!'
            );
        }
        $this->direction = $direction;
        echo $this->name.' is going '.$direction."\n";

        return $this;
    }

    private function generateRandomName(int $length = 6) : string
    {
        $length = ($length > 3) ? $length : 6;
        $name = '';

        $consonants = 'bcdfghklmnpqrstvwxyz';
        $vowels = 'aeiou';

        for ($i = 0; $i < $length; $i++) {
            if ($i % 2 == 0) {
                $name .= $consonants[rand(0, strlen($consonants)-1)];
            } else {
                $name .= $vowels[rand(0, strlen($vowels)-1)];
            }
        }

        return ucfirst($name);
    }
}

Upon instantiation, a snake is given a name. If none is provided, a simple function generates one at random. The direction is null at the time of instantiation, and the directions that can be passed in are limited by the DIRECTIONS constant of the class.

Next, we’ll update our SnakeGame class.

<?php

namespace PHPSnake;

class SnakeGame
{

    /** @var array */
    private $snakes = [];

    public function __construct()
    {

    }

    /**
     * Adds a snake to the game
     * @param Snake $s
     * @return SnakeGame
     */
    public function addSnake(Snake $s) : SnakeGame
    {
        $this->snakes[] = $s;
        return $this;
    }

    /**
     * Runs the game
     */
    public function run() : void
    {
        if (count($this->snakes) < 1) {
            throw new \Exception('Too few players!');
        }
        system('stty cbreak -echo');

        $stdin = fopen('php://stdin', 'r');

        while (1) {
            $c = ord(fgetc($stdin));
            echo "Char read: $c\n";
        }
    }
}

This moves the keypress watching logic into a run function which we call after adding the necessary snakes to the game via addSnake.

Finally, we can update the frontcontroller to use these updated classes.

<?php

use PHPSnake\Snake;
use PHPSnake\SnakeGame;

require_once 'classes/Snake.php';
require_once 'classes/SnakeGame.php';

$param = ($argc > 1) ? $argv[1] : '';

$game = new SnakeGame();
$game->addSnake(new Snake());
$game->run();

Now things are getting a bit more structured! Finally, let’s map the directions to the snakes.

We’ll have custom key mappings for each player, and depending on how many players we include, that’s how many mappings we’ll load. That way is much simpler than a long switch block. Let’s add the $mappings property to our SnakeGame class:

    /**
     * Key mappings
     * @var array
     */
    private $mappings = [
        [
            65 => 'up',
            66 => 'down',
            68 => 'left',
            67 => 'right',
            56 => 'up',
            50 => 'down',
            52 => 'left',
            54 => 'right',
        ],
        [
            119 => 'up',
            115 => 'down',
            97 => 'left',
            100 => 'right',
        ],

    ];

Each array corresponds to a single player/snake. The main player can be controlled either via the cursor keys, or the corresponding numpad numbers. Player two can only be controlled via the WSAD keys. You can see how easy this makes adding new mappings for additional players.

Then, let’s update the run method:

    /**
     * Runs the game
     */
    public function run() : void
    {
        if (count($this->snakes) < 1) {
            throw new \Exception('Too few players!');
        }

        $mappings = [];
        foreach ($this->snakes as $i => $snake) {
            foreach ($this->mappings[$i] as $key => $dir) {
                $mappings[$key] = [$dir, $i];
            }
        }

        system('stty cbreak -echo');

        $stdin = fopen('php://stdin', 'r');

        while (1) {
            $c = ord(fgetc($stdin));
            echo "Char read: $c\n";

            if (isset($mappings[$c])) {
                $mapping = $mappings[$c];
                $this->snakes[$mapping[1]]->setDirection($mapping[0]);
            }

        }
    }

The run method now loads as many mappings as there are snakes, and reorders them so that the keycode is the key of the mapping array, and the direction and snake index are a child array – this makes it very easy to later on just single-line the direction changes. If we run our game now (I updated play.php to add two snakes), we’ll notice that pressing random keys just produces key codes, while pressing WSAD or the cursor keys outputs the name of the snake and the direction it’ll be moving in after the keypress:

Snakes yelling out directions

We now have a rather nicely structured game base for monitoring key presses and responding to them.

Conclusion

In this tutorial, we looked at keypress input game loops in PHP CLI applications. This unconventional use of PHP is about to get even weirder in part two, where we’ll deal with rendering players, movement, borders, and collisions.

In the meanwhile, do you have any ideas on how to further improve fetching the keypress? Let us know in the comments!