Model View Events with Action Replay — No Frameworks Allowed

(Originally published in December 2020 – moved here from another blog site that I had)

In a previous blog post I mused on playful creativity and innovation. I reflected on the software industry from what I have seen in my career so far. I talked about how companies can become overburdened with processes and bureaucracy. It gets to the point where getting ideas from our heads into working software in the hands of our users becomes a real chore. We can forget how to play and innovate becuase of the walls of process that get in our way.

Over the Christmas holidays, I got an itching for some playful creativity. To put it bluntly, I just wanted to fire up a text editor and knock out some code to build something! I looked into the world of endless possiblilities that was the blank text buffer in front of me. I thought to myself “Hello Javascript, my old friend, its been a while!” so lets build some small browser game. I’m also quite partial to event driven systems. I love capturing actions that can be assembled into a state at a later point. The game, Tik Tak Toe, came to mind. I also remembered being at a conference a few years ago and seeing a talk about one of the many Javascript frameworks out there. The speaker showed time-travel debugging and the ability to “replay” an application up to a certain point by replaying all the events up to that point. So I thought “sure lets make Tic Tak Toe with action replay!”. Then my kids can play it and replay games to see the moves that lead up to the win!

So, I was off to the races. I had a blank text buffer in front of me. I was sitting on my comfortable arm-chair with my coffee in hand. My kids were busy playing a game that they had just concocted from their imagination. By the way, one thing that has thought me a lot about playful innovation is seeing the games that my kids invent for themselves involving different characters and role-plays. It’s an amazing thing!

One might ask which framework did I use? How did I narrow it down from the multitude of Javascript frameworks out there that can manage events and state and show stuff on the screen? I’ve used many frameworks over the years both server-side and client-side. They’re handy. They help get stuff done without having to build, say, a web server from scratch. But, let’s face it, they can be a complete pain!Granted, small libraries are often needed but a fully fledged framework the likes of Angular, React or Spring Boot is generally not needed until a project reaches a certain size.

I wanted to show some boxes on the screen, allow the users to enter in Xs and Os and capture everything that happens so that it can be replayed later. Most of this could nearly be achieved in the time taken for NPM to download Angular or some other framework and for it to be all bootstrapped into a project. No, I was going commando on this one — an html file and, in the end, 2 Javascript files.

But surely I used NPM to download and use some Javascript testing framework and got it all hooked up with tasks to run my tests? Nah, I just wrote a test module in the same Javascript file as the code I was testing. Then I could run my tests by starting a node repl and loading in that file.
The main logic is in checking for a win or a game that has completed with no winner. I extracted this into a separate Javascript file and added tests for it in there too. A sippet of this is:

const tests = () => {
    let testsPassed;
    let gameboardWithNoCompletedRowsOrColumnsButAllCellsCompleted = [
        ['X','O', 'X'],
        ['O','O', 'X'],
        ['O','X', 'O']
    ]

    testsPassed = !winChecker.thereIsAWinner(gameboardWithNoCompletedRowsOrColumnsButAllCellsCompleted);
    console.log('Test thereIsAWinner gameboardWithNoCompletedRowsOrColumnsButAllCellsCompleted ' + testsPassed);

    testsPassed = winChecker.gameBoardIsFull(gameboardWithNoCompletedRowsOrColumnsButAllCellsCompleted);
    console.log('Test gameboardIsFull gameboardWithNoCompletedRowsOrColumnsButAllCellsCompleted ' + testsPassed);

    let gameboardWithCompletedRow0 = [
        ['X','X', 'X'],
        ['O','O', 'X'],
        ['O','X', 'O']
    ]

    ....
    ....
    ....

So going commando on this meant that I needed some simple way to dispatch and subscribe to all the events that correspond to actions on the game. For example, these include:

winner-determined-event
completed-with-no-winner-event
game-board-updated-event
turn-taken-event

with each event also holding data related to it e.g. the player that took their turn in the turn-taken-event. I took care of this in a module I called eventDispatcher as follows:

let eventDispatcher = function() {
    let replaying = false;
    let events = [];
    let subscribers = {};
    let dispatchEvent = (ev) => {
        if (!replaying) { 
            events.push(ev);
        }

        for (const sub of subscribers[ev.name]) {
            sub(ev);
        } 
    }

    let subscribe = (eventName, f) => {
        if(subscribers[eventName]) {
            subscribers[eventName].push(f);
        } else {
            subscribers[eventName] = [f];
        }
    }

    let replay = () => {
        replaying = true;
        let i = 1 
        for (const ev of events) {
            setTimeout(() => dispatchEvent(ev), 1000 * i);
            i++;
        }
        setTimeout(() => replaying = false, 1000 * events.length)
    }

    return {
        dispatch: dispatchEvent,
        subscribe: subscribe,
        replay: replay
    }
}();

This module also handles storing and replaying events. The storing and replaying of events should really be moved out to another appropriately named module called ‘eventStore’. But, come on, give me a break, I was having some fun! Subscribers are stored in a simple hash keyed by event names. The events themselves are stored by pushing them into an array. The replay mechanism can then process this array of events and dispatch each one to replay it. The dispatch mechanism simply calls the subscrbers — which are functions — for the event being dispatched.

Following a CQRS’ish (Command Query Responsibility Segregation) type design, the core of the game doesn’t store end-state but just stores the events. However, we need boxes with X’s and Os to be drawn on the screen, user clicks/taps to be accepted or the “Action Replay” button to be pressed, along with other data being displayed. For this, there is a view module which uses its own ongoing record of the state which it tracks based on events that it responds to from the eventDispatcher. This is the “query” part of this CQRS’ish design. A snippet from the start of the view module is below. It subscribes to the various events of the system and updates its own view model accordingly and re-draws the screen.

let view = function() {
    let viewModel = {
        currentPlayer: 'X',
        winner: null
    };

    let gameBoard = new Array(3).fill(new Array(3).fill(''));

    eventDispatcher.subscribe('game-board-updated-event', gameBoardUpdatedEvent => {
        viewModel.currentPlayer = gameBoardUpdatedEvent.nextPlayer;
        gameBoard = gameBoardUpdatedEvent.gameboard;
        drawBoard(gameBoard);
    });

    eventDispatcher.subscribe('winner-determined-event', winnerDeterminedEvent => {
        viewModel.winner = winnerDeterminedEvent.winner;
        gameBoard = winnerDeterminedEvent.gameboard;
        drawBoard(gameBoard);
    });

    eventDispatcher.subscribe('completed-with-no-winner-event', completedWithNoWinnerEvent => {
        viewModel.winner = 'NO_WINNER';
        gameBoard = completedWithNoWinnerEvent.gameboard;
        drawBoard(gameBoard);
    });

    document.getElementById('replay-button').addEventListener('click', () => {
        viewModel = {
            currentPlayer: 'X',
            winner: null
        };

        let gameboard = new Array(3).fill(new Array(3).fill(''));
        drawBoard(gameboard);
        eventDispatcher.replay();
    });

    const drawBoard = (gameBoard) => {
        let heading = '';
        if (viewModel.winner === 'NO_WINNER') {
            heading = 'Game completed with no winner';
        } else if (viewModel.winner) {
            heading = viewModel.winner + ' has won!!'  
        } else {
            heading = 'Current Player is: ' + viewModel.currentPlayer;
        }
        let headingView = document.getElementById('heading');
        headingView.textContent = heading;

        let gameBoardView = document.getElementById('gameBoard');
    ...
    ...
    ...

And, yes, that gameBoard variable should be part of the viewModel object — something to be refactored a bit. The rest of the view module just contains code to draw the rows and the cells. It also attaches a click handler to each cell:

  cellView.addEventListener('click', 
        () => gameEngine.handleEvent(
            {
                name: 'turn-taken-event',
                player: viewModel.currentPlayer,
                row: rIndex,
                col: cIndex, 
                gameboard: gameBoard
            }));

The main engine for the game decides what to do for each turn-taken-event. It’s in a module called gameEngine. It checks to see if the game has been won or completed and dispatches events accordingly. It’s main logic is below:

let handleTurnTaken = (turnTakenEvent) => {
    if(turnTakenEvent.gameboard[turnTakenEvent.row][turnTakenEvent.col] !== 'X' && turnTakenEvent.gameboard[turnTakenEvent.row][turnTakenEvent.col] !== 'O') {

        console.log('Player ' + turnTakenEvent.player + ' has taken a turn');
        let nextPlayer = turnTakenEvent.player === 'O' ? 'X' : 'O';
        let nextGameBoard = turnTakenEvent.gameboard.map(
            (row, rIndex) => {
                return row.map((col, cIndex) => {
                    return rIndex === turnTakenEvent.row && cIndex === turnTakenEvent.col ?
                        turnTakenEvent.player : col 
                });
            }
        ) 

        if (winChecker.thereIsAWinner(nextGameBoard)) {
            eventDispatcher.dispatch({
                name: 'winner-determined-event',
                winner: turnTakenEvent.player,
                gameboard: nextGameBoard
            });
        }
        else if (winChecker.gameBoardIsFull(nextGameBoard)){
            eventDispatcher.dispatch({
                name: 'completed-with-no-winner-event',
                gameboard: nextGameBoard
            });
        } else {
            eventDispatcher.dispatch({
                name: 'game-board-updated-event',
                nextPlayer: nextPlayer,
                gameboard: nextGameBoard 
            });
        }
    }
}

The full code for the game is just 3 files and can be found on my github: https://github.com/priort/tic-tak-toe-with-action-replay
If you clone the repo and open index.html in a browser, you should be good to go. I’ve only tried it out using the latest version of Firefox so I don’t know if I have full cross-browser support just yet!

It was a lot of fun knocking this out. When I clicked the “Action Replay” button and saw all the events being replayed a second apart, I got that thrill that I first got when I wrote a simple program in Pascal back in my college days to render lines on a screen at increasing angles. I get even more of a thrill when I play the Tic Tak Toe game with my kids and watch as they hit “Action Replay” to see all the moves of each game. Creating something from nothing and then seeing or hearing about users’ joy in using that creation is one of the main reasons I love programming. It was nice to rekindle the joy of programming again over the holidays!

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

%d bloggers like this: