Erlang: Writing a Tetris clone Part 2 – Gameplay mechanics

Last night I was able to make the second video in my series about implementing a Tetris clone in Erlang. Yay!

In this video I start where I left off in the first video where I had ended with a data abstraction to represent the play field (called the “well” in Tetris lingo), a data abstraction for the game pieces, some colored sprites to draw the game board with, and a GUI that could draw a game board and a single random piece every time it was opened as well as print to stdout any key press events that I made.

Oh but by the way…

I have a small confession to make — I didn’t actually start where I left off. I mentioned the stopping point (the “Draw the board with a random piece on it” commit), then I mentioned the next thing I did which was implement basic movement (the “Basic (unsafe) movement” commit), then I completely blew past it and never explained the way I implemented unsafe movement in the first place. The trouble with having skipped that is that I had intended to discuss how keystroke capture actually works, where it is in the GUI code, and follow the event through the system so that people could get that idea into their head earlier than later because it is so basic to making a game!

So instead I’ll explain how that works and point out where it is in the code here in this post and cover it at the beginning of the third video.

Getting Input

If we look at the “Basic (unsafe) movement” commit there is a file called ertltris/src/et_gui.erl that is the code for the GUI process. In the init/1 function we see the wx server get started, a “frame” is created (the main window in wx parlance), some various widget elements and things are all established and on line 112 we see this:

ok = wxPanel:connect(Frame, char_hook),

This is connecting the Frame to a window manager event called char_hook. I do mention this in the video, but it is important to point this out here. I should also point out that I’m mistakenly calling wxPanel:connect/2 instead of wxFrame:connect/2 — which is technically incorrect, but due to the nature of the underlying inheritance among the C++ classes that make up wx and the way that’s all masked in the generated library wrappers that make up wxErlang, it actually doesn’t cause any errors. wxPanel and wxFrame are all ancestors of wxEventHandler.

Anyway… what connecting to this event does is tells wx that whenever the frame is the focus it should relay keystroke events to the program. Inside Erlang they arrive as messages that carry a #wx{} record that includes an event element that carries another record that provides all the relevant information about the event. You can receive these in the handle_event/2 callback function of wx_object. Pretty nice. This means you can deal with GUI events in very much the same way you can handle network events as well as inter-process messages within the Erlang runtime: everything is a message.

IDIOMS IDIOMS IDIOMS!

As I often say: Develop idioms.

A common idiom I use in wxErlang code is to match on an event type in handle_event/2, assign any relevant event data to variables, and then within a case inside the handle_event clause that matched that event type determine where we want to dispatch the event (if at all). You can see a very clear example of this on lines 191-203 of this commit.

handle_event(#wx{event = #wxKey{keyCode = Code}}, State = #s{frame = Frame}) ->
    ok =
        case Code of
             32 -> et_con:random_piece();
             88 -> et_con:rotate(l);
             90 -> et_con:rotate(r);
            314 -> et_con:move(l);
            315 -> et_con:rotate(r);
            316 -> et_con:move(r);
            317 -> et_con:move(d);
            _  -> tell("KeyCode: ~p", [Code])
        end,
    {noreply, State};

I could have made each one of those dispatch decisions inside the function head instead of using a case statement within a clause, but I find it much easier to read this particular style where we open a very compact dispatch span per event type we’re looking for than have a ton of handle_event clauses. In complex GUI applications you can wind up with a lot of special keystroke events and it becomes super cumbersome to match them all as function heads. Not just writing the function heads, but trying to find a specific event gets kind of messy because the code starts looking so scrambled.

Each of the different key codes matched in the dispatching part of that function correspond to some gameplay event. Note that the GUI doesn’t really care what the status of the game is at all, it just cares that it is relaying the mapped commands to the game control process and then carries on doing GUI stuff.

This is important to point out: Nearly all communication between the controller and the GUI is asynchronous. You almost never want blocking calls to pass between them. There may be a need for a blocking call in some special code, but this is almost always a bad idea.

Buh bye!

That pretty much sums up what I wanted to cover that I forgot to mention in the video. Input handling is really important. If you want to jump ahead, check out the way the handle_event/2 function evolved in the latest commits to see how menu commands are intercepted, or go check out the handle_event/2 function in the Erlang Minesweeper clone!

Don’t forget to give me all the delicious likes and stars and channel subs! BWAHAHA! I’ll catch you magnificent nerds in the next video!

11 thoughts on “Erlang: Writing a Tetris clone Part 2 – Gameplay mechanics

  1. Hello Mr. Craig Everett
    Erltris header files on the Gitlab are missing (-include(“$zx_include/zx_logger.hrl”).
    Anyhow great mathematical and smart coding

    1. Hi! Sorry, I have to approve first comments — I get over a thousand spam attempts every day.
      The program is based on ZX, so it is expected to be launched by ZX specifically. ZX creates the environment that knows how to path complete for the includes.
      Here is the install information: https://zxq9.com/projects/zomp/qs.install.en.html
      ZX is basically a dynamic develop/package/build/launch tool system that makes things a bit less insane than forcing everything to be a release.

  2. Hello Mr. Everett
    This reply still does not indicate where header file is!
    The directory is zx_include
    The header zx_logger.hrl
    Now what zx does and how it’s set up it is not so important.
    I am following the code and want to be able to run the program but not through zx.
    If you can please just the header file, it is missing!!!

    1. > I am following the code and want to be able to run the program but not through zx.
      That’s the problem.

      ZX is the launching harness this was written with and every assumption about its execution environment is based on that. Without ZX there is no zx_logger because zx provides that. You can remove that include and write your own log/1,2,3 function (and there might be a tell/1,2,3 in there somewhere, but those can be converted to log calls also) and the thing should work — but you’re really fighting against the current for no gain, especially if you want to edit the code and play around with it (the ZX development pattern is “edit code” -> zx runlocal -> “edit code” -> zx runlocal.

      Launching OTP applications is a non-trivial environment setup problem, which is why releases exist, rebar3 was written, relx was written, reltools exist, etc. ZX was written to solve the same set of problems but without requiring that everything be a release (especially since client-facing code shouldn’t be release based anyway). ZX is sort of like PyPI + Virtualenv but for Erlang. Fortunately Erltris has no dependencies, so you should be able to run it without ZX if you just implement a log function (or remove all the logging functions — just delete the logging lines).

      I recommend giving ZX a shot, though, because it is just easier in every way, and makes writing new GUI apps way less painful (it will template WX apps for you).
      wget -q https://zxq9.com/projects/zomp/get_zx && bash get_zx
      From there you can do any of:

      • zx run erltris
      • zxh run erltris (zxh gives you an Erlang shell)
      • Run it from the GUI app browser: zx run vapor
      • Or run it from the repo: git clone https://gitlab.com/zxq9/erltris.git && cd erltris && zx runlocal

      If you use the last option there, you are on the easy path to forking and doing whatever you want with it (and it will also remain easy to package for Windows in that case).

      If you don’t like ZX, do rm -rf ~/zomp and it will be completely removed. ZX is written in Erlang and doesn’t do anything weird to your system — it was specifically written to avoid the “I installed a weird thing and now I don’t understand what happened to my home environment” problem.

      1. Thanks for reply
        I just realized that Erlang is so similar to prolog. (I use winprolog)
        My goal is to continue programming in Erlang without dependency
        to zx.
        However I think your code is structural and logical.
        Thanks..

        1. Sure. There is no problem with using ZX or not using ZX — it is just in the context of this particular project that does depend on ZX that it is much easier to go with the flow and use it than not (and much easier to use ZX if you intend to do any client-facing coding in general). Most backend systems are still based on releases and use rebar3 along with some assortment of CI or similar tools. ZX is just a convenient way to play with code quickly so you don’t get lost in the maze of release-related madness!

          Have fun coding! You’ll find Erlang to be one of those special languages that you start appreciating the “weirdness” of more rather than less as you gain experience (as opposed to most languages where the more you learn about them the less attractive the design choices start to seem).

          1. Hi,
            Running , I get this error;
            108> {ok, State#s{well = NewWell}}.
            =CRASH REPORT==== 18-Feb-2022::18:32:12.845000 ===
            crasher:
            initial call: et_gui:init/1
            pid:
            registered_name: et_gui
            exception exit: {undef,
            [{zx_daemon,get_home,[],[]},
            how do I get zx_daemon run . I run from werl
            Also, Is there no other way to run this program without zx_daemon!
            Thanks…

          2. Ah! Right. This uses a facility provided by zx_daemon to discover where the actual home of the project is — it requires this because it has to be able to find its graphics files when everything starts up. The easiest way to do this on Windows would be to run the InstallVapor.msi and run it from Vapor (it is a GUI front-end for ZX). Windows is, unfortunately, just about the worst development platform possible for non-Microsoft languages, so without access to OSX or a Linux system, you’re going to have a few complications.
            I just made a video for you that shows a way to get around the Windows madness, but unfortunately for you, it does depend on ZX/Vapor to make everything work:

  3. Hello Mr. Craig Everett
    Finally I was able to run your code (with minor changes) under windows o.s. and it works just fine. I am not windows fanatic, it is just more popular!
    (I will not use your code for anything other than learning purposes)
    As I said before, your code is so structured and logical.
    By the way where can I ask you question about tuple of tuples , creation / retrieval of data.
    Thanks

  4. Hi,
    As I have understood, the advantage of using records instead of tuples is that fields in a record are accessed by name, whereas fields in a tuple are accessed by position
    If you have time (please), do you have a structured programming example:
    1. How to store information in a tuple of tuples, and how to extract information from the tuple of tuples.
    2. How to store information in a record. And how to extract information from the record.
    3. How to update in formation in tuple of tuples and in records.

    Thanks
    p.s. If you don’t have time, I understand.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.