TetrisThis project walks you through writing a clone of the popular Tetris game. It uses the QT5 cross-platform GUI library for producing its graphics. This tutorial will teach you how to interface to a non-trivial foreign library and write a fun and interactive program with Stanza. DownloadsAll of the source files for the tutorial can be downloaded here. If you simply wish to play with the finished product, we also provide a prebuilt binary. Prebuilt Binariesosxtetris.dmg - for Macintosh OS-X Source Fileslinuxtetris.zip - for Linux osxtetris.zip - for Macintosh OS-X PreparationStanzaThis tutorial was written for Stanza 0.9.4 or higher. Please download that from www.lbstanza.org if you haven't already. QTThis tutorial also requires a 64-bit version of the QT5 library. Please download that from www.qt.io/download if you haven't already. CompilationDownload the source files for your platform from the links above and unzip them to a folder called tetris . To compile the project, first open the script/make.sh file and find the line that sets the QTDIR variable. QTDIR=/Users/psli/Qt5.6.0
Replace that line with the path to your own QT installation. By default, QT is installed in the user's home directory. Once that line has been replaced, open the terminal, navigate to the tetris folder, and type ./scripts/make.sh
If everything is in the proper place, and QT has been properly installed, then the script should produce a binary called tetris . Run it by typing ./tetris
and enjoy playing a game of Tetris! Use the left, right, and down arrow keys to move the block; use the up arrow key to rotate the block; and use the spacebar to drop the block quickly to the bottom. Once the game is over, you may press spacebar again to start a new game. Writing a C Interface to QTWe will using the cross-platform QT5 graphical user interface (GUI) library for displaying the graphics needed for Tetris. QT5 is written in the C++ programming language, and so the first thing that we will need to do is write bindings to it such that we may use QT from Stanza. In the future, a binding for QT will be provided as part of the Stanza libraries and this step can be skipped. If you wish to just proceed to writing Tetris you may skip ahead to the next section; otherwise, continue reading this section to learn about how to connect to a sophisticated foreign library. The .pro FileQT comes with its own build system and preprocessor called qmake and moc for implementing its signals and slots system. Create a file called tetris.pro containing the following. TEMPLATE = lib CONFIG += staticlib CONFIG += x86_64
QT += widgets
HEADERS = libqt.h SOURCES = libqt.cpp
The header file containing all of the definitions for our QT C bindings are in the file libqt.h and the source file containing their implementations are in the file libqt.cpp . Exposing C++ Functionality through C FunctionsThe Stanza foreign function interface can only connect to the C programming language, and so the very first thing that needs to be done is to expose QT's functionality through a collection of C functions. In the file libqt.cpp , the following include statements import the QT definitions that we're interested in writing bindings for. #include<QApplication> #include<QWidget> #include<QTimer> #include<QMouseEvent> #include<QKeyEvent> #include<QPainter> #include<QBrush> #include<QPen> #include "libqt.h"
We will write a C function to wrap over each C++ virtual function call, constructor call, and destructor call that we require. For example, the following functions wrap over the QPainter functionality required for Tetris. extern "C" { QPainter* QPainter_new (QWidget* widget){ return new QPainter(widget);} void QPainter_delete (QPainter* p){ delete p;} void QPainter_set_pen (QPainter* p, QPen* pen){ p->setPen(*pen);} void QPainter_set_brush (QPainter* p, QBrush* brush){ p->setBrush(*brush);} void QPainter_set_opacity (QPainter* p, float opacity){ p->setOpacity(opacity);} void QPainter_draw_line (QPainter* p, int x, int y, int x2, int y2){ p->drawLine(x, y, x2, y2);} void QPainter_draw_rect (QPainter* p, int x, int y, int width, int height){ p->drawRect(x, y, width, height);} void QPainter_draw_pixmap (QPainter* p, int x, int y, int width, int height, QPixmap* pixmap){ p->drawPixmap(x, y, width, height, *pixmap);} }
Notice especially that all the above definitions are wrapped in an extern "C" { ... }
block. This tells the C++ compiler that those functions are meant to be called using the C calling convention. The functionalities of the QApplication , QMouseEvent , QKeyEvent , QBrush , QColor , QPixmap , and QPen QT classes are wrapped up in an identical fashion to QPainter . extern "C" { QApplication* QApplication_new (int argc, char* argv[]){ return new QApplication(argc, argv);} void QApplication_delete (QApplication* x){ delete x;} int QApplication_exec (QApplication* x){ return x->exec();}
int QMouseEvent_x (QMouseEvent* e){ return e->x();} int QMouseEvent_y (QMouseEvent* e){ return e->y();}
int QKeyEvent_key (QKeyEvent* e){ return e->key();}
QBrush* QBrush_new (){ return new QBrush();} QBrush* QBrush_new_c (QColor* c){ return new QBrush(*c);} void QBrush_delete (QBrush* b){ delete b;} QColor* QColor_new (int r, int g, int b, int a){ return new QColor(r, g, b, a);} void QColor_delete (QColor* c){ delete c;}
QPixmap* QPixmap_load (char* filepath){ QPixmap* pixmap = new QPixmap(); int r = pixmap->load(filepath); if(r) return pixmap; else return 0; } void QPixmap_delete (QPixmap* p){ delete p;} int QPixmap_width (QPixmap* p){ return p->width();} int QPixmap_height (QPixmap* p){ return p->height();}
QPen* QPen_new (QColor* c, int thickness){ return new QPen(*c, thickness, Qt::SolidLine);} void QPen_delete (QPen* p){ delete p;} }
C Bindings for QWidgetThe QT functionality above was easily exposed as a collection of functions because they don't require the use of any callbacks. The functions are called with simple values and simple values are returned. However, the QWidget class implements much of its functionality through the use of overridden virtual functions. For example, every time a keyboard key is pressed when a widget is in focus, its virtual keyPressEvent method is called by the QT framework. We will expose this functionality by writing a custom QWidget class that calls a special listener function for each of the virtual methods we are interested in exposing. In libqt.h , include the following statements for importing the class definition we are interested in. #include<QApplication> #include<QWidget> #include<QTimer>
Here is the definition of our custom QWidget class. class StzQtWidget : public QWidget{ Q_OBJECT public: StzQtWidget(QWidget* parent); int width; int height; int listener; QSize sizeHint() const; protected: void paintEvent(QPaintEvent *event); void mousePressEvent(QMouseEvent* event); void mouseReleaseEvent(QMouseEvent* event); void mouseMoveEvent(QMouseEvent* event); void keyPressEvent(QKeyEvent *event); };
It contains the extra fields, width and height , for indicating the preferred size of the widget. The field, listener , is the box identifier of the listener object that will handle the virtual function events. We are interested in handling paint events, mouse events, and keyboard events. Here is the default constructor for our custom widget. It sets the default size, and we use -1 to indicate that there initially is no event listener. StzQtWidget::StzQtWidget(QWidget* parent) : QWidget(parent) { width = 100; height = 100; listener = -1; }
We override the definition of sizeHint to return the values of its width and height fields. QSize StzQtWidget::sizeHint() const{ return QSize(width, height); }
The following code exposes the functionality of the QWidget virtual functions by having them call externally defined listener functions. Here, we simply provide the definitions of the listener functions. Later we will write implementations for these listener functions in Stanza. extern "C" void QtWidget_paintEvent (int listener, QPaintEvent* event); extern "C" void QtWidget_mousePressEvent (int listener, QMouseEvent* event); extern "C" void QtWidget_mouseReleaseEvent (int listener, QMouseEvent* event); extern "C" void QtWidget_mouseMoveEvent (int listener, QMouseEvent* event); extern "C" void QtWidget_keyPressEvent (int listener, QKeyEvent* event);
void StzQtWidget::paintEvent(QPaintEvent* event){ QtWidget_paintEvent(listener, event); }
void StzQtWidget::mousePressEvent(QMouseEvent* event){ QtWidget_mousePressEvent(listener, event); }
void StzQtWidget::mouseReleaseEvent(QMouseEvent* event){ QtWidget_mouseReleaseEvent(listener, event); }
void StzQtWidget::mouseMoveEvent(QMouseEvent* event){ QtWidget_mouseMoveEvent(listener, event); }
void StzQtWidget::keyPressEvent(QKeyEvent *event){ QtWidget_keyPressEvent(listener, event); }
And, just as before, creation, deletion, and field accessors for our widget are exposed as a collection of C functions. extern "C" { StzQtWidget* QtWidget_new (QWidget* parent) { return new StzQtWidget(parent);} void QtWidget_delete (StzQtWidget* x){ delete x;} void QtWidget_show (StzQtWidget* x){ x->show();} void QtWidget_update (StzQtWidget* x){ x->update();} void QtWidget_set_width (StzQtWidget* x, int width){ x->width = width;} void QtWidget_set_height (StzQtWidget* x, int height){ x->height = height;} void QtWidget_set_listener (StzQtWidget* x, int listener){ x->listener = listener;} int QtWidget_listener (StzQtWidget* x){ return x->listener;} }
C Bindings for QTimerThe bindings for the QTimer class are written in an identical fashion to those for QWidget . In libqt.h we define our custom QTimer class. class StzQtTimer : public QTimer{ Q_OBJECT public: int callback; StzQtTimer(int callback); private Q_SLOTS: void tick(); };
It contains an extra field for holding the box identifier of the callback we will call every time the slot tick is activated. Here is the implementation of the constructor for our custom timer class. It is passed the identifier of the callback function we will call every time that the tick slot is activated. The connect call is QT's syntax for specifying that the tick method is to be called each time the timer reaches zero. StzQtTimer::StzQtTimer(int on_tick){ callback = on_tick; connect(this, SIGNAL(timeout()), this, SLOT(tick())); }
The tick method simply calls an external callback function. extern "C" void call_function (int func);
void StzQtTimer::tick(){ call_function(callback); }
And similarly, the creation, deletion, and field accessors of our custom timer class are exposed through simple C functions. extern "C" { StzQtTimer* QTimer_new (int func, int interval){ StzQtTimer* t = new StzQtTimer(func); t->setInterval(interval); return t; } void QTimer_delete (StzQtTimer* t){delete t;} int QTimer_callback (StzQtTimer* t){return t->callback;} void QTimer_start (StzQtTimer* t){t->start();} void QTimer_stop (StzQtTimer* t){t->stop();} }
Compiling our C Bindingslibqt.h and libqt.cpp contains the complete implementation of our QT bindings for C. Now we can compile them to a statically-linkable stand-alone library.
Open the terminal and navigate to the folder containing the source files and the tetris.pro file. If you are using Macintosh OS-X, then type in the following /Users/psli/Qt5.6.0/5.6/clang_64/bin/qmake tetris.pro -r -spec macx-clang make
Replace the /Users/psli/Qt5.6.0 with the path to your own QT installation. This should produce the library libtetris.a which we will link against later in our Stanza Tetris code. If you are using Linux, then type in the following /home/psli/Qt5.6.0/5.6/gcc_64/bin/qmake ../src/tetris.pro -r -spec linux-g++ make
Replace the /home/psli/Qt5.6.0 with the path to your own QT installation. Writing a Stanza Interface to QTNow that we've finished writing a C interface to QT, it is now in a form that can be called from Stanza. In this section we will write LoStanza functions and types for calling our C interface. QMouseEvent and QKeyEventThe interface to the QMouseEvent class is a simple wrapper over its C pointer. Here is its LoStanza type definition. public lostanza deftype QMouseEvent : event: ptr<?>
To retrieve its associated x and y fields, we write LoStanza functions that call the C field accessors that we wrote in the previous section. extern QMouseEvent_x: (ptr<?>) -> int extern QMouseEvent_y: (ptr<?>) -> int
public lostanza defn x (e:ref<QMouseEvent>) -> ref<Int> : return new Int{call-c QMouseEvent_x(e.event)}
public lostanza defn y (e:ref<QMouseEvent>) -> ref<Int> : return new Int{call-c QMouseEvent_y(e.event)}
The QKeyEvent bindings are implemented identically to the QMouseEvent bindings. public lostanza deftype QKeyEvent : event: ptr<?>
extern QKeyEvent_key: (ptr<?>) -> int public lostanza defn key (e:ref<QKeyEvent>) -> ref<Int> : return new Int{call-c QKeyEvent_key(e.event)}
QBrush, QColor, QPixmap, QPenThe bindings for the QBrush class is similar to those for QMouseEvent and QKeyEvent except for the need to create and delete the C pointer. Here is the definition of the Lostanza type. public lostanza deftype QBrush : value:ptr<?> marker:ref<LivenessMarker>
In addition to the C pointer, we store an additional LivenessMarker for tracking the lifetime of the object. When this marker is no longer live, we will automatically free the C pointer. Here are the definitions of the two functions for creating a QBrush . extern QBrush_new : () -> ptr<?> extern QBrush_new_c : (ptr<?>) -> ptr<?> extern QBrush_delete : (ptr<?>) -> int
public lostanza defn QBrush () -> ref<QBrush> : val ptr = call-c QBrush_new() val m = c-autofree-marker(addr(QBrush_delete), ptr) return new QBrush{ptr, m}
public lostanza defn QBrush (c:ref<QColor>) -> ref<QBrush> : val ptr = call-c QBrush_new_c(c.value) val m = c-autofree-marker(addr(QBrush_delete), ptr) return new QBrush{ptr, m}
Note especially the use of c-autofree-marker for creating a marker that automatically calls a C function on a pointer when the marker is no longer live. The bindings for QColor , QPixmap , and QPen are written in a similar fashion to QBrush . They also contain a LivenessMarker object to handle automatically freeing their C pointers. Here are the definitions for supporting QColor . extern QColor_new : (int, int, int, int) -> ptr<?> extern QColor_delete : (ptr<?>) -> int
public lostanza deftype QColor : value:ptr<?> marker:ref<LivenessMarker>
public lostanza defn QColor (r:ref<Int>, g:ref<Int>, b:ref<Int>, a:ref<Int>) -> ref<QColor> : val ptr = call-c QColor_new(r.value, g.value, b.value, a.value) val m = c-autofree-marker(addr(QColor_delete), ptr) return new QColor{ptr, m}
Here are the definitions for supporting QPixmap . extern QPixmap_load : (ptr<byte>) -> ptr<?> extern QPixmap_delete : (ptr<?>) -> int extern QPixmap_width : (ptr<?>) -> int extern QPixmap_height : (ptr<?>) -> int
public lostanza deftype QPixmap : value:ptr<?> marker:ref<LivenessMarker>
public lostanza defn QPixmap (filepath:ref<String>) -> ref<QPixmap> : val ptr = call-c QPixmap_load(addr!(filepath.chars)) val m = c-autofree-marker(addr(QPixmap_delete), ptr) return new QPixmap{ptr, m}
public lostanza defn width (p:ref<QPixmap>) -> ref<Int> : return new Int{call-c QPixmap_width(p.value)} public lostanza defn height (p:ref<QPixmap>) -> ref<Int> : return new Int{call-c QPixmap_height(p.value)}
Here are the definitions for supporting QPen . extern QPen_new : (ptr<?>, int) -> ptr<?> extern QPen_delete : (ptr<?>) -> int
public lostanza deftype QPen : value:ptr<?> marker:ref<LivenessMarker>
public lostanza defn QPen (c:ref<QColor>, thickness:ref<Int>) -> ref<QPen> : val ptr = call-c QPen_new(c.value, thickness.value) val m = c-autofree-marker(addr(QPen_delete), ptr) return new QPen{ptr, m}
QApplicationThe LoStanza type representing the QApplication class is defined to be a subtype of Resource so that we may use the resource keyword with it. When used with the resource keyword, the free function will be called at the end of the application's scope. extern QApplication_delete: (ptr<?>) -> int extern QApplication_exec: (ptr<?>) -> int
public lostanza deftype QApplication <: Resource : value: ptr<?>
public lostanza defn exec (a:ref<QApplication>) -> ref<False> : call-c QApplication_exec(a.value) return false
lostanza defmethod free (w:ref<QApplication>) -> ref<False> : call-c QApplication_delete(w.value) return false
The C function for creating QApplication objects requires the command line arguments passed into the program. They can be retrieved from the externally-defined values input_argc and input_argv which are initialized by Stanza on program entry. extern input_argc: long extern input_argv: ptr<ptr<byte>> extern QApplication_new: (int, ptr<ptr<byte>>) -> ptr<?>
public lostanza defn QApplication () -> ref<QApplication> : return new QApplication{call-c QApplication_new(input_argc as int, input_argv)}
QPainterThe QPainter bindings are written similar to the ones for QBrush except that we do not use a LivenessMarker for automatically freeing its C pointer. This is because the QT framework requires a QPainter object to be deleted at a specific point, typically at the end of its scope. We will declare it as a subtype of Resource to allow us to accomplish this easily using the resource keyword. extern QPainter_new : (ptr<?>) -> ptr<?> extern QPainter_set_pen: (ptr<?>, ptr<?>) -> int extern QPainter_set_brush: (ptr<?>, ptr<?>) -> int extern QPainter_delete: (ptr<?>) -> int extern QPainter_set_opacity: (ptr<?>, float) -> int extern QPainter_draw_line: (ptr<?>, int, int, int, int) -> int extern QPainter_draw_rect: (ptr<?>, int, int, int, int) -> int extern QPainter_draw_pixmap: (ptr<?>, int, int, int, int, ptr<?>) -> int
public lostanza deftype QPainter <: Resource : value:ptr<?> public lostanza defn QPainter (w:ref<QWidget>) -> ref<QPainter> : return new QPainter{call-c QPainter_new(w.value)} public lostanza defn set-pen (w:ref<QPainter>, p:ref<QPen>) -> ref<False> : call-c QPainter_set_pen(w.value, p.value) return false
public lostanza defn set-brush (w:ref<QPainter>, b:ref<QBrush>) -> ref<False> : call-c QPainter_set_brush(w.value, b.value) return false
public lostanza defn set-opacity (w:ref<QPainter>, o:ref<Float>) -> ref<False> : call-c QPainter_set_opacity(w.value, o.value) return false
public lostanza defn draw-line (w:ref<QPainter>, x:ref<Int>, y:ref<Int>, x2:ref<Int>, y2:ref<Int>) -> ref<False> : call-c QPainter_draw_line(w.value, x.value, y.value, x2.value, y2.value) return false
public lostanza defn draw-rect (w:ref<QPainter>, x:ref<Int>, y:ref<Int>, width:ref<Int>, height:ref<Int>) -> ref<False> : call-c QPainter_draw_rect(w.value, x.value, y.value, width.value, height.value) return false
public lostanza defn draw-pixmap (w:ref<QPainter>, x:ref<Int>, y:ref<Int>, width:ref<Int>, height:ref<Int>, pixmap:ref<QPixmap>) -> ref<False> : call-c QPainter_draw_pixmap(w.value, x.value, y.value, width.value, height.value, pixmap.value) return false
lostanza defmethod free (w:ref<QPainter>) -> ref<False> : call-c QPainter_delete(w.value) return false
QWidgetThe QWidget type is also defined as a Resource so that it may be freed at the end of its scope. extern QtWidget_new: (ptr<?>) -> ptr<?> extern QtWidget_delete: (ptr<?>) -> int extern QtWidget_show: (ptr<?>) -> int extern QtWidget_update: (ptr<?>) -> int extern QtWidget_set_width: (ptr<?>, int) -> int extern QtWidget_set_height: (ptr<?>, int) -> int extern QtWidget_set_listener: (ptr<?>, int) -> int extern QtWidget_listener: (ptr<?>) -> int
public lostanza deftype QWidget <: Resource : value: ptr<?>
public lostanza defn QWidget (parent:ref<QWidget>) -> ref<QWidget> : return new QWidget{call-c QtWidget_new(parent.value)}
public lostanza defn QWidget () -> ref<QWidget> : return new QWidget{call-c QtWidget_new(0L as ptr<?>)}
public lostanza defn set-width (w:ref<QWidget>, x:ref<Int>) -> ref<False> : call-c QtWidget_set_width(w.value, x.value) return false
public lostanza defn set-height (w:ref<QWidget>, x:ref<Int>) -> ref<False> : call-c QtWidget_set_height(w.value, x.value) return false
public lostanza defn set-listener (w:ref<QWidget>, x:ref<QWidgetListener>) -> ref<False> : call-c QtWidget_set_listener(w.value, box-object(x)) return false
public lostanza defn show (w:ref<QWidget>) -> ref<False> : call-c QtWidget_show(w.value) return false
public lostanza defn update (w:ref<QWidget>) -> ref<False> : call-c QtWidget_update(w.value) return false
lostanza defmethod free (w:ref<QWidget>) -> ref<False> : val listener = call-c QtWidget_listener(w.value) if listener >= 0 : free-box(listener) call-c QtWidget_delete(w.value) return false
Notice especially the calls to box-object and free-box in the set-listener and free functions. Stanza values cannot be stored directly in C structures because the garbage collector is free to move the object and thus invalidate the original pointer value. box-object takes a Stanza value, stores it at a location that is visible to the garbage collector, and returns the integer identifier of the box it was stored in. This identifier is held constant until it is manually freed using free-box when the object is no longer used. A QWidgetListener is a simple type that supports methods for each of the callbacks we're interested in from QWidget . The default implementations for each of the multis do not do anything. public deftype QWidgetListener public defmulti painted (l:QWidgetListener) -> ? public defmulti mouse-pressed (l:QWidgetListener, e:QMouseEvent) -> ? public defmulti mouse-released (l:QWidgetListener, e:QMouseEvent) -> ? public defmulti mouse-moved (l:QWidgetListener, e:QMouseEvent) -> ? public defmulti key-pressed (l:QWidgetListener, e:QKeyEvent) -> ?
defmethod painted (l:QWidgetListener) : false defmethod mouse-pressed (l:QWidgetListener, e:QMouseEvent) : false defmethod mouse-released (l:QWidgetListener, e:QMouseEvent) : false defmethod mouse-moved (l:QWidgetListener, e:QMouseEvent) : false defmethod key-pressed (l:QWidgetListener, e:QKeyEvent) : false
QTimerThe QTimer type is defined as a Resource so that it may be freed at the end of its scope. A QTimer is created from a callback function and the time interval between each call to the callback function. Similar to how set-listener is implemented for QWidget , the constructor function stores the on-tick callback function into a box and then passes the box's identifier to the QTimer_new function. When the QTimer is freed, the box containing the callback function is also freed. extern QTimer_new: (int, int) -> ptr<?> extern QTimer_delete: (ptr<?>) -> int extern QTimer_callback: (ptr<?>) -> int extern QTimer_start: (ptr<?>) -> int extern QTimer_stop: (ptr<?>) -> int
extern defn call_function (func:int) -> int : val f = boxed-object(func) as ref<(() -> ?)> [f]() return 0
public lostanza deftype QTimer <: Resource : value: ptr<?>
public lostanza defn QTimer (on-tick:ref<(() -> ?)>, interval:ref<Int>) -> ref<QTimer> : val t = call-c QTimer_new(box-object(on-tick), interval.value) return new QTimer{t} lostanza defmethod free (t:ref<QTimer>) -> ref<False> : val callback = call-c QTimer_callback(t.value) free-box(callback) call-c QTimer_delete(t.value) return false
public lostanza defn start (t:ref<QTimer>) -> ref<False> : call-c QTimer_start(t.value) return false
public lostanza defn stop (t:ref<QTimer>) -> ref<False> : call-c QTimer_stop(t.value) return false
Note the definition of the call_function external function. It is written to be called from C with the identifier of the box containing the function to call. The first line val f = boxed-object(func) as ref<(() -> ?)>
retrieves the stored callback function from the box and casts it to a reference to a function. The second line [f]()
then dereferences the function and calls it. [x] is the syntax in LoStanza for dereferencing a pointer or reference. Key CodesThe following constants are used by QT to indicate the identity of the different keyboard keys that can be pressed. These values were retrieved from the QT documentation. public val KEY-A = 65 public val KEY-B = 66 public val KEY-C = 67 public val KEY-D = 68 public val KEY-E = 69 public val KEY-F = 70 public val KEY-G = 71 public val KEY-H = 72 public val KEY-I = 73 public val KEY-J = 74 public val KEY-K = 75 public val KEY-L = 76 public val KEY-M = 77 public val KEY-N = 78 public val KEY-O = 79 public val KEY-P = 80 public val KEY-Q = 81 public val KEY-R = 82 public val KEY-S = 83 public val KEY-T = 84 public val KEY-U = 85 public val KEY-V = 86 public val KEY-W = 87 public val KEY-X = 88 public val KEY-Y = 89 public val KEY-Z = 90 public val KEY-1 = 49 public val KEY-2 = 50 public val KEY-3 = 51 public val KEY-4 = 52 public val KEY-5 = 53 public val KEY-6 = 54 public val KEY-7 = 55 public val KEY-8 = 56 public val KEY-9 = 57 public val KEY-0 = 48 public val KEY-UP = 16777235 public val KEY-DOWN = 16777237 public val KEY-LEFT = 16777234 public val KEY-RIGHT = 16777236 public val KEY-SPACE = 32
Writing TetrisThe implementation of the Tetris game is divided into the following categories: - The
Tile type for representing and managing Tetris tiles.
- The
Board type for representing and managing the Tetris board.
- The
Drawer type for drawing a visual representation of the Tetris board.
- The
Game type for handling timing and key presses for updating and drawing the tetris board.
Tetris TilesEvery tile in Tetris is represented as an instance of the type Tile . It supports three basic functions, rows and cols , for retrieving the tile's height and width, and dots for retrieving its shape. dots returns a collection of 3-element tuples. The first and second element of each tuple is the row and column of the dot , and the last element is an integer representing the color of the dot . deftype Tile defmulti cols (t:Tile) -> Int defmulti rows (t:Tile) -> Int defmulti dots (t:Tile) -> Collection<[Int, Int, Int]>
To create a tile, we supply it the dimensions of the tile, and a collection of either False or Int values for indicating either the absence or color of a dot. defn Tile (cols:Int, rows:Int, dots:Seqable<False|Int>) : val dot-seq = to-seq(dots) val dot-tuple = to-tuple $ for r in (rows - 1) through 0 by -1 seq-cat : for c in 0 to cols seq? : match(next(dot-seq)) : (dot:Int) : One([r, c, dot]) (dot:False) : None() new Tile : defmethod cols (this) : cols defmethod rows (this) : rows defmethod dots (this) : dot-tuple
We can now create a collection containing all of the standard tiles in Tetris. val ALL-TILES = let : val _ = false [ ;I Tile Tile(4, 4, [_ _ 0 _ _ _ 0 _ _ _ 0 _ _ _ 0 _])
;J Tile Tile(3, 3, [_ 1 _ _ 1 _ 1 1 _])
;L Tile Tile(3, 3, [_ 2 _ _ 2 _ _ 2 2])
;O Tile Tile(2, 2, [3 3 3 3])
;S Tile Tile(3, 3, [_ 4 4 4 4 _ _ _ _])
;Z Tile Tile(3, 3, [6 6 _ _ 6 6 _ _ _])
;T Tile Tile(3, 3, [_ 5 _ 5 5 5 _ _ _]) ]
The rotate function takes a Tile as input and returns a new Tile representing the same tile rotated counter-clockwise by 90 degrees. defn rotate (t:Tile) : new Tile : defmethod cols (this) : rows(t) defmethod rows (this) : cols(t) defmethod dots (this) : new Collection<[Int, Int, Int]> : defmethod to-seq (this) : for [r, c, color] in dots(t) seq : [c, rows(t) - r - 1, color]
The random-tile function returns a random tile selected from the collection of standard Tetris tiles. defn random-tile () : val n = length(ALL-TILES) ALL-TILES[rand(n)]
The Tetris BoardThe Tetris board is a 25x10 sized board, with 20 rows that are visible, and 5 invisible rows at the top. Each cell on the board may be occupied by a dot. At any one time, a single Tetris tile may be active and falling. Once it falls to the bottom or hits an occupied cell then it is stamped onto the board. If a row on the board is completely occupied by dots then it is cleared, and every row above it is shifted down one row. The game ends if an invisible row ever contains a dot. The Board type supports the following operations. deftype Board defmulti rows (b:Board) -> Int defmulti cols (b:Board) -> Int defmulti vis-rows (b:Board) -> Int defmulti reset (b:Board) -> False defmulti get (b:Board, r:Int, c:Int) -> False|Int defmulti active-tile (b:Board) -> False|Tile defmulti active-tile-pos (b:Board) -> [Int, Int] defmulti spawn-tile (b:Board) -> False defmulti rotate-tile (b:Board) -> False|True defmulti slide-tile (b:Board, dr:Int, dc:Int) -> False|True defmulti stamp-active-tile (b:Board) -> False defmulti active-tile-on-ground? (b:Board) -> True|False defmulti game-over? (b:Board) -> False|True
rows returns the number of rows on the board (25).
cols returns the number of columns on the board (10).
vis-rows returns the number of visible rows on the board (20).
get returns the status of the cell at row r , and column c . The integer representing the color of the dot is returned if the cell is occupied, otherwise false is returned.
active-tile returns the Tile representing it if there is one.
active-tile-pos returns a tuple containing the row and column of the currently active tile.
spawn-tile sets the currently active tile to a newly spawned random tile in the invisible section of the board.
rotate-tile rotates the currently active tile. It returns true if the rotation was successful.
slide-tile slides the currently active tile by dr rows and dc columns. It returns true if the slide was successful.
stamp-active-tile stamps the currently active tile onto the board.
active-tile-on-ground? returns true if the currently active tile is sitting either on the bottom or on top of an occupied dot.
game-over? returns true if the game is over, caused by an invisible row being occupied by a dot.
reset resets the game by clearing all the cells and setting no tile as active.
defn Board () : ;Board Stats val num-rows = 25 val num-vis-rows = 20 val num-cols = 10 val board = Array<Int|False>(num-rows * num-cols, false) defn board-get (r:Int, c:Int) : board[r * num-cols + c] defn board-set (r:Int, c:Int, color:False|Int) : board[r * num-cols + c] = color
;Tile Stats var tile:False|Tile = false var tile-pos:[Int, Int] = [0, 0]
;Game Stats var game-over? = false
;Stamp a tile at a location defn stamp (t:Tile, r:Int, c:Int) : for [tr, tc, color] in dots(t) do : val dr = r + tr val dc = c + tc fatal("Illegal Stamp") when (not in-bounds?(dr, dc)) or occupied?(dr, dc) board-set(dr, dc, color)
;Does a tile fit in a given location? defn in-bounds? (r:Int, c:Int) : r >= 0 and c >= 0 and r < num-rows and c < num-cols defn occupied? (r:Int, c:Int) : board-get(r, c) is Int defn fits? (t:Tile, r:Int, c:Int) : for [tr, tc, _] in dots(t) all? : val dr = r + tr val dc = c + tc in-bounds?(dr, dc) and not occupied?(dr, dc) defn kick (t:Tile, r:Int, c:Int) : val cl = for i in 1 to cols(t) find : fits?(t, r, c - i) val cr = for i in 1 to cols(t) find : fits?(t, r, c + i) match(cl, cr) : (cl:False, cr:False) : false (cl:Int, cr:False) : (- cl) (cl:False, cr:Int) : cr (cl:Int, cr:Int) : cr when cr < cl else (- cl)
;Find and clear full lines defn clear-filled-lines () : defn copy-row (r1:Int, r2:Int) : if r1 != r2 : for c in 0 to num-cols do : board-set(r2, c, board-get(r1, c)) defn clear-row (r:Int) : for c in 0 to num-cols do : board-set(r, c, false) defn filled? (r:Int) : all?(occupied?{r, _}, 0 to num-cols) val filled-rows = to-seq(0 through num-rows) for r in 0 to num-rows do : copy-row(r, next(filled-rows)) when not filled?(r) do(clear-row, next(filled-rows) to num-rows)
;Have we lost? defn check-game-over? () : for r in num-vis-rows to num-rows any? : any?(occupied?{r, _} 0 to num-cols) new Board : defmethod rows (this) : num-rows defmethod vis-rows (this) : num-vis-rows defmethod cols (this) : num-cols defmethod active-tile (this) : tile
defmethod reset (this) : board[0 to false] = repeat(false) tile = false game-over? = false defmethod active-tile-pos (this) : fatal("No active tile") when tile is False tile-pos
defmethod get (this, r:Int, c:Int) -> False|Int : fatal("Out of Bounds") when not in-bounds?(r, c) board-get(r, c) defmethod slide-tile (this, dr:Int, dc:Int) : fatal("Game is over") when game-over? val [tr, tc] = tile-pos if fits?(tile as Tile, tr + dr, tc + dc) : tile-pos = [tr + dr, tc + dc] true defmethod rotate-tile (this) : fatal("Game is over") when game-over? val [tr, tc] = tile-pos val rtile = rotate(tile as Tile) if fits?(rtile, tr, tc) : tile = rtile true else : match(kick(rtile, tr, tc)) : (dc:Int) : tile = rtile tile-pos = [tr, tc + dc] true (dc:False) : false
defmethod stamp-active-tile (this) : fatal("Game is over") when game-over? val [r, c] = tile-pos stamp(tile as Tile, r, c) tile = false clear-filled-lines() game-over? = check-game-over?() defmethod spawn-tile (this) : fatal("Game is over") when game-over? fatal("Tile Exists") when tile is Tile tile = random-tile() tile-pos = [20, 3] defmethod game-over? (this) : game-over? defmethod active-tile-on-ground? (this) : val [tr, tc] = active-tile-pos(this) not fits?(tile as Tile, tr - 1, tc)
Drawing the BoardThe Drawer type is very simple and contains two simple functions. deftype Drawer defmulti draw (d:Drawer, w:QWidget) -> False defmulti size (d:Drawer) -> [Int, Int]
draw draws the Tetris board onto the given widget w .
size returns the size of the board in pixels. The first element of the returned tuple is the width and the second is the height.
defn Drawer (b:Board) : ;Coordinate system val [bx, by] = [10, 10] val [dx, dy] = [24, 24] defn coord (r:Int, c:Int) : val maxy = by + vis-rows(b) * dy [bx + c * dx, maxy - r * dy - dy]
defn visible? (r:Int, c:Int) : r >= 0 and c >= 0 and r < vis-rows(b) and c < cols(b)
;Tile Colored Brushes val colored-brushes = to-tuple{seq(QBrush, _)} $ [ QColor(0, 255, 255, 255) QColor(0, 0, 255, 255) QColor(255, 165, 0, 255) QColor(255, 255, 0, 255) QColor(0, 255, 0, 255) QColor(139, 0, 139, 255) QColor(255, 0, 0, 255)]
;Colored Pens val white = QColor(255, 255, 255, 255) val gray = QColor(230, 230, 230, 255) val black = QColor(0, 0, 0, 255) val white-pen = QPen(white, 2) val black-pen = QPen(black, 2) val gray-brush = QBrush(gray) new Drawer : defmethod size (this) : [cols(b) * dx + 2 * bx vis-rows(b) * dy + 2 * by] defmethod draw (this, w:QWidget) : resource p = QPainter(w) ;Draw Grid set-pen(p, white-pen) set-brush(p, gray-brush) for r in 0 to vis-rows(b) do : for c in 0 to cols(b) do : val [x, y] = coord(r, c) draw-rect(p, x, y, dx, dy) ;Draw tiles set-pen(p, black-pen) for r in 0 to vis-rows(b) do : for c in 0 to cols(b) do : match(b[r, c]) : (color:Int) : val [x, y] = coord(r, c) set-brush(p, colored-brushes[color]) draw-rect(p, x, y, dx, dy) (color:False) : false ;Draw active tile match(active-tile(b)) : (t:Tile) : val [r, c] = active-tile-pos(b) for [tr, tc, color] in dots(t) do : if visible?(r + tr, c + tc) : val [x, y] = coord(r + tr, c + tc) set-brush(p, colored-brushes[color]) draw-rect(p, x, y, dx, dy) (t:False) : false
Note the use of the resource keyword in the creation of the QPainter object. This is mandated by the QT library. The QPainter object must be freed at the end of the draw method. Controlling the GameThe Tetris game controller will handle the game timing and keyboard events. deftype Game defmulti update (g:Game) -> False defmulti key-pressed (g:Game, k:Int) -> False defmulti draw (g:Game, widget:QWidget) -> False defmulti size (g:Game) -> [Int, Int]
The key-pressed function is called each time a keyboard key is pressed. The update function is assumed to be called sixty times a second, each time followed by a call to draw . The size function returns the dimensions of the playing board. defn Game () : ;Game components val b = Board() val drawer = Drawer(b) ;Block drop timer val normal-period = 30 val drop-period = 1 var period = 30 var drop-timer = period
;Spawn tile defn drop-next-tile () : spawn-tile(b) period = normal-period
;Safety timer defn add-safety-time () : if active-tile-on-ground?(b) : drop-timer = period
;Spawn initial tile drop-next-tile() new Game : defmethod update (this) : if not game-over?(b) : drop-timer = drop-timer - 1 if drop-timer <= 0 : drop-timer = period if not slide-tile(b, -1, 0) : stamp-active-tile(b) drop-next-tile() when not game-over?(b) defmethod key-pressed (this, k:Int) : if game-over?(b) : if k == KEY-SPACE : reset(b) drop-timer = period drop-next-tile() else : switch {k == _} : KEY-UP : if rotate-tile(b) : add-safety-time() KEY-DOWN : slide-tile(b, -1, 0) KEY-LEFT : if slide-tile(b, 0, -1) : add-safety-time() KEY-RIGHT : if slide-tile(b, 0, 1) : add-safety-time() KEY-SPACE : drop-timer = 0 period = drop-period else : false false defmethod draw (this, w:QWidget) : draw(drawer, w) defmethod size (this) : size(drawer)
The timing is controlled by the period and drop-timer variables. The drop-timer variable decreases by one each time update is called. When it hits zero, then the currently active tile drops by one, and the drop-timer is reset to the period . The period , by default, is equal to normal-period , but a user may speed it up to drop-period by pressing the space bar. In the update function, the currently active tile is dropped by one every time the drop-timer hits zero. If the active tile cannot be dropped, then it has hit the ground and it is stamped onto the board. The key-pressed function is straightforward except for the call to add-safety-time . The Tetris mechanics allows players to slide tiles indefinitely even after hitting the ground. Every time a tile is successfully slid or rotated, drop-timer is reset if the tile is touching the ground. Putting it TogetherFinally we put everything together by first creating a QApplication object as mandated by QT, and a QWidget object for drawing our Tetris game. The widget's listener will forward events to the Game object. We will use a QTimer with a period of 16 milliseconds to update and paint our game sixty times per second. defn main () : resource app = QApplication() resource widget = QWidget() val game = Game() ;Set size val [w, h] = size(game) set-width(widget, w) set-height(widget, h) ;Event Handling set-listener{widget, _} $ new QWidgetListener : defmethod painted (this) : draw(game, widget) defmethod key-pressed (this, e:QKeyEvent) : key-pressed(game, key(e)) ;Set Framerate Timer resource timer = QTimer{_, 16} $ fn () : update(game) update(widget) start(timer) ;Start! show(widget) exec(app)
main()
That concludes the description of the Tetris game! The actual mechanics of Tetris are quite simple to write, and most of our work for this project went into writing the Stanza QT bindings. These bindings will eventually be written and provided for you but it is still useful to know how to write such bindings yourself should the need arise. More AdditionsThe Tetris game presented here is very simple and is missing various features. To flesh out the game, you can add the following features: - Gradually speed up the rate at which the tile falls.
- Draw a display to show the next block that will be dropped.
- Display a score counter and invent a way to compute the player's score.
- Draw an effect to notify the player whenever s/he completes a tetris by clearing four lines at once.
- Add controls to rotate the tiles clock-wise and counter-clock-wise.
- Add fancier animations when clearing lines and dropping tiles.
|