-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathtetris.html
More file actions
46 lines (45 loc) · 68.5 KB
/
tetris.html
File metadata and controls
46 lines (45 loc) · 68.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
<!DOCTYPE html>
<html>
<head>
<title>L.B.Stanza</title>
<link type="text/css" rel="stylesheet" href="resources/mainstyle.css">
<link type="text/css" rel="stylesheet" href="resources/documentation.css">
</head>
<body>
<table class="wrap">
<tr><td colspan="3" class="banner">
<a href="index.html">Home</a><a href="philosophy.html">Philosophy</a><a href="downloads.html">Downloads</a><a href="documentation.html">Documentation</a><a href="people.html">People</a><a href="community.html">Community</a><a href="news.html">News</a><a href="reference.html">Reference</a>
</td></tr>
<tr>
<td class="nav">
<h1>NAVIGATION</h1>
<h2><a href="#anchor131">Tetris</a></h2><h3><a href="#anchor132">Downloads</a></h3><h4><a href="#anchor133">Prebuilt Binaries</a></h4><h4><a href="#anchor134">Source Files</a></h4><h3><a href="#anchor135">Preparation</a></h3><h4><a href="#anchor136">Stanza</a></h4><h4><a href="#anchor137">QT</a></h4><h4><a href="#anchor138">Compilation</a></h4><h3><a href="#anchor139">Writing a C Interface to QT</a></h3><h4><a href="#anchor140">The .pro File</a></h4><h4><a href="#anchor141">Exposing C++ Functionality through C Functions</a></h4><h4><a href="#anchor142">C Bindings for QWidget</a></h4><h4><a href="#anchor143">C Bindings for QTimer</a></h4><h4><a href="#anchor144">Compiling our C Bindings</a></h4><h3><a href="#anchor145">Writing a Stanza Interface to QT</a></h3><h4><a href="#anchor146">QMouseEvent and QKeyEvent</a></h4><h4><a href="#anchor147">QBrush, QColor, QPixmap, QPen</a></h4><h4><a href="#anchor148">QApplication</a></h4><h4><a href="#anchor149">QPainter</a></h4><h4><a href="#anchor150">QWidget</a></h4><h4><a href="#anchor151">QTimer</a></h4><h4><a href="#anchor152">Key Codes</a></h4><h3><a href="#anchor153">Writing Tetris</a></h3><h4><a href="#anchor154">Tetris Tiles</a></h4><h4><a href="#anchor155">The Tetris Board</a></h4><h4><a href="#anchor156">Drawing the Board</a></h4><h4><a href="#anchor157">Controlling the Game</a></h4><h4><a href="#anchor158">Putting it Together</a></h4><h3><a href="#anchor159">More Additions</a></h3>
</td>
<td class="main">
<h1 id="anchor131">Tetris</h1><p>This 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.</p><table width=100%>
<tr><td align=center>
<img src="resources/tetris/shot1.png" style="height:400px"></img>
<img src="resources/tetris/shot2.png" style="height:400px"></img>
</td></tr>
</table><h2 id="anchor132">Downloads</h2><p>All 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.</p><h3 id="anchor133">Prebuilt Binaries</h3><p><a href="resources/tetris/osxtetris.dmg">osxtetris.dmg</a> - for Macintosh OS-X</p><h3 id="anchor134">Source Files</h3><p><a href="resources/tetris/linuxtetris.zip">linuxtetris.zip</a> - for Linux</p><p><a href="resources/tetris/osxtetris.zip">osxtetris.zip</a> - for Macintosh OS-X</p><h2 id="anchor135">Preparation</h2><p></p><h3 id="anchor136">Stanza</h3><p>This tutorial was written for Stanza 0.9.4 or higher. Please download that from <code>www.lbstanza.org</code> if you haven't already.</p><h3 id="anchor137">QT</h3><p>This tutorial also requires a 64-bit version of the QT5 library. Please download that from <code>www.qt.io/download</code> if you haven't already.</p><h3 id="anchor138">Compilation</h3><p>Download the source files for your platform from the links above and unzip them to a folder called <code>tetris</code>. To compile the project, first open the <code>script/make.sh</code> file and find the line that sets the <code>QTDIR</code> variable.</p><pre><code>QTDIR=/Users/psli/Qt5.6.0</code></pre><p>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 <code>tetris</code> folder, and type</p><pre><code>./scripts/make.sh</code></pre><p>If everything is in the proper place, and QT has been properly installed, then the script should produce a binary called <code>tetris</code>. Run it by typing</p><pre><code>./tetris</code></pre><p>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.</p><h2 id="anchor139">Writing a C Interface to QT</h2><p>We 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.</p><h3 id="anchor140">The .pro File</h3><p>QT comes with its own build system and preprocessor called <code>qmake</code> and <code>moc</code> for implementing its <span style="font-style:italic;">signals and slots</span> system. Create a file called <code>tetris.pro</code> containing the following.</p><pre><code>TEMPLATE = lib<br>CONFIG += staticlib<br>CONFIG += x86_64<br><br>QT += widgets<br><br>HEADERS = libqt.h<br>SOURCES = libqt.cpp</code></pre><p>The header file containing all of the definitions for our QT C bindings are in the file <code>libqt.h</code> and the source file containing their implementations are in the file <code>libqt.cpp</code>. </p><h3 id="anchor141">Exposing C++ Functionality through C Functions</h3><p>The 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.</p><p>In the file <code>libqt.cpp</code>, the following include statements import the QT definitions that we're interested in writing bindings for.</p><pre><code> #include<QApplication><br> #include<QWidget><br> #include<QTimer><br> #include<QMouseEvent><br> #include<QKeyEvent><br> #include<QPainter><br> #include<QBrush><br> #include<QPen><br> #include "libqt.h"</code></pre><p>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 <code>QPainter</code> functionality required for Tetris.</p><pre><code>extern "C" {<br> QPainter* QPainter_new (QWidget* widget){<br> return new QPainter(widget);}<br> void QPainter_delete (QPainter* p){<br> delete p;}<br> void QPainter_set_pen (QPainter* p, QPen* pen){<br> p->setPen(*pen);}<br> void QPainter_set_brush (QPainter* p, QBrush* brush){<br> p->setBrush(*brush);}<br> void QPainter_set_opacity (QPainter* p, float opacity){<br> p->setOpacity(opacity);}<br> void QPainter_draw_line (QPainter* p, int x, int y, int x2, int y2){<br> p->drawLine(x, y, x2, y2);}<br> void QPainter_draw_rect (QPainter* p, int x, int y, int width, int height){<br> p->drawRect(x, y, width, height);}<br> void QPainter_draw_pixmap (QPainter* p, int x, int y,<br> int width, int height, QPixmap* pixmap){<br> p->drawPixmap(x, y, width, height, *pixmap);}<br>}</code></pre><p>Notice especially that all the above definitions are wrapped in an </p><pre><code>extern "C" { ... }</code></pre><p>block. This tells the C++ compiler that those functions are meant to be called using the C calling convention. The functionalities of the <code>QApplication</code>, <code>QMouseEvent</code>, <code>QKeyEvent</code>, <code>QBrush</code>, <code>QColor</code>, <code>QPixmap</code>, and <code>QPen</code> QT classes are wrapped up in an identical fashion to <code>QPainter</code>. </p><pre><code>extern "C" { <br> QApplication* QApplication_new (int argc, char* argv[]){<br> return new QApplication(argc, argv);}<br> void QApplication_delete (QApplication* x){<br> delete x;}<br> int QApplication_exec (QApplication* x){<br> return x->exec();}<br><br> int QMouseEvent_x (QMouseEvent* e){<br> return e->x();}<br> int QMouseEvent_y (QMouseEvent* e){<br> return e->y();}<br><br> int QKeyEvent_key (QKeyEvent* e){<br> return e->key();}<br><br> QBrush* QBrush_new (){<br> return new QBrush();}<br> QBrush* QBrush_new_c (QColor* c){<br> return new QBrush(*c);}<br> void QBrush_delete (QBrush* b){<br> delete b;}<br> <br> QColor* QColor_new (int r, int g, int b, int a){<br> return new QColor(r, g, b, a);}<br> void QColor_delete (QColor* c){<br> delete c;}<br><br> QPixmap* QPixmap_load (char* filepath){<br> QPixmap* pixmap = new QPixmap();<br> int r = pixmap->load(filepath);<br> if(r) return pixmap;<br> else return 0;<br> }<br> void QPixmap_delete (QPixmap* p){<br> delete p;}<br> int QPixmap_width (QPixmap* p){<br> return p->width();}<br> int QPixmap_height (QPixmap* p){<br> return p->height();}<br><br> QPen* QPen_new (QColor* c, int thickness){<br> return new QPen(*c, thickness, Qt::SolidLine);}<br> void QPen_delete (QPen* p){<br> delete p;}<br>}</code></pre><h3 id="anchor142">C Bindings for QWidget</h3><p>The QT functionality above was easily exposed as a collection of functions because they don't require the use of any <span style="font-style:italic;">callbacks</span>. The functions are called with simple values and simple values are returned. However, the <code>QWidget</code> 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 <code>keyPressEvent</code> method is called by the QT framework. We will expose this functionality by writing a custom <code>QWidget</code> class that calls a special <span style="font-style:italic;">listener</span> function for each of the virtual methods we are interested in exposing.</p><p>In <code>libqt.h</code>, include the following statements for importing the class definition we are interested in.</p><pre><code>#include<QApplication><br>#include<QWidget><br>#include<QTimer></code></pre><p>Here is the definition of our custom <code>QWidget</code> class. </p><pre><code>class StzQtWidget : public QWidget{<br> Q_OBJECT<br> public:<br> StzQtWidget(QWidget* parent);<br> int width;<br> int height;<br> int listener;<br> QSize sizeHint() const;<br> protected:<br> void paintEvent(QPaintEvent *event);<br> void mousePressEvent(QMouseEvent* event);<br> void mouseReleaseEvent(QMouseEvent* event);<br> void mouseMoveEvent(QMouseEvent* event); <br> void keyPressEvent(QKeyEvent *event);<br>};</code></pre><p>It contains the extra fields, <code>width</code> and <code>height</code>, for indicating the preferred size of the widget. The field, <code>listener</code>, is the <span style="font-style:italic;">box identifier</span> of the listener object that will handle the virtual function events. We are interested in handling paint events, mouse events, and keyboard events. </p><p>Here is the default constructor for our custom widget. It sets the default size, and we use <code>-1</code> to indicate that there initially is no event listener.</p><pre><code>StzQtWidget::StzQtWidget(QWidget* parent) : QWidget(parent) {<br> width = 100;<br> height = 100;<br> listener = -1;<br>}</code></pre><p>We override the definition of <code>sizeHint</code> to return the values of its <code>width</code> and <code>height</code> fields.</p><pre><code>QSize StzQtWidget::sizeHint() const{<br> return QSize(width, height);<br>} </code></pre><p>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.</p><pre><code>extern "C" void QtWidget_paintEvent (int listener, QPaintEvent* event);<br>extern "C" void QtWidget_mousePressEvent (int listener, QMouseEvent* event);<br>extern "C" void QtWidget_mouseReleaseEvent (int listener, QMouseEvent* event);<br>extern "C" void QtWidget_mouseMoveEvent (int listener, QMouseEvent* event);<br>extern "C" void QtWidget_keyPressEvent (int listener, QKeyEvent* event);<br><br>void StzQtWidget::paintEvent(QPaintEvent* event){<br> QtWidget_paintEvent(listener, event);<br>}<br><br>void StzQtWidget::mousePressEvent(QMouseEvent* event){<br> QtWidget_mousePressEvent(listener, event);<br>}<br><br>void StzQtWidget::mouseReleaseEvent(QMouseEvent* event){<br> QtWidget_mouseReleaseEvent(listener, event);<br>}<br><br>void StzQtWidget::mouseMoveEvent(QMouseEvent* event){<br> QtWidget_mouseMoveEvent(listener, event);<br>}<br><br>void StzQtWidget::keyPressEvent(QKeyEvent *event){<br> QtWidget_keyPressEvent(listener, event);<br>}</code></pre><p>And, just as before, creation, deletion, and field accessors for our widget are exposed as a collection of C functions.</p><pre><code>extern "C" {<br> StzQtWidget* QtWidget_new (QWidget* parent) {<br> return new StzQtWidget(parent);}<br> void QtWidget_delete (StzQtWidget* x){<br> delete x;}<br> void QtWidget_show (StzQtWidget* x){<br> x->show();}<br> void QtWidget_update (StzQtWidget* x){<br> x->update();}<br> void QtWidget_set_width (StzQtWidget* x, int width){<br> x->width = width;}<br> void QtWidget_set_height (StzQtWidget* x, int height){<br> x->height = height;}<br> void QtWidget_set_listener (StzQtWidget* x, int listener){<br> x->listener = listener;}<br> int QtWidget_listener (StzQtWidget* x){<br> return x->listener;}<br>}</code></pre><h3 id="anchor143">C Bindings for QTimer</h3><p>The bindings for the <code>QTimer</code> class are written in an identical fashion to those for <code>QWidget</code>. In <code>libqt.h</code> we define our custom <code>QTimer</code> class.</p><pre><code>class StzQtTimer : public QTimer{<br> Q_OBJECT<br> public:<br> int callback;<br> StzQtTimer(int callback);<br> private Q_SLOTS:<br> void tick();<br>};</code></pre><p>It contains an extra field for holding the box identifier of the callback we will call every time the <span style="font-style:italic;">slot</span> <code>tick</code> is activated.</p><p>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 <code>tick</code> slot is activated. The <code>connect</code> call is QT's syntax for specifying that the <code>tick</code> method is to be called each time the timer reaches zero. </p><pre><code>StzQtTimer::StzQtTimer(int on_tick){<br> callback = on_tick;<br> connect(this, SIGNAL(timeout()), this, SLOT(tick()));<br>}</code></pre><p>The <code>tick</code> method simply calls an external callback function.</p><pre><code>extern "C" void call_function (int func);<br><br>void StzQtTimer::tick(){<br> call_function(callback);<br>}</code></pre><p>And similarly, the creation, deletion, and field accessors of our custom timer class are exposed through simple C functions.</p><pre><code>extern "C" {<br> StzQtTimer* QTimer_new (int func, int interval){<br> StzQtTimer* t = new StzQtTimer(func);<br> t->setInterval(interval);<br> return t;<br> }<br> void QTimer_delete (StzQtTimer* t){delete t;}<br> int QTimer_callback (StzQtTimer* t){return t->callback;}<br> void QTimer_start (StzQtTimer* t){t->start();}<br> void QTimer_stop (StzQtTimer* t){t->stop();}<br>}</code></pre><h3 id="anchor144">Compiling our C Bindings</h3><p><code>libqt.h</code> and <code>libqt.cpp</code> contains the complete implementation of our QT bindings for C. Now we can compile them to a statically-linkable stand-alone library. </p><p>Open the terminal and navigate to the folder containing the source files and the <code>tetris.pro</code> file. If you are using Macintosh OS-X, then type in the following</p><pre><code>/Users/psli/Qt5.6.0/5.6/clang_64/bin/qmake tetris.pro -r -spec macx-clang<br>make</code></pre><p>Replace the <code>/Users/psli/Qt5.6.0</code> with the path to your own QT installation. This should produce the library <code>libtetris.a</code> which we will link against later in our Stanza Tetris code.</p><p>If you are using Linux, then type in the following</p><pre><code>/home/psli/Qt5.6.0/5.6/gcc_64/bin/qmake ../src/tetris.pro -r -spec linux-g++<br>make</code></pre><p>Replace the <code>/home/psli/Qt5.6.0</code> with the path to your own QT installation.</p><h2 id="anchor145">Writing a Stanza Interface to QT</h2><p>Now 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.</p><h3 id="anchor146">QMouseEvent and QKeyEvent</h3><p>The interface to the <code>QMouseEvent</code> class is a simple wrapper over its C pointer. Here is its LoStanza type definition.</p><pre><code>public lostanza deftype QMouseEvent :<br> event: ptr<?></code></pre><p>To retrieve its associated <code>x</code> and <code>y</code> fields, we write LoStanza functions that call the C field accessors that we wrote in the previous section.</p><pre><code>extern QMouseEvent_x: (ptr<?>) -> int<br>extern QMouseEvent_y: (ptr<?>) -> int<br><br>public lostanza defn x (e:ref<QMouseEvent>) -> ref<Int> :<br> return new Int{call-c QMouseEvent_x(e.event)}<br><br>public lostanza defn y (e:ref<QMouseEvent>) -> ref<Int> :<br> return new Int{call-c QMouseEvent_y(e.event)}</code></pre><p>The <code>QKeyEvent</code> bindings are implemented identically to the <code>QMouseEvent</code> bindings.</p><pre><code>public lostanza deftype QKeyEvent :<br> event: ptr<?><br><br>extern QKeyEvent_key: (ptr<?>) -> int<br> <br>public lostanza defn key (e:ref<QKeyEvent>) -> ref<Int> :<br> return new Int{call-c QKeyEvent_key(e.event)}</code></pre><h3 id="anchor147">QBrush, QColor, QPixmap, QPen</h3><p>The bindings for the <code>QBrush</code> class is similar to those for <code>QMouseEvent</code> and <code>QKeyEvent</code> except for the need to create and delete the C pointer. Here is the definition of the Lostanza type.</p><pre><code>public lostanza deftype QBrush :<br> value:ptr<?><br> marker:ref<LivenessMarker></code></pre><p>In addition to the C pointer, we store an additional <code>LivenessMarker</code> for tracking the lifetime of the object. When this marker is no longer live, we will automatically free the C pointer.</p><p>Here are the definitions of the two functions for creating a <code>QBrush</code>.</p><pre><code>extern QBrush_new : () -> ptr<?><br>extern QBrush_new_c : (ptr<?>) -> ptr<?><br>extern QBrush_delete : (ptr<?>) -> int<br><br>public lostanza defn QBrush () -> ref<QBrush> :<br> val ptr = call-c QBrush_new()<br> val m = c-autofree-marker(addr(QBrush_delete), ptr)<br> return new QBrush{ptr, m}<br><br>public lostanza defn QBrush (c:ref<QColor>) -> ref<QBrush> :<br> val ptr = call-c QBrush_new_c(c.value)<br> val m = c-autofree-marker(addr(QBrush_delete), ptr)<br> return new QBrush{ptr, m}</code></pre><p>Note especially the use of <code>c-autofree-marker</code> for creating a marker that automatically calls a C function on a pointer when the marker is no longer live.</p><p>The bindings for <code>QColor</code>, <code>QPixmap</code>, and <code>QPen</code> are written in a similar fashion to <code>QBrush</code>. They also contain a <code>LivenessMarker</code> object to handle automatically freeing their C pointers.</p><p>Here are the definitions for supporting <code>QColor</code>.</p><pre><code>extern QColor_new : (int, int, int, int) -> ptr<?><br>extern QColor_delete : (ptr<?>) -> int<br><br>public lostanza deftype QColor :<br> value:ptr<?><br> marker:ref<LivenessMarker><br><br>public lostanza defn QColor (r:ref<Int>, g:ref<Int>,<br> b:ref<Int>, a:ref<Int>) -> ref<QColor> :<br> val ptr = call-c QColor_new(r.value, g.value, b.value, a.value)<br> val m = c-autofree-marker(addr(QColor_delete), ptr)<br> return new QColor{ptr, m}</code></pre><p>Here are the definitions for supporting <code>QPixmap</code>.</p><pre><code>extern QPixmap_load : (ptr<byte>) -> ptr<?><br>extern QPixmap_delete : (ptr<?>) -> int<br>extern QPixmap_width : (ptr<?>) -> int<br>extern QPixmap_height : (ptr<?>) -> int<br><br>public lostanza deftype QPixmap :<br> value:ptr<?><br> marker:ref<LivenessMarker><br><br>public lostanza defn QPixmap (filepath:ref<String>) -> ref<QPixmap> :<br> val ptr = call-c QPixmap_load(addr!(filepath.chars))<br> val m = c-autofree-marker(addr(QPixmap_delete), ptr)<br> return new QPixmap{ptr, m}<br><br>public lostanza defn width (p:ref<QPixmap>) -> ref<Int> :<br> return new Int{call-c QPixmap_width(p.value)}<br> <br>public lostanza defn height (p:ref<QPixmap>) -> ref<Int> :<br> return new Int{call-c QPixmap_height(p.value)}</code></pre><p>Here are the definitions for supporting <code>QPen</code>.</p><pre><code>extern QPen_new : (ptr<?>, int) -> ptr<?><br>extern QPen_delete : (ptr<?>) -> int<br><br>public lostanza deftype QPen :<br> value:ptr<?><br> marker:ref<LivenessMarker><br><br>public lostanza defn QPen (c:ref<QColor>, thickness:ref<Int>) -> ref<QPen> :<br> val ptr = call-c QPen_new(c.value, thickness.value)<br> val m = c-autofree-marker(addr(QPen_delete), ptr)<br> return new QPen{ptr, m}</code></pre><h3 id="anchor148">QApplication</h3><p>The LoStanza type representing the <code>QApplication</code> class is defined to be a subtype of <code>Resource</code> so that we may use the <code>resource</code> keyword with it. When used with the <code>resource</code> keyword, the <code>free</code> function will be called at the end of the application's scope.</p><pre><code>extern QApplication_delete: (ptr<?>) -> int<br>extern QApplication_exec: (ptr<?>) -> int<br><br>public lostanza deftype QApplication <: Resource :<br> value: ptr<?><br><br>public lostanza defn exec (a:ref<QApplication>) -> ref<False> :<br> call-c QApplication_exec(a.value)<br> return false<br><br>lostanza defmethod free (w:ref<QApplication>) -> ref<False> :<br> call-c QApplication_delete(w.value)<br> return false</code></pre><p>The C function for creating <code>QApplication</code> objects requires the command line arguments passed into the program. They can be retrieved from the externally-defined values <code>input_argc</code> and <code>input_argv</code> which are initialized by Stanza on program entry.</p><pre><code>extern input_argc: long<br>extern input_argv: ptr<ptr<byte>><br>extern QApplication_new: (int, ptr<ptr<byte>>) -> ptr<?><br><br>public lostanza defn QApplication () -> ref<QApplication> :<br> return new QApplication{call-c QApplication_new(input_argc as int, input_argv)}</code></pre><h3 id="anchor149">QPainter</h3><p>The <code>QPainter</code> bindings are written similar to the ones for <code>QBrush</code> except that we do not use a <code>LivenessMarker</code> for automatically freeing its C pointer. This is because the QT framework requires a <code>QPainter</code> object to be deleted at a specific point, typically at the end of its scope. We will declare it as a subtype of <code>Resource</code> to allow us to accomplish this easily using the <code>resource</code> keyword.</p><pre><code>extern QPainter_new : (ptr<?>) -> ptr<?><br>extern QPainter_set_pen: (ptr<?>, ptr<?>) -> int<br>extern QPainter_set_brush: (ptr<?>, ptr<?>) -> int<br>extern QPainter_delete: (ptr<?>) -> int<br>extern QPainter_set_opacity: (ptr<?>, float) -> int<br>extern QPainter_draw_line: (ptr<?>, int, int, int, int) -> int<br>extern QPainter_draw_rect: (ptr<?>, int, int, int, int) -> int<br>extern QPainter_draw_pixmap: (ptr<?>, int, int, int, int, ptr<?>) -> int<br><br>public lostanza deftype QPainter <: Resource :<br> value:ptr<?><br> <br>public lostanza defn QPainter (w:ref<QWidget>) -> ref<QPainter> :<br> return new QPainter{call-c QPainter_new(w.value)}<br> <br>public lostanza defn set-pen (w:ref<QPainter>, p:ref<QPen>) -> ref<False> :<br> call-c QPainter_set_pen(w.value, p.value)<br> return false<br><br>public lostanza defn set-brush (w:ref<QPainter>, b:ref<QBrush>) -> ref<False> :<br> call-c QPainter_set_brush(w.value, b.value)<br> return false<br><br>public lostanza defn set-opacity (w:ref<QPainter>, o:ref<Float>) -> ref<False> :<br> call-c QPainter_set_opacity(w.value, o.value)<br> return false<br><br>public lostanza defn draw-line (w:ref<QPainter>, x:ref<Int>, y:ref<Int>,<br> x2:ref<Int>, y2:ref<Int>) -> ref<False> :<br> call-c QPainter_draw_line(w.value, x.value, y.value, x2.value, y2.value)<br> return false<br><br>public lostanza defn draw-rect (w:ref<QPainter>, x:ref<Int>, y:ref<Int>,<br> width:ref<Int>, height:ref<Int>) -> ref<False> :<br> call-c QPainter_draw_rect(w.value, x.value, y.value, width.value, height.value)<br> return false<br><br>public lostanza defn draw-pixmap (w:ref<QPainter>, x:ref<Int>, y:ref<Int>,<br> width:ref<Int>, height:ref<Int>,<br> pixmap:ref<QPixmap>) -> ref<False> :<br> call-c QPainter_draw_pixmap(w.value, x.value, y.value,<br> width.value, height.value, pixmap.value)<br> return false<br><br>lostanza defmethod free (w:ref<QPainter>) -> ref<False> :<br> call-c QPainter_delete(w.value)<br> return false</code></pre><h3 id="anchor150">QWidget</h3><p>The <code>QWidget</code> type is also defined as a <code>Resource</code> so that it may be freed at the end of its scope.</p><pre><code>extern QtWidget_new: (ptr<?>) -> ptr<?><br>extern QtWidget_delete: (ptr<?>) -> int<br>extern QtWidget_show: (ptr<?>) -> int<br>extern QtWidget_update: (ptr<?>) -> int<br>extern QtWidget_set_width: (ptr<?>, int) -> int<br>extern QtWidget_set_height: (ptr<?>, int) -> int<br>extern QtWidget_set_listener: (ptr<?>, int) -> int<br>extern QtWidget_listener: (ptr<?>) -> int<br><br>public lostanza deftype QWidget <: Resource :<br> value: ptr<?><br><br>public lostanza defn QWidget (parent:ref<QWidget>) -> ref<QWidget> :<br> return new QWidget{call-c QtWidget_new(parent.value)}<br><br>public lostanza defn QWidget () -> ref<QWidget> :<br> return new QWidget{call-c QtWidget_new(0L as ptr<?>)}<br><br>public lostanza defn set-width (w:ref<QWidget>, x:ref<Int>) -> ref<False> :<br> call-c QtWidget_set_width(w.value, x.value)<br> return false<br><br>public lostanza defn set-height (w:ref<QWidget>, x:ref<Int>) -> ref<False> :<br> call-c QtWidget_set_height(w.value, x.value)<br> return false<br><br>public lostanza defn set-listener (w:ref<QWidget>,<br> x:ref<QWidgetListener>) -> ref<False> :<br> call-c QtWidget_set_listener(w.value, box-object(x))<br> return false<br><br>public lostanza defn show (w:ref<QWidget>) -> ref<False> :<br> call-c QtWidget_show(w.value)<br> return false<br><br>public lostanza defn update (w:ref<QWidget>) -> ref<False> :<br> call-c QtWidget_update(w.value)<br> return false<br><br>lostanza defmethod free (w:ref<QWidget>) -> ref<False> :<br> val listener = call-c QtWidget_listener(w.value)<br> if listener >= 0 :<br> free-box(listener)<br> call-c QtWidget_delete(w.value)<br> return false</code></pre><p>Notice especially the calls to <code>box-object</code> and <code>free-box</code> in the <code>set-listener</code> and <code>free</code> 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. <code>box-object</code> 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 <code>free-box</code> when the object is no longer used.</p><p>A <code>QWidgetListener</code> is a simple type that supports methods for each of the callbacks we're interested in from <code>QWidget</code>. The default implementations for each of the multis do not do anything.</p><pre><code>public deftype QWidgetListener<br>public defmulti painted (l:QWidgetListener) -> ?<br>public defmulti mouse-pressed (l:QWidgetListener, e:QMouseEvent) -> ?<br>public defmulti mouse-released (l:QWidgetListener, e:QMouseEvent) -> ?<br>public defmulti mouse-moved (l:QWidgetListener, e:QMouseEvent) -> ?<br>public defmulti key-pressed (l:QWidgetListener, e:QKeyEvent) -> ?<br><br>defmethod painted (l:QWidgetListener) : false<br>defmethod mouse-pressed (l:QWidgetListener, e:QMouseEvent) : false<br>defmethod mouse-released (l:QWidgetListener, e:QMouseEvent) : false<br>defmethod mouse-moved (l:QWidgetListener, e:QMouseEvent) : false<br>defmethod key-pressed (l:QWidgetListener, e:QKeyEvent) : false</code></pre><h3 id="anchor151">QTimer</h3><p>The <code>QTimer</code> type is defined as a <code>Resource</code> so that it may be freed at the end of its scope. A <code>QTimer</code> is created from a callback function and the time interval between each call to the callback function. Similar to how <code>set-listener</code> is implemented for <code>QWidget</code>, the constructor function stores the <code>on-tick</code> callback function into a box and then passes the box's identifier to the <code>QTimer_new</code> function. When the <code>QTimer</code> is freed, the box containing the callback function is also freed. </p><pre><code>extern QTimer_new: (int, int) -> ptr<?><br>extern QTimer_delete: (ptr<?>) -> int<br>extern QTimer_callback: (ptr<?>) -> int<br>extern QTimer_start: (ptr<?>) -> int<br>extern QTimer_stop: (ptr<?>) -> int<br><br>extern defn call_function (func:int) -> int :<br> val f = boxed-object(func) as ref<(() -> ?)><br> [f]()<br> return 0<br><br>public lostanza deftype QTimer <: Resource :<br> value: ptr<?><br><br>public lostanza defn QTimer (on-tick:ref<(() -> ?)>, interval:ref<Int>) -> ref<QTimer> :<br> val t = call-c QTimer_new(box-object(on-tick), interval.value)<br> return new QTimer{t}<br> <br>lostanza defmethod free (t:ref<QTimer>) -> ref<False> :<br> val callback = call-c QTimer_callback(t.value)<br> free-box(callback)<br> call-c QTimer_delete(t.value)<br> return false<br><br>public lostanza defn start (t:ref<QTimer>) -> ref<False> :<br> call-c QTimer_start(t.value)<br> return false<br><br>public lostanza defn stop (t:ref<QTimer>) -> ref<False> :<br> call-c QTimer_stop(t.value)<br> return false</code></pre><p>Note the definition of the <code>call_function</code> external function. It is written to be called from C with the identifier of the box containing the function to call. The first line</p><pre><code>val f = boxed-object(func) as ref<(() -> ?)></code></pre><p>retrieves the stored callback function from the box and casts it to a reference to a function. The second line </p><pre><code>[f]()</code></pre><p>then <span style="font-style:italic;">dereferences</span> the function and calls it. <code>[x]</code> is the syntax in LoStanza for dereferencing a pointer or reference. </p><h3 id="anchor152">Key Codes</h3><p>The 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. </p><pre><code>public val KEY-A = 65<br>public val KEY-B = 66<br>public val KEY-C = 67<br>public val KEY-D = 68<br>public val KEY-E = 69<br>public val KEY-F = 70<br>public val KEY-G = 71<br>public val KEY-H = 72<br>public val KEY-I = 73<br>public val KEY-J = 74<br>public val KEY-K = 75<br>public val KEY-L = 76<br>public val KEY-M = 77<br>public val KEY-N = 78<br>public val KEY-O = 79<br>public val KEY-P = 80<br>public val KEY-Q = 81<br>public val KEY-R = 82<br>public val KEY-S = 83<br>public val KEY-T = 84<br>public val KEY-U = 85<br>public val KEY-V = 86<br>public val KEY-W = 87<br>public val KEY-X = 88<br>public val KEY-Y = 89<br>public val KEY-Z = 90<br>public val KEY-1 = 49<br>public val KEY-2 = 50<br>public val KEY-3 = 51<br>public val KEY-4 = 52<br>public val KEY-5 = 53<br>public val KEY-6 = 54<br>public val KEY-7 = 55<br>public val KEY-8 = 56<br>public val KEY-9 = 57<br>public val KEY-0 = 48<br>public val KEY-UP = 16777235<br>public val KEY-DOWN = 16777237<br>public val KEY-LEFT = 16777234<br>public val KEY-RIGHT = 16777236<br>public val KEY-SPACE = 32</code></pre><h2 id="anchor153">Writing Tetris</h2><p>The implementation of the Tetris game is divided into the following categories:</p><ol><li>The <code>Tile</code> type for representing and managing Tetris tiles.
</li><li>The <code>Board</code> type for representing and managing the Tetris board.
</li><li>The <code>Drawer</code> type for drawing a visual representation of the Tetris board.
</li><li>The <code>Game</code> type for handling timing and key presses for updating and drawing the tetris board.
</li></ol><h3 id="anchor154">Tetris Tiles</h3><p>Every tile in Tetris is represented as an instance of the type <code>Tile</code>. It supports three basic functions, <code>rows</code> and <code>cols</code>, for retrieving the tile's height and width, and <code>dots</code> for retrieving its shape. <code>dots</code> returns a collection of 3-element tuples. The first and second element of each tuple is the row and column of the <code>dot</code>, and the last element is an integer representing the color of the <code>dot</code>.</p><pre><code>deftype Tile<br>defmulti cols (t:Tile) -> Int<br>defmulti rows (t:Tile) -> Int<br>defmulti dots (t:Tile) -> Collection<[Int, Int, Int]></code></pre><p>To create a tile, we supply it the dimensions of the tile, and a collection of either <code>False</code> or <code>Int</code> values for indicating either the absence or color of a dot. </p><pre><code>defn Tile (cols:Int, rows:Int, dots:Seqable<False|Int>) :<br> val dot-seq = to-seq(dots)<br> val dot-tuple = to-tuple $<br> for r in (rows - 1) through 0 by -1 seq-cat :<br> for c in 0 to cols seq? :<br> match(next(dot-seq)) :<br> (dot:Int) : One([r, c, dot])<br> (dot:False) : None()<br> new Tile :<br> defmethod cols (this) : cols<br> defmethod rows (this) : rows<br> defmethod dots (this) : dot-tuple</code></pre><p>We can now create a collection containing all of the standard tiles in Tetris.</p><pre><code>val ALL-TILES = let :<br> val _ = false<br> [<br> ;I Tile<br> Tile(4, 4,<br> [_ _ 0 _<br> _ _ 0 _<br> _ _ 0 _<br> _ _ 0 _])<br><br> ;J Tile<br> Tile(3, 3,<br> [_ 1 _<br> _ 1 _<br> 1 1 _])<br><br> ;L Tile<br> Tile(3, 3,<br> [_ 2 _<br> _ 2 _<br> _ 2 2])<br><br> ;O Tile<br> Tile(2, 2,<br> [3 3<br> 3 3])<br><br> ;S Tile<br> Tile(3, 3,<br> [_ 4 4<br> 4 4 _<br> _ _ _])<br><br> ;Z Tile<br> Tile(3, 3,<br> [6 6 _<br> _ 6 6<br> _ _ _])<br><br> ;T Tile<br> Tile(3, 3,<br> [_ 5 _<br> 5 5 5<br> _ _ _])<br> ]</code></pre><p>The <code>rotate</code> function takes a <code>Tile</code> as input and returns a new <code>Tile</code> representing the same tile rotated counter-clockwise by 90 degrees.</p><pre><code>defn rotate (t:Tile) :<br> new Tile :<br> defmethod cols (this) : rows(t)<br> defmethod rows (this) : cols(t)<br> defmethod dots (this) :<br> new Collection<[Int, Int, Int]> :<br> defmethod to-seq (this) :<br> for [r, c, color] in dots(t) seq :<br> [c, rows(t) - r - 1, color]</code></pre><p>The <code>random-tile</code> function returns a random tile selected from the collection of standard Tetris tiles.</p><pre><code>defn random-tile () :<br> val n = length(ALL-TILES)<br> ALL-TILES[rand(n)]</code></pre><h3 id="anchor155">The Tetris Board</h3><p>The 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 <span style="font-style:italic;">falling</span>. Once it falls to the bottom or hits an occupied cell then it is <span style="font-style:italic;">stamped</span> 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.</p><p>The <code>Board</code> type supports the following operations.</p><pre><code>deftype Board<br>defmulti rows (b:Board) -> Int<br>defmulti cols (b:Board) -> Int<br>defmulti vis-rows (b:Board) -> Int<br>defmulti reset (b:Board) -> False<br>defmulti get (b:Board, r:Int, c:Int) -> False|Int<br>defmulti active-tile (b:Board) -> False|Tile<br>defmulti active-tile-pos (b:Board) -> [Int, Int]<br>defmulti spawn-tile (b:Board) -> False<br>defmulti rotate-tile (b:Board) -> False|True<br>defmulti slide-tile (b:Board, dr:Int, dc:Int) -> False|True<br>defmulti stamp-active-tile (b:Board) -> False<br>defmulti active-tile-on-ground? (b:Board) -> True|False<br>defmulti game-over? (b:Board) -> False|True</code></pre><p><code>rows</code> returns the number of rows on the board (25).</p><p><code>cols</code> returns the number of columns on the board (10).</p><p><code>vis-rows</code> returns the number of visible rows on the board (20).</p><p><code>get</code> returns the status of the cell at row <code>r</code>, and column <code>c</code>. The integer representing the color of the dot is returned if the cell is occupied, otherwise <code>false</code> is returned.</p><p><code>active-tile</code> returns the <code>Tile</code> representing it if there is one.</p><p><code>active-tile-pos</code> returns a tuple containing the row and column of the currently active tile.</p><p><code>spawn-tile</code> sets the currently active tile to a newly spawned random tile in the invisible section of the board.</p><p><code>rotate-tile</code> rotates the currently active tile. It returns <code>true</code> if the rotation was successful.</p><p><code>slide-tile</code> slides the currently active tile by <code>dr</code> rows and <code>dc</code> columns. It returns <code>true</code> if the slide was successful.</p><p><code>stamp-active-tile</code> stamps the currently active tile onto the board.</p><p><code>active-tile-on-ground?</code> returns <code>true</code> if the currently active tile is sitting either on the bottom or on top of an occupied dot.</p><p><code>game-over?</code> returns <code>true</code> if the game is over, caused by an invisible row being occupied by a dot.</p><p><code>reset</code> resets the game by clearing all the cells and setting no tile as active.</p><pre><code>defn Board () :<br> ;Board Stats<br> val num-rows = 25<br> val num-vis-rows = 20<br> val num-cols = 10<br> val board = Array<Int|False>(num-rows * num-cols, false)<br> defn board-get (r:Int, c:Int) : board[r * num-cols + c]<br> defn board-set (r:Int, c:Int, color:False|Int) : board[r * num-cols + c] = color<br><br> ;Tile Stats<br> var tile:False|Tile = false<br> var tile-pos:[Int, Int] = [0, 0]<br><br> ;Game Stats<br> var game-over? = false<br><br> ;Stamp a tile at a location<br> defn stamp (t:Tile, r:Int, c:Int) :<br> for [tr, tc, color] in dots(t) do :<br> val dr = r + tr<br> val dc = c + tc<br> fatal("Illegal Stamp") when (not in-bounds?(dr, dc)) or occupied?(dr, dc)<br> board-set(dr, dc, color) <br><br> ;Does a tile fit in a given location?<br> defn in-bounds? (r:Int, c:Int) :<br> r >= 0 and c >= 0 and r < num-rows and c < num-cols<br> defn occupied? (r:Int, c:Int) :<br> board-get(r, c) is Int<br> defn fits? (t:Tile, r:Int, c:Int) :<br> for [tr, tc, _] in dots(t) all? :<br> val dr = r + tr<br> val dc = c + tc<br> in-bounds?(dr, dc) and not occupied?(dr, dc)<br> defn kick (t:Tile, r:Int, c:Int) :<br> val cl = for i in 1 to cols(t) find : fits?(t, r, c - i)<br> val cr = for i in 1 to cols(t) find : fits?(t, r, c + i)<br> match(cl, cr) :<br> (cl:False, cr:False) : false<br> (cl:Int, cr:False) : (- cl)<br> (cl:False, cr:Int) : cr<br> (cl:Int, cr:Int) : cr when cr < cl else (- cl)<br><br> ;Find and clear full lines<br> defn clear-filled-lines () :<br> defn copy-row (r1:Int, r2:Int) :<br> if r1 != r2 :<br> for c in 0 to num-cols do :<br> board-set(r2, c, board-get(r1, c))<br> defn clear-row (r:Int) :<br> for c in 0 to num-cols do :<br> board-set(r, c, false)<br> defn filled? (r:Int) :<br> all?(occupied?{r, _}, 0 to num-cols)<br> val filled-rows = to-seq(0 through num-rows)<br> for r in 0 to num-rows do :<br> copy-row(r, next(filled-rows)) when not filled?(r)<br> do(clear-row, next(filled-rows) to num-rows) <br><br> ;Have we lost?<br> defn check-game-over? () :<br> for r in num-vis-rows to num-rows any? :<br> any?(occupied?{r, _} 0 to num-cols) <br> <br> new Board :<br> defmethod rows (this) : num-rows<br> defmethod vis-rows (this) : num-vis-rows<br> defmethod cols (this) : num-cols<br> defmethod active-tile (this) : tile<br><br> defmethod reset (this) :<br> board[0 to false] = repeat(false)<br> tile = false<br> game-over? = false<br> <br> defmethod active-tile-pos (this) :<br> fatal("No active tile") when tile is False<br> tile-pos<br><br> defmethod get (this, r:Int, c:Int) -> False|Int :<br> fatal("Out of Bounds") when not in-bounds?(r, c)<br> board-get(r, c)<br> <br> defmethod slide-tile (this, dr:Int, dc:Int) :<br> fatal("Game is over") when game-over?<br> val [tr, tc] = tile-pos<br> if fits?(tile as Tile, tr + dr, tc + dc) :<br> tile-pos = [tr + dr, tc + dc]<br> true<br> <br> defmethod rotate-tile (this) :<br> fatal("Game is over") when game-over?<br> val [tr, tc] = tile-pos<br> val rtile = rotate(tile as Tile)<br> if fits?(rtile, tr, tc) :<br> tile = rtile<br> true<br> else :<br> match(kick(rtile, tr, tc)) :<br> (dc:Int) :<br> tile = rtile<br> tile-pos = [tr, tc + dc]<br> true<br> (dc:False) :<br> false <br><br> defmethod stamp-active-tile (this) :<br> fatal("Game is over") when game-over?<br> val [r, c] = tile-pos<br> stamp(tile as Tile, r, c)<br> tile = false<br> clear-filled-lines()<br> game-over? = check-game-over?()<br> <br> defmethod spawn-tile (this) :<br> fatal("Game is over") when game-over?<br> fatal("Tile Exists") when tile is Tile<br> tile = random-tile()<br> tile-pos = [20, 3]<br> <br> defmethod game-over? (this) :<br> game-over?<br> <br> defmethod active-tile-on-ground? (this) :<br> val [tr, tc] = active-tile-pos(this)<br> not fits?(tile as Tile, tr - 1, tc)</code></pre><h3 id="anchor156">Drawing the Board</h3><p>The <code>Drawer</code> type is very simple and contains two simple functions.</p><pre><code>deftype Drawer<br>defmulti draw (d:Drawer, w:QWidget) -> False<br>defmulti size (d:Drawer) -> [Int, Int]</code></pre><p><code>draw</code> draws the Tetris board onto the given widget <code>w</code>.</p><p><code>size</code> returns the size of the board in pixels. The first element of the returned tuple is the width and the second is the height.</p><pre><code>defn Drawer (b:Board) : <br> ;Coordinate system<br> val [bx, by] = [10, 10]<br> val [dx, dy] = [24, 24]<br> defn coord (r:Int, c:Int) :<br> val maxy = by + vis-rows(b) * dy<br> [bx + c * dx,<br> maxy - r * dy - dy]<br><br> defn visible? (r:Int, c:Int) :<br> r >= 0 and c >= 0 and<br> r < vis-rows(b) and c < cols(b)<br><br> ;Tile Colored Brushes<br> val colored-brushes = to-tuple{seq(QBrush, _)} $ [<br> QColor(0, 255, 255, 255)<br> QColor(0, 0, 255, 255)<br> QColor(255, 165, 0, 255)<br> QColor(255, 255, 0, 255)<br> QColor(0, 255, 0, 255)<br> QColor(139, 0, 139, 255)<br> QColor(255, 0, 0, 255)]<br><br> ;Colored Pens<br> val white = QColor(255, 255, 255, 255)<br> val gray = QColor(230, 230, 230, 255)<br> val black = QColor(0, 0, 0, 255)<br> val white-pen = QPen(white, 2)<br> val black-pen = QPen(black, 2)<br> val gray-brush = QBrush(gray)<br> <br> new Drawer :<br> defmethod size (this) :<br> [cols(b) * dx + 2 * bx<br> vis-rows(b) * dy + 2 * by]<br> <br> defmethod draw (this, w:QWidget) :<br> resource p = QPainter(w)<br> <br> ;Draw Grid<br> set-pen(p, white-pen)<br> set-brush(p, gray-brush)<br> for r in 0 to vis-rows(b) do :<br> for c in 0 to cols(b) do :<br> val [x, y] = coord(r, c)<br> draw-rect(p, x, y, dx, dy)<br> <br> ;Draw tiles<br> set-pen(p, black-pen) <br> for r in 0 to vis-rows(b) do :<br> for c in 0 to cols(b) do :<br> match(b[r, c]) :<br> (color:Int) :<br> val [x, y] = coord(r, c)<br> set-brush(p, colored-brushes[color])<br> draw-rect(p, x, y, dx, dy)<br> (color:False) : false<br> <br> ;Draw active tile<br> match(active-tile(b)) :<br> (t:Tile) :<br> val [r, c] = active-tile-pos(b)<br> for [tr, tc, color] in dots(t) do :<br> if visible?(r + tr, c + tc) :<br> val [x, y] = coord(r + tr, c + tc)<br> set-brush(p, colored-brushes[color])<br> draw-rect(p, x, y, dx, dy)<br> (t:False) :<br> false</code></pre><p>Note the use of the <code>resource</code> keyword in the creation of the <code>QPainter</code> object. This is mandated by the QT library. The QPainter object must be freed at the end of the draw method. </p><h3 id="anchor157">Controlling the Game</h3><p>The Tetris game controller will handle the game timing and keyboard events.</p><pre><code>deftype Game<br>defmulti update (g:Game) -> False<br>defmulti key-pressed (g:Game, k:Int) -> False<br>defmulti draw (g:Game, widget:QWidget) -> False<br>defmulti size (g:Game) -> [Int, Int]</code></pre><p>The <code>key-pressed</code> function is called each time a keyboard key is pressed.</p><p>The <code>update</code> function is assumed to be called sixty times a second, each time followed by a call to <code>draw</code>. </p><p>The <code>size</code> function returns the dimensions of the playing board.</p><pre><code>defn Game () :<br> ;Game components<br> val b = Board()<br> val drawer = Drawer(b)<br> <br> ;Block drop timer<br> val normal-period = 30<br> val drop-period = 1<br> var period = 30<br> var drop-timer = period<br><br> ;Spawn tile<br> defn drop-next-tile () :<br> spawn-tile(b)<br> period = normal-period<br><br> ;Safety timer<br> defn add-safety-time () :<br> if active-tile-on-ground?(b) :<br> drop-timer = period<br><br> ;Spawn initial tile<br> drop-next-tile()<br> new Game :<br> defmethod update (this) :<br> if not game-over?(b) :<br> drop-timer = drop-timer - 1<br> if drop-timer <= 0 :<br> drop-timer = period<br> if not slide-tile(b, -1, 0) :<br> stamp-active-tile(b)<br> drop-next-tile() when not game-over?(b)<br> <br> defmethod key-pressed (this, k:Int) :<br> if game-over?(b) :<br> if k == KEY-SPACE :<br> reset(b)<br> drop-timer = period<br> drop-next-tile()<br> else :<br> switch {k == _} :<br> KEY-UP :<br> if rotate-tile(b) :<br> add-safety-time()<br> KEY-DOWN :<br> slide-tile(b, -1, 0)<br> KEY-LEFT :<br> if slide-tile(b, 0, -1) :<br> add-safety-time()<br> KEY-RIGHT :<br> if slide-tile(b, 0, 1) :<br> add-safety-time()<br> KEY-SPACE :<br> drop-timer = 0<br> period = drop-period<br> else :<br> false<br> false <br> <br> defmethod draw (this, w:QWidget) :<br> draw(drawer, w)<br> <br> defmethod size (this) :<br> size(drawer)</code></pre><p>The timing is controlled by the <code>period</code> and <code>drop-timer</code> variables. The <code>drop-timer</code> variable decreases by one each time <code>update</code> is called. When it hits zero, then the currently active tile drops by one, and the <code>drop-timer</code> is reset to the <code>period</code>. The <code>period</code>, by default, is equal to <code>normal-period</code>, but a user may speed it up to <code>drop-period</code> by pressing the space bar.</p><p>In the <code>update</code> function, the currently active tile is dropped by one every time the <code>drop-timer</code> hits zero. If the active tile cannot be dropped, then it has hit the ground and it is stamped onto the board.</p><p>The <code>key-pressed</code> function is straightforward except for the call to <code>add-safety-time</code>. The Tetris mechanics allows players to <span style="font-style:italic;">slide</span> tiles indefinitely even after hitting the ground. Every time a tile is successfully slid or rotated, <code>drop-timer</code> is reset if the tile is touching the ground.</p><h3 id="anchor158">Putting it Together</h3><p>Finally we put everything together by first creating a <code>QApplication</code> object as mandated by QT, and a <code>QWidget</code> object for drawing our Tetris game. The widget's listener will forward events to the <code>Game</code> object. We will use a <code>QTimer</code> with a period of 16 milliseconds to update and paint our game sixty times per second.</p><pre><code>defn main () :<br> resource app = QApplication()<br> resource widget = QWidget()<br> val game = Game()<br> <br> ;Set size<br> val [w, h] = size(game)<br> set-width(widget, w)<br> set-height(widget, h)<br> <br> ;Event Handling<br> set-listener{widget, _} $<br> new QWidgetListener :<br> defmethod painted (this) :<br> draw(game, widget)<br> defmethod key-pressed (this, e:QKeyEvent) :<br> key-pressed(game, key(e))<br> <br> ;Set Framerate Timer<br> resource timer = QTimer{_, 16} $ fn () :<br> update(game)<br> update(widget)<br> start(timer)<br> <br> ;Start!<br> show(widget)<br> exec(app)<br><br>main()</code></pre><p>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.</p><h2 id="anchor159">More Additions</h2><p>The Tetris game presented here is very simple and is missing various features. To flesh out the game, you can add the following features:</p><ol><li>Gradually speed up the rate at which the tile falls.
</li><li>Draw a display to show the next block that will be dropped.
</li><li>Display a score counter and invent a way to compute the player's score.
</li><li>Draw an effect to notify the player whenever s/he completes a tetris by clearing four lines at once.
</li><li>Add controls to rotate the tiles clock-wise and counter-clock-wise.
</li><li>Add fancier animations when clearing lines and dropping tiles.
</li></ol>
</td>
<td class="rest">
<img url="resources/spacer.gif"></img>
</td>
</tr>
<tr><td colspan="3" class="footer">
Site design by Luca Li. Copyright 2015.
</td></tr>
</table>
</body>
</html>