The Emacs's Gamegrid library #2


Last time, I explained how to initialize the Gamegrid library.

This time, I'll show how to initialize the game buffer. Actually, this step should be performed after initializing the display, but that is a bit complex, so I believe it's better to proceed top-down. Thus, buffer initialization before display.

Starting the game

The function called to start a new game (the my-gamegrid-game-start-game function used in the previous article) is actually fairly standard: it needs to reset the game to its initial state, stop the game loop, set up the proper keymap, then start the game loop again.

As such, the function should look like this:

(defconst my-gamegrid-game-tick 0.5
  "Time interval between each updates.")
(define my-gamegrid-game-start-game ()
  "Start a new game."
  (unless (string= (buffer-name (current-buffer)) my-gamegrid-game-buffer-name)
    (error "To start a new game, switch to the `my-gamegrid-game-buffer-name' buffer."))
  (use-local-map my-gamegrid-game-mode-map)
  (gamegrid-start-timer my-gamegrid-game-tick #'my-gamegrid-game-update-game))

The unless check is used so that games can't be started while the game buffer is not current. It's not really obligatory, but it avoids some annoiances from a user experience point of view.

There is not really a need of a my-gamegrid-game-reset-game function, its body can be placed directly in the my-gamegrid-game-start-game function. However, if starting a new game requires some long and/or complex steps, having a separate function used specifically to reset the game state makes things more manageable.

The call to use-local-map changes the keymap from the “null” map containing only the “new game” and “bury buffer” functions, to the map containing all the bindings used to play the game.

gamegrid-start-timer is the function used to start the game loop. It is actually a fixed-length timer: the length of the interval is defined by its first argument. In our case, the length is 0.5 (0.3 seconds more than Snake). The higher the value, the longer the game “sleeps” between each update steps.

The second argument is the updating function. I'll explain it in a later article.

What still needs to be defined is the my-gamegrid-game-reset-game function. This function will perform the buffer initialization itself, plus anything needed to reset the game state. In the case of this series of articles, I don't have any particular requirements, so I'll put only the bare minimum.

(define my-gamegrid-game-reset-game ()
  "Reset the game."

Since the game is being restarted, the timer with the update function is stopped, then, the buffer is initialized.

Initializing the buffer

When Gamegrid initializes a buffer, it is actually defining a rectangular area in which it will define a grid. As such, it is required to provide the width and the height of this area. It's essentially the same as giving the window dimensions when programming with libraries like SDL or GTK.

Other than the dimensions, functions dealing with changing the buffer contents require a third argument: the role of this argument will be explained in more detail when talking about the display, but for now it should be thought of as an identifier.

The following function will initialize the buffer to display an empty space enclosed in walls:

(defconst my-gamegrid-game-buffer-width 16)
(defconst my-gamegrid-game-buffer-height 16)

(defconst my-gamegrid-game-empty 0)
(defconst my-gamegrid-game-floor 1)
(defconst my-gamegrid-game-wall 2)
(define my-gamegrid-game-init-buffer ()
  "Initialize the buffer."
  (gamegrid-init-buffer my-gamegrid-game-buffer-width
  (let ((buffer-read-only nil))
    (dotimes (y my-gamegrid-game-buffer-height)
      (dotimes (x my-gamegrid-game-buffer-width)
        (gamegrid-set-cell x y my-gamegrid-game-wall)))
    (let ((y 1)
          (wmax (1- my-gamegrid-game-buffer-width))
          (hmax (1- my-gamegrid-game-buffer-height)))
      (while (< y hmax)
      (let ((x 1))
        (while (< x wmax)
          (gamegrid-set-cell x y my-gamegrid-game-floor)
          (setq x (1+ x))))
      (setq y (1+ y))))))

The gamegrid-init-buffer function will initialize the grid. The dimensions are not pixels or characters, but the number of cells for each row and column. In this case, the grid will have 16 cells per row, and 16 cells per column.

Grid cells don't have a fixed size, and they are not required to have the same size either. It all depends on display initialization.

buffer-read-only is let-bound to nil because the game's major mode is derived from special-mode, which by default makes the buffer read-only.

After that, there are some simple loops to set up each cell with gamegrid-set-cell This function takes the cell coordinates (starting from 0) as the first two arguments, and the identifier introduced earlier as the last argument.

Thus, a call to (gamegrid-set-cell 3 5 my-gamegrid-game-wall) will place a wall at position (3, 5). Like other common graphics library, (0, 0) is the top-left corner of the buffer.

After calling my-gamegrid-game-init-buffer, the current buffer will be initialized. Calling gamegrid-set-cell with negative coordinates or with values higher than the ones passed as the first two arguments of gamegrid-init-buffer, will generate an error.