Tetris Analyzer in Clojure - part 1

The last couple of weeks have been a whole lot of fun, and the reason is that I have started to learn Clojure! Clojure syntax is very different from languages like C or Java but if you give it a chance, you will soon realize that the language and the ideas behind it, are actually very powerful and elegant!

This is the first of a series of blog posts where I'm going to implement a program in Clojure that can play Tetris. I have already made different versions of Tetris Analyzer in other languages with focus on performance, but the Clojure version will prioritize readability and simplicity.

In this and the next blog post, we will reach the first goal to put a piece on a board (represented as text):


I will go through all the code in detail and no previous knowledge in functional programming is required. It's recommended, however that you read the previous post A short introduction to Clojure and optionally Going Functional where I describes what caught my interest in Clojure. It's also recommended that you have some experience in at least one other programming language!

On the one hand, this is not a course in Clojure, but on the other hand, I will do my best to explain all new concepts that are needed to understand the code. I have left out some basic concepts like immutability, homoiconicity, macros and the reader which may be introduced in future blog posts.

Project structure

The project is hosted at GitHub and has the following structure:
langs/clojure/unoptimized/src/tetrisanalyzer/core.clj
langs/clojure/unoptimized/test/tetrisanalyzer/core_test.clj
langs/clojure/unoptimized/project.clj
langs/cpp/...
langs/java/...
langs/scala/...
The file project.clj is used by Leiningen to build the project and seems to be the de-facto standard in the Clojure community. The core.clj file contains the source code of the program and core_test.clj contains the unit tests. I have used Light Table as development environment and Expectations to execute the unit tests (which runs in the background and executes the tests every time a test is changed).

I will not explain how to set up a development environment, but a good start is to take a look at this screen cast by James Trunk where he explains the basics about Clojure and how to get started with Light Table and TDD using Expectations.

The program

The program is stored in the file core.clj and I recommend you to take a brief look at it right now. In the first statement you'll find the namespace macro ns:
(ns tetrisanalyzer.core)

Namespaces are used to group code, just as packages are in Java:
package tetrisanalyzer;

You may noticed that the file itself (core.clj) is included in the namespace name. In object oriented languages like Java, you group "functions" as methods in classes. A more natural thing to do, in a functional language like Clojure, is to use the file itself as a placeholder for data and functions.

Comments

The next statement is a comment:
;; ===== Pieces =====

One way to write a comment in Clojure is to begin with a semicolon. A convetion is to write double semicolons if the comment stands by itselt and a single semicolon if it is preceded by a statement on the same row.
;; some code
(def x 123) ; my favourite value

...in Java you would write:
// some code
int x = 123; // my favourite value

Pieces

All pieces is stored in a multi dimensional vector:

(def pieces [ nil
  ;; I (1)
  [[[0 0] [1 0] [2 0] [3 0]]
   [[0 0] [0 1] [0 2] [0 3]]]

  ;; Z (2)
  [[[0 0] [1 0] [1 1] [2 1]]
   [[1 0] [0 1] [1 1] [0 2]]]

  ;; S (3)
  [[[1 0] [2 0] [0 1] [1 1]]
   [[0 0] [0 1] [1 1] [1 2]]]

  ;; J (4)
  [[[0 0] [1 0] [2 0] [2 1]]
   [[0 0] [1 0] [0 1] [0 2]]
   [[0 0] [0 1] [1 1] [2 1]]
   [[1 0] [1 1] [0 2] [1 2]]]

  ;; L (5)
  [[[0 0] [1 0] [2 0] [0 1]]
   [[0 0] [0 1] [0 2] [1 2]]
   [[2 0] [0 1] [1 1] [2 1]]
   [[0 0] [1 0] [1 1] [1 2]]]

  ;; T (6)
  [[[0 0] [1 0] [2 0] [1 1]]
   [[0 0] [0 1] [1 1] [0 2]]
   [[1 0] [0 1] [1 1] [2 1]]
   [[1 0] [0 1] [1 1] [1 2]]]

  ;; O (7)
  [[[0 0] [1 0] [0 1] [1 1]]]])

The variable pieces is defined by using the special form def. The first element at index zero has the value nil and corresponds to the undefined value null in Java. This value is put here to let the pieces start at index 1.

Tetris has seven different pieces, each made of four squares. A piece can be rotated 90 degrees a number of times until reaching the start position, resulting in these 19 combinations:


We can get the shape of Z (piece 2) in its start possition (index 0) by writing:
((pieces 2) 0)
;= [[0 0] [1 0] [1 1] [2 1]]

...the returned vector corresponds to this shape:
If we put rotation 0 and 1 in a vector, we get:
  ;; Z (2)
  [[[0 0] [1 0] [1 1] [2 1]]
   [[1 0] [0 1] [1 1] [0 2]]]

Maps

The next statement in core.clj is used to convert from a "piece character" to an index:
(def char->piece { \- 0 \I 1 \Z 2 \S 3 \J 4 \L 5 \T 6 \O 7 \x 8 \# 9 })

Commas are treated as white spaces as a way to enchance readability, and we might as well have written:
(def char->piece { \- 0, \I 1, \Z 2, \S 3, \J 4, \L 5, \T 6, \O 7, \x 8, \# 9 })

The statement defines a map with the name char->piece (-> tries to imitate an arrow). Hyphen and > (amongst others) are fully legal characters to be used in names.
Maps work as you would expect, except maybe that they are immutable just as all other data structures in Clojure.

We can "calculate" the index of a piece by writing:
(char->piece \Z)
;= 2

Clean code

The Clojure version:
(def char->piece { \- 0 \I 1 \Z 2 \S 3 \J 4 \L 5 \T 6 \O 7 \x 8 \# 9 })
(char->piece \Z)
...is much cleaner than the Java version:
Map<Character, Integer> charToPiece = new HashMap<Character, Integer>();
  charToPiece.put('-', 0);
  charToPiece.put('I', 1);
  charToPiece.put('Z', 2);
  charToPiece.put('S', 3);
  charToPiece.put('J', 4);
  charToPiece.put('L', 5);
  charToPiece.put('T', 6);
  charToPiece.put('O', 7);
  charToPiece.put('x', 8);
  charToPiece.put('#', 9);

  charToPiece.get('Z');
...and the latter makes me think of my first design principle:

If it's ugly - then it's wrong!

Functions

The next statement defines a function that converts from a piece index to a piece character:
(defn piece->char [piece] (nth "-IZSJLTOx#" piece))

The function is defined by the macro defn. A call to the function will delegate to the nth function and return the corresponding character, e.g.:
(piece->char 2)
;= Z

The java version could look something like this:
public char pieceToChar(int piece) {
    return "-IZSJLTOx#".toCharArray()[piece];
}

In Java we use the reserved word return to return a value from the method. That is not needed in Clojure since it's always the last statement of the function body that is returned, in this case the call to nth.

Unit testing

To better understand the purpose of the next statement, the definition of the function rotate-and-move-piece, we can have a look at the corresponding test in core-test.clj:
;; This function returns a list of "pairs": [y x] piece
;; that can be used by the function assoc
;; (via set-piece) to put a piece on a board.
;;
;;                         piece rotation x y
;;                         ----- -------- - -
;;  (rotate-and-move-piece    6      1    4 2)
;;
;;        123456           (6 = T)
;;    0  #------#
;;    1  #------#
;;    2  #---T--#   [2 4] 6
;;    3  #---TT-#   [3 4] 6 [3 5] 6
;;    4  #---T--#   [4 4] 6
;;       ########
(expect '([2 4] 6, [3 4] 6, [3 5] 6, [4 4] 6)
        (rotate-and-move-piece 6 1 4 2))

If you are familiar with any XUnit testing framework like JUnit, then the function expect corresponds to assertEquals where the first argument is the expected outcome and the second is the statement under test.

We have to put a ' in front of the expected list, otherwise it will be treated as a function call and we get:


This call failed because we used too many arguments. We can fix this by changing the number of arguments to one:
([2 4] 1)
;= 4

The map function

The first example in the map documentation looks like this:
(map inc [1 2 3 4 5])
;= (2 3 4 5 6)

The map function takes two arguments, in this example the function inc and the collection [1 2 3 4 5]. The function inc is applied to each element in the collection to generate the resulting list.

It's also possible to inline our own version of inc:
(map (fn [x] (+ 1 x)) [1 2 3 4 5])
;= (2 3 4 5 6)
...where the statement:
(fn [x] (+ 1 x))
...is an anonymous function defined by fn. There is also a shorter syntax for anonymous functions, that will be described in the next blog post!

The mapcat macro

The first example in the mapcat documentation looks like this:
(mapcat reverse [[3 2 1 0] [6 5 4] [9 8 7]])
;= (0 1 2 3 4 5 6 7 8 9)

If we use the map function instead, we get:
(map reverse [[3 2 1 0] [6 5 4] [9 8 7]])
;= ((0 1 2 3) (4 5 6) (7 8 9))
The difference between map and mapcat is that the latter concatenates the result into a single list.

rotate-and-move-piece

Now when we know more about anonymous functions and the mapcat macro, it's time to look at the next statement in core.clj:
(defn rotate-and-move-piece [piece rotation x y]
  (mapcat (fn [[px py]] [[(+ y py) (+ x px)] piece]) ((pieces piece) rotation)))

This statement consists of four parts:


Let's take a closer look at the function body:


The statement ((pieces piece) rotation) will return a shape of a pice, e.g. [[0 0] [1 0] [1 1] [2 1]]. The construct [px py] is destructuring each element in that vector into the variables px and py to be used by the anonymous function body.

If you are totally new to Clojure this was probably too much to take in! My recommendation is therefor that you install a development environment like Light Table and start playing around with the language. Another nice way to get started with Clojure is by following this 5 min online tour.

The resulting list of the call to rotate-and-move-piece can be used to put a piece on a board, which is the topic of the next blog post!

Best regards,
Joakim Tengsrand

0 comments

Post a Comment