A Roguelike in Common Lisp, Part 2

This is the second part in an ongoing series about building a roguelike game in Common Lisp. The first part is here.

This corresponds to Part 2 in Trystan Spangler’s tutorial and in Steve Losh’s version.

The code for this part is available here.

Summary

We’ll talk a bit about the structure of the game: the game loop, scenes, and rendering.

We will set up four different “scenes” to the “game”, and have a way to “win” and “lose”. At the end of this section, the game will look like this:

This slideshow requires JavaScript.

The Game Loop

Virtually every video game has a simple loop that goes like this:

setup()
while not game_over:
  render(game_state)
  input = get_user_input()
  update(input, game_state)

Each pass through this loop is called a frame. Most games accept input asynchronously, but roguelikes are synchronous games and work very simply. They render the game state, then wait for the user to press a single key, change the game state based on that input, then they loop.

Depending on what state the game is in, the same input may have different results. During normal play, pressing the up key may move your character up one tile. On the title screen, it may select from a different item, or be the first character in a cheat code.

To keep these very different sets of display logic and state update logic separated, we introduce a concept called Scenes.

Scenes

Scenes are another fairly universal concept in game development. Usually, a game will have a title screen scene, a game over scene, a gameplay scene, maybe a map, etc.

In our program, like Trystan’s, we use objects to represent the scenes. We use generic methods to dispatch display and input handling functions.

The Code

Game Loop

The outer loop of our program looks like this:

(defun gameloop ()
  (let* ((window charms:*standard-window*)
	 (scene  (initialize)))
    (loop named :game-loop
       while *running*
       do (progn
	    (charms:clear-window window)
	    (display-scene scene window)
	    (setf scene (handle-input scene (charms:get-char window :ignore-error t)))))))

(defun main ()
  (charms:with-curses ()
    (charms:disable-echoing)
    (charms:enable-raw-input :interpret-control-characters t)
    (gameloop)))

main is the entry point into our program. There is a little bit of cl-charms ritual, but it mostly just sets up,gameloop which handles the actual loop as described above.

At a high level, gameloop calls initialize to set up the initial state of the game. Then, it calls display-scene and handle-input in a loop until the game is over.

initialize

initialize will be responsible for the initial setup of the game state. It also returns the initial scene for the game. For now, the only state of the game that we’ll store is whether or not the game is running so we can know when to terminate the game loop. We store this in a defparameter called *running*.

The initial scene of the game is a class we’ve called start-scene. We will describe each of our scenes, and the methods available to them, below. We expect initialize to set up the game’s initial scene and return it.

(defparameter *running* nil)

(defun initialize (window)
  (setf *running* t)
  (make-instance 'start-scene))

display-scene

display-scene is a defgeneric that takes a scene (the current one, usually) and a cl-charms window and renders that scene to the window.

handle-input

handle-input is a defgeneric which takes in the current scene and applies input (a keystroke) to it. It is expected to return a modified scene instance, which will become the new scene.

For now, all the implementations of handle-input either return themselves, unmodified, or an entirely new scene. In the near future, we will store more state in the scene, and this data will be updated by handle-input.

Scenes

The scenes in our program are represented as a class hierarchy. The topmost is simply start-scene. Our interface is two defgenerics, display-scene and handle-input.

For now, the scene has no slots. In the future, perhaps we will use the scene to hold the game state.

(defclass scene () ())

(defgeneric display-scene (scene window))

(defgeneric handle-input (scene key))

We will have four specialized scenes in our game:

  • A title scene, called start-scene, which will explain the game, display copyright info, etc.
  • A gameplay scene, called play-scene, which will show our dungeon map, our player, and the game as it is being played.
  • A gameover scene for having won the game called win-scene
  • A gameover scene for having lost, called lose-scene

start-scene uses display-scene to display a rudimentary title screen. It has two valid input keys: Space, which starts a new game by initializing a play-scene; and Q, which will terminate the game by SETFing *running* to nil.

(defclass start-scene () ())

(defmethod display-scene ((s start-scene) window)
  (draw-string window 0 2 "        CORYS ROGUELIKE")
  (draw-string window 0 4 "    Press [Space] to start")
  (draw-string window 0 5 "       Press [Q] to quit"))

(defmethod handle-input ((s start-scene) key)
  (case key
    (#\Space (make-instance 'start-scene))
    (#\Q (progn
	   (setf *running* nil)
	   s))
    (otherwise s)))

play-scene will soon be the meat of the game, but for now, it just shows a message and the “game” consists of pressing either Escape to win (by switching to a win-scene) or Enter to lose (by switching to a lose-scene).

(defclass play-scene () ())

(defmethod display-scene ((s play-scene) window)
  (draw-string window 0 0 "          You are having fun.")
  (draw-string window 0 1 "-- press [Esc] to lose or [Enter] to win --"))

(defmethod handle-input ((s play-scene) key)
  (case key
    (#\Escape (make-instance 'win-scene))
    (#\Newline (make-instance 'lose-scene))
    (otherwise s)))

win-scene and lose-scene both simply display a message and wait for the play to press Enter to instantiate and return a new start-scene.

(defclass win-scene () ())

(defmethod display-scene ((s win-scene) window)
  (draw-string window 0 0 "         !! YOU WIN !!")
  (draw-string window 0 1 "-- press [Enter] to restart --"))

(defmethod handle-input ((s win-scene) key)
    (case key
      (#\Newline (make-instance 'start-scene))
      (otherwise s)))

(defclass lose-scene () ())

(defmethod display-scene ((s lose-scene) window)
  (draw-string window 0 0 "           You lose.")
  (draw-string window 0 1 "-- press [Enter] to restart --"))

(defmethod handle-input ((s lose-scene) key)
  (case key
    (#\Newline (make-instance 'lose-scene))
    (otherwise s)))