From an idea to a working game in 7 days — part 3

I had a map, I had enemies, everything was moving — but in order to reach my goals I needed more features and, in turn, more code. What could I do to keep everything loosely coupled and with clear responsibilities, so that I didn’t have unforeseen consequences when refactoring?

My iterative approach on development proved beneficial: I had not optimized prematurely, but, instead, refactored my classes as I needed them to be for a new feature.

But I had now reached a point where everything became a bit unwieldy. Originally, I had most of my game logic as part of the class that formed an individual level. That made sense, since it was the logical place to keep all information: a level knew about the player object, the enemies and the map itself — and there wasn’t much more to the game than that until now.

Controlling the flow

All this changed when I began dealing with the yet to be implemented features: an overall game structure and the combat system. Suddenly, the individual level was no longer the centre of my game.

I needed a place, or class, to hold my game state. This class should have information about all objects in order to be able to send messages to them. In turn, none of the other individual objects (like player or enemy class instances) should need to know about what comprises a level or the game itself.

I created a GameEngine class, whose job it was to manage the game and all moving parts. It had references to all important objects and implemented what I called an Action Controller.

The Action Controller was a First-in-first-out stack that would store and execute all commands that could be triggered by a game event (like the player using a skill or the game requiring an enemy to act).

class ActionController
attr_reader :queue
  def initialize
@queue = []
end
  def enqueue(command, param = nil)
@queue << {command: command, param: param}
end
  def execute(engine)
until @queue.empty? do
action = @queue.shift
#
# call appropriate object methods for each action
#
end
end
end

This solution kept my code from becoming too dependent on all the moving parts. I started adding features like a combat system and skills that the player could use. All those systems were simple class-based components that would communicate through a single interface: the GameEngine class and its Action Controller queue.

The beauty of “fundamentals first”

It’s no big secret that I absolutely love Launch School’s idea of teaching strong fundamentals. Having a good understanding of Object-Oriented Programming with Ruby, it came natural to use the paradigm to my advantage. Take, for example, the game’s effects system. I wanted to add some spice to the combat, and decided to implement effects that could befall a monster: it could be poisoned (taking damage over time), or stunned. To be able to create many different effects without much effort, I relied heavily on the concept of polymorphism:

class Poison < Effect
def initialize(rounds, dmg)
@dmg = dmg
super(
name: ‘Poison’,
rounds_remaining: rounds,
description: ‘Unit takes damage over time.’,
adjective: ‘poisoned’
)
end
  def effect(unit)
unit.take_dmg(@dmg)
end
end

Here’s the Poison effect class as an example. Whenever it’s a monster’s turn, it cycles through all active effects. It only needs to call the effect method on each of its active effects and pass itself along in order to execute each effect’s special properties. And every effect can use the whole range of possible public methods available to the enemy class.

The road goes ever on and on …

My week was nearing its end, I had spent about 30 to 35 hours so far, and I finally managed to reach my goal: I had a playable game, in which you could control a hero, slay monsters, collect new skills and climb deeper down into the unknown. If you managed to survive the 10th floor, you would win the game.

If you haven’t tried the game yet, you can find the repository on Github. Follow the installation instructions in the Readme and you can play the game in your browser.

Am I satisfied with the result? Yes… and no. I keep having great ideas for new and exciting features, there are still bugs to be squashed, the game needs a serious amount of balancing to make it fun, and the code smells in more than one place. I’m definitely going to return to it when I gain more proficiency and frontend knowledge.

Even looking at the features I have, numerous problems remain: the Cellular Automata-based map generation is still prone to generate some unusable maps. One feature I’d love to add is the use of my pathfinding algorithm to find out if all cells are connected and if the overall size of the level is suitable for play.

The pathfinding algorithm itself can be improved substantially: adding heuristics in order to implement Dijkstra’s algorithm would yield better results. Implementing a cache, that would prevent the algorithm to calculate the same position twice, would improve the performance.

Performance in general is the current code’s crucial flaw: since every action results in a full HTTP request with a complete page rerender in the browser, every millisecond of latency makes the game feel more sluggish. Changing the frontend to use AJAX requests to only rerender what’s necessary is something that I could tackle once I reach the JavaScript portion of Launch School’s Core Curriculum.

But all its problems aside: I had successfully managed to tie together 2,000 lines of code and still have a system that I could easily refactor or add to. Even without any tests (it was supposed to be fun, remember?), debugging was mostly easy and there were almost no unexpected side effects when I changed something. Plus, it was satisfying to see that Launch School has taught me enough to be able to put together a bigger project, to not fear algorithmic thinking, and to trust my Ruby knowledge to create whatever I want.

This project would have been completely impossible just a few months ago, and I owe it to Launch School’s superior pedagogy that I could now turn an ambitious idea into reality.

And I had a memorable christmas vacation.