The Room Do I dare disturb the universe?

A Roguelike in Perl Part 0 - The Setup

Introductions

Perl 5.38.0 just dropped, and with it comes (in my opinion) one of the most exciting experiments in Perl, a new built-in object system. If Moose was the postmodern object system for Perl then feature 'class' is post-postmodern, it’s a meta-modern object system.

The last couple of years I’ve not been paid to write Perl, so I’ve had to learn a few other languages. I’ve discovered the best way for me to learn a language currently is to write a roguelike game with that language. There happens to be a pretty good tutorial that has been copied to a bunch of languages, but not Perl.

So I thought let’s kill two birds with one stone. Let’s write a roguelike in Perl using the new object system.

Before We Begin

For this series of blog posts I’m going to assume you know Perl, but you maybe haven’t worked with it in a decade or two. I’m also going to assume you’re comfortable in a unix shell knowing how to run commands and edit files to change those commands.

Maybe none of the above is true, you’re still free to try to follow along. Just don’t freak out if things don’t make sense.

Getting Everything Installed

Since this is specifically about the new class feature in 5.38.0 you’re going to need a copy of that. My recommendation is to use plenv to set up a custom perl to work with. Go follow Tokuhiro-san’s instructions on getting plenv setup.

Once you have plenv set up, you’ll need to create a directory to work in. On my machine we’ll go with $HOME/dev/possessive_frogs (I used github’s random repo name). You can name yours whatever you’d like.

gh repo create --public --clone possessive_frogs
cd posessive_frogs
echo '5.38.0' > .perl-version
plenv install $(cat .perl-version)
plenv install-cpanm

Those last three lines create a .perl-version file to let plenv know which version of Perl we’re expecting to use, then tell plenv to install that version of perl. We want the most recent version, at this writing 5.38.0. Finally, we have plenv install cpanm, a simple CPAN client.

Now we need to install the game library. At this time, that library isn’t on CPAN, but it is on github:

cpanm https://github.com/perigrin/perl-games-rot.git

This will install the Games::ROT library and its dependencies. They’re minimal so it should “just work”. That’s the prep work done, let’s write some code and see if everything works!

The Code

The new class feature only exists in perl with versions 5.38.0 or higher, so let’s start a new file game.pl and set a minimum version.

use 5.38.0;

Setting the minimum has a side effect of enabling strict as well as a bunch of other features (in this case); say, state, unicode_strings, unicode_eval, evalbytes, current_sub, fc, postderef_qq, bitwise, isa, signatures, and module_true. Check the documentation for the feature pragma for more on what each of these do.

Next we’ll need to enable the experimental class feature. We explicitly enable it with the experimental pragma.

use experimental  'class';

The class feature is experimental because it’s still in development and may change. The experimental pragma has been bundled with perl since 5.20.0 (2014) and it enables experimental features while disabling the warnings they generate for being experimental.

We use Games::ROT;, bringing in the game utilities library. Then comes the new stuff. Let’s start by declaring a new Engine class to represent our game engine.

class Engine { ... }

The class keyword declares a new package which is intended to be a class. It also enables all of the other new keywords within its scope. Keywords like the field keyword, which introduces state variables into the class. Let’s add a $height and a $width field so our game has a definitive screen size.

class Engine {
    field $height :param;
    field $width :param;
}

These variables are lexically scoped and access the data in the underlying object. With the new class feature, objects are no longer guaranteed to be any specific kind of blessed data reference, they’re intentionally opaque to help promote encapsulation. It’s best to just treat the data as a bundle of lexically scoped variables.

This default toward encapsulation is different from existing Perl object systems. Moose, Moo, and even the built in blessed data structure all end up promoting very public APIs to access all of your state. The class feature doesn’t provide anything by default, certainly not in this early version. You must explicitly declare which fields become parameters to the object constructor, hence the :param attribute on the $height and $width fields. This is why at the bottom we can pass width => 80 and height => 50 to Engine->new(). If we didn’t include them we would get an exception about an required parameter missing. And if we include something that the class doesn’t know about, we get an error about unrecognized parameters.

 class Engine {
    field $height :param;
    field $width :param;

    field $app = Games::ROT->new(
        screen_width  => $width,
        screen_height => $height,
    );

    [...]
}

Let’s leverage that encapsulation by having an $app field to hold our Games::ROT utility. We can assign it right in the declaration and this assignment is evaluated every time the constructor is run. We don’t want users of this class to be able to mess with the Games::ROT instance, so we don’t have a :param attribute on the field and we don’t define any methods that let us change it or even read it directly.

Having the Games::ROT object is great but it doesn’t really get the game started. Games::ROT has a run method that takes a callback for the game run loop, so we could write something like this:

method run() { $app->run( sub { $self->render() } ) }

And then explicitly call $engine->run() later when we create an instance of our engine class. But, I’d like the Engine to start running when it’s created:

class Engine {

    [...]

    ADJUST {
        # start the app
        $app->run( sub { $self->render() } );
    }

    [...]
}

ADJUST blocks allow post-construction adjustments to the object. In this case, we tell Games::ROT to run our main game loop: the render method.

method render() {
    my $x = $width / 2;
    my $y = $height / 2;

    $app->draw($x, $y, 'Hello World', '#fff', '#000');
}

For now let’s just render something simple to the middle of the screen to make sure that everything works.

Finally we need to create our game engine object and kick things off:

my $engine = Engine->new(
    width => 80,
    height => 50
);

All together it should look like this:

#!/usr/bin/env perl
use 5.38.0;
use experimental  'class';

use Games::ROT;

class Engine {
    field $height :param;
    field $width: param;
    field $app = Games::ROT->new(
        screen_width  => $width,
        screen_height => $height,
    );

    ADJUST {
        # start the app
        $app->run( sub { $self->render() } );
    }

    method render() {
        my $x = $width / 2;
        my $y = $height / 2;

        $app->draw($x, $y, 'Hello World', '#fff', '#000');
    }
}

my $engine = Engine->new(
    width => 80,
    height => 50
);

If we save our file and then run carton exec perl game.pl, we should get Hello World printed nicely in roughly the middle of our terminal.

Tune in for Part 1, where we turn this hello world into something moving.

Thanks to @tmtowtdi@mastodon.social for pointing out a capitalization bug in render().