Tetris in D

Posted 2020-08-03

I'm going to start a new series this week, called "X in D". Each entry will be a small program to implement some simple concept. But, I will not limit to just the simple concept like I do for documentation examples - I'll go a little beyond the basics for more fun.

Core D Development Statistics

In the community

Community announcements

See more at the announce forum.

Tetris in D

First, we need to set up. Get my arsd repo so you can dmd -i with it.

https://github.com/adamdruppe/arsd

Either git clone it or download the zip and rename the folder to arsd, putting it in your working directory. I may be using several modules in there as time goes on, so while I normally just list the specific files you need, here I suggest you grab the whole thing. dmd -i will automatically pick out the ones you need when compiling as long as the arsd folder is right there. (Or you can put it somewhere else and consistently pass -I/path/to/arsd/.. - yes, the parent directory that contains the arsd folder, not the arsd folder itself, to dmd.)

After that, you're ready to go.

Here's tetris.d:

1 import arsd.simpledisplay;
2 import arsd.simpleaudio;
3 
4 enum PieceSize = 16;
5 
6 enum SettleStatus {
7 	none,
8 	settled,
9 	cleared,
10 	tetris,
11 	gameOver,
12 }
13 
14 class Board {
15 	int width;
16 	int height;
17 	int[] state;
18 	int score;
19 	this(int width, int height) {
20 		state = new int[](width * height);
21 		this.width = width;
22 		this.height = height;
23 	}
24 
25 	SettleStatus settlePiece(Piece piece) {
26 		if(piece.y <= 0)
27 			return SettleStatus.gameOver;
28 
29 		SettleStatus status = SettleStatus.settled;
30 		foreach(yo, line; pieces[piece.type][piece.rotation]) {
31 			int mline = line;
32 			foreach(xo; 0 .. 4) {
33 				if(mline & 0b1000)
34 					state[(piece.y+yo) * width + xo + piece.x] = piece.type + 1;
35 				mline <<= 1;
36 			}
37 		}
38 
39 		int[4] del;
40 		int delPos = 0;
41 
42 		foreach(y; piece.y .. piece.y + 4) {
43 			int presentCount;
44 			if(y >= height)
45 				break;
46 			foreach(x; 0 .. width)
47 				if(state[y * width + x])
48 					presentCount++;
49 
50 			if(presentCount == width) {
51 				del[delPos++] = y;
52 				status = SettleStatus.cleared;
53 			}
54 		}
55 
56 		if(delPos == 4) {
57 			score += 4; // tetris bonus!
58 			status = SettleStatus.tetris;
59 		}
60 
61 		foreach(p; 0 .. delPos) {
62 			foreach_reverse(y; 0 .. del[p])
63 				state[(y + 1) * width .. (y + 2) * width] = state[(y + 0) * width .. (y + 1) * width];
64 			state[0 .. width] = 0;
65 
66 			score++;
67 		}
68 
69 		return status;
70 
71 		/+
72 		import std.stdio;
73 		writeln;
74 		writeln;
75 		foreach(y; 0 .. height) {
76 			foreach(x; 0 .. width) {
77 				write(state[y * width + x]);
78 			}
79 			writeln("");
80 		}
81 		+/
82 	}
83 
84 	SettleStatus trySettle(Piece piece) {
85 		auto pieceMap = pieces[piece.type][piece.rotation];
86 		int ph = 4;
87 		foreach_reverse(line; pieceMap) {
88 			if(line)
89 				break;
90 			ph--;
91 		}
92 		if(ph + piece.y >= this.height) {
93 			if(!piece.settleNextFrame) {
94 				piece.settleNextFrame = true;
95 				return SettleStatus.none;
96 			} else {
97 				return settlePiece(piece);
98 			}
99 		}
100 		piece.y++;
101 		if(collisionDetect(piece)) {
102 			piece.y--;
103 
104 			if(!piece.settleNextFrame) {
105 				piece.settleNextFrame = true;
106 				return SettleStatus.none;
107 			} else {
108 				return settlePiece(piece);
109 			}
110 		} else {
111 			piece.settleNextFrame = false;
112 		}
113 		piece.y--;
114 		return SettleStatus.none;
115 	}
116 
117 	bool collisionDetect(Piece piece) {
118 		auto pieceMap = pieces[piece.type][piece.rotation];
119 		foreach_reverse(yo,line; pieceMap) {
120 			int mline = line;
121 			foreach(xo; 0 .. 4) {
122 				if(mline & 0b1000) {
123 					if(state[(piece.y+yo) * this.width + xo + piece.x])
124 						return true;
125 				}
126 				mline <<= 1;
127 			}
128 		}
129 		return false;
130 	}
131 
132 	void redraw(SimpleWindow window) {
133 		auto painter = window.draw();
134 		int x, y;
135 		foreach(s; state) {
136 			painter.fillColor = s ? palette[s - 1] : Color.black;
137 			painter.outlineColor = s ? Color.white : Color.black;
138 			painter.drawRectangle(Point(x, y) * PieceSize, PieceSize, PieceSize);
139 			x++;
140 			if(x == width) {
141 				x = 0;
142 				y++;
143 			}
144 		}
145 	}
146 }
147 
148 static immutable ubyte[][][] pieces = [
149 	// long straight
150 	[[0b1000,
151 	  0b1000,
152 	  0b1000,
153 	  0b1000],
154 	 [0b1111,
155 	  0b0000,
156 	  0b0000,
157 	  0b0000]],
158 	 // l
159 	[[0b1000,
160 	  0b1000,
161 	  0b1100,
162 	  0b0000],
163 	 [0b0010,
164 	  0b1110,
165 	  0b0000,
166 	  0b0000],
167 	 [0b1100,
168 	  0b0100,
169 	  0b0100,
170 	  0b0000],
171 	 [0b1110,
172 	  0b1000,
173 	  0b0000,
174 	  0b0000]],
175 	 // j
176 	[[0b0100,
177 	  0b0100,
178 	  0b1100,
179 	  0b0000],
180 	 [0b1000,
181 	  0b1110,
182 	  0b0000,
183 	  0b0000],
184 	 [0b1100,
185 	  0b1000,
186 	  0b1000,
187 	  0b0000],
188 	 [0b1110,
189 	  0b0010,
190 	  0b0000,
191 	  0b0000]],
192 	 // n
193 	[[0b1100,
194 	  0b0110,
195 	  0b0000,
196 	  0b0000],
197 	 [0b0100,
198 	  0b1100,
199 	  0b1000,
200 	  0b0000]],
201 	 // other n
202 	[[0b0110,
203 	  0b1100,
204 	  0b0000,
205 	  0b0000],
206 	 [0b1000,
207 	  0b1100,
208 	  0b0100,
209 	  0b0000]],
210 	 // t
211 	[[0b0100,
212 	  0b1110,
213 	  0b0000,
214 	  0b0000],
215 	 [0b1000,
216 	  0b1100,
217 	  0b1000,
218 	  0b0000],
219 	 [0b1110,
220 	  0b0100,
221 	  0b0000,
222 	  0b0000],
223 	 [0b0100,
224 	  0b1100,
225 	  0b0100,
226 	  0b0000]],
227 	// square
228 	[[0b1100,
229 	  0b1100,
230 	  0b0000,
231 	  0b0000]],
232 ];
233 
234 immutable Color[] palette = [
235 	Color.red,
236 	Color.blue,
237 	Color.green,
238 	Color.yellow,
239 	Color.teal,
240 	Color.purple,
241 	Color.gray
242 ];
243 
244 static assert(palette.length == pieces.length);
245 
246 class Piece {
247 	SimpleWindow window;
248 	Board board;
249 	this(SimpleWindow window, Board board) {
250 		this.window = window;
251 		this.board = board;
252 	}
253 
254 	static int randomType() {
255 		import std.random;
256 		return uniform(0, cast(int) pieces.length);
257 	}
258 
259 	int width() {
260 		int fw = 0;
261 		foreach(int s; pieces[type][rotation]) {
262 			int w = 4;
263 			while(w && ((s & 1) == 0)) {
264 				w--;
265 				s >>= 1;
266 			}
267 			if(w > fw)
268 				fw = w;
269 		}
270 		return fw;
271 	}
272 
273 	void reset(int type) {
274 		this.type = type;
275 		rotation = 0;
276 		x = board.width / 2 + 1;
277 		y = 0;
278 		settleNextFrame = false;
279 	}
280 
281 	int type;
282 	int rotation;
283 
284 	int x;
285 	int y;
286 
287 	bool settleNextFrame;
288 
289 	void erase() {
290 		draw(true);
291 	}
292 
293 	void draw(bool erase = false) {
294 		auto painter = window.draw();
295 		painter.fillColor = erase ? Color.black : palette[type];
296 		painter.outlineColor = erase ? Color.black : Color.white;
297 		foreach(yo, line; pieces[type][rotation]) {
298 			int mline = line;
299 			foreach(xo; 0 .. 4) {
300 				if(mline & 0b1000)
301 					painter.drawRectangle(Point(cast(int) (x + xo), cast(int) (y + yo)) * PieceSize, PieceSize, PieceSize);
302 				mline <<= 1;
303 			}
304 		}
305 	}
306 
307 	void moveDown() {
308 		if(!settleNextFrame) {
309 			y++;
310 			if(board.collisionDetect(this))
311 				y--;
312 		}
313 	}
314 
315 	void moveLeft() {
316 		if(x) {
317 			x--;
318 			if(board.collisionDetect(this))
319 				x++;
320 		}
321 	}
322 
323 	void moveRight() {
324 		if(x + width < board.width) {
325 			x++;
326 			if(board.collisionDetect(this))
327 				x--;
328 		}
329 	}
330 
331 	void rotate() {
332 		rotation++;
333 		if(rotation >= pieces[type].length)
334 			rotation = 0;
335 		if(x + width > board.width)
336 			x = board.width - width;
337 	}
338 }
339 
340 void main() {
341 	auto audio = AudioOutputThread(0);
342 	audio.start();
343 
344 	auto board = new Board(15, 25);
345 
346 	auto window = new SimpleWindow(board.width * PieceSize, board.height * PieceSize, "Detris");
347 
348 	// clear screen to black
349 	{
350 		auto painter = window.draw();
351 		painter.outlineColor = Color.black;
352 		painter.fillColor = Color.black;
353 		painter.drawRectangle(Point(0, 0), Size(window.width, window.height));
354 	}
355 
356 	Piece currentPiece = new Piece(window, board);
357 
358 	int frameCounter;
359 	bool downPressed;
360 
361 	int gameOverY = 0;
362 
363 	int difficulty = 1;
364 
365 	window.eventLoop(100 / 5, () {
366 
367 		if(gameOverY > board.height + 2)
368 			window.close();
369 
370 		if(frameCounter <= 0) {
371 			currentPiece.erase();
372 			currentPiece.moveDown();
373 			currentPiece.draw();
374 			auto sb = board.score;
375 			bool donew = false;
376 			final switch (board.trySettle(currentPiece)) {
377 				case SettleStatus.none:
378 				break;
379 				case SettleStatus.settled:
380 					audio.beep(400);
381 					donew = true;
382 				break;
383 				case SettleStatus.cleared:
384 					audio.beep(1100);
385 					audio.beep(400);
386 					donew = true;
387 				break;
388 				case SettleStatus.tetris:
389 					audio.beep(1200);
390 					audio.beep(400);
391 					audio.beep(700);
392 					donew = true;
393 				break;
394 				case SettleStatus.gameOver:
395 					audio.boop();
396 
397 					board.redraw(window);
398 
399 					auto painter = window.draw();
400 					painter.outlineColor = Color.white;
401 					painter.fillColor = Color(127, 127, 127);
402 					painter.drawRectangle(Point(0, 0), Size(window.width, gameOverY * PieceSize));
403 					gameOverY++;
404 				break;
405 			}
406 
407 			if(donew) {
408 				currentPiece.reset(Piece.randomType());
409 				if(board.score != sb)
410 					board.redraw(window);
411 			}
412 
413 			frameCounter = 2 * 5 * 3;
414 		}
415 
416 		if(downPressed)
417 			frameCounter -= 12;
418 		frameCounter -= difficulty;
419 	}, (KeyEvent kev) {
420 		if(kev.key == Key.Down)
421 			downPressed = kev.pressed;
422 		if(!kev.pressed) return;
423 		switch(kev.key) {
424 			case Key.Space:
425 				currentPiece.erase();
426 				currentPiece.rotate();
427 				currentPiece.draw();
428 				audio.beep(1100);
429 				audio.beep(1000);
430 			break;
431 			case Key.Left:
432 				currentPiece.erase();
433 				currentPiece.moveLeft();
434 				currentPiece.draw();
435 			break;
436 			case Key.Right:
437 				currentPiece.erase();
438 				currentPiece.moveRight();
439 				currentPiece.draw();
440 			break;
441 			case Key.LeftBracket:
442 				if(difficulty)
443 					difficulty--;
444 			break;
445 			case Key.RightBracket:
446 				if(difficulty < 10)
447 					difficulty++;
448 			break;
449 			default:
450 		}
451 	});
452 
453 	import std.stdio;
454 	writeln(board.score);
455 }

simpledisplay.d and simpleaudio.d are only fully supported on Windows and Linux. And yes, it may make beeps as you play, so make sure your volume isn't too high.

This might not be your first time seeing this code. I first wrote it about two years ago while on an airplane flight, and just fixed a few small bugs since then. But at this point, it is playable which means I stopped coding and started playing!

Move the piece with arrows. Use spacebar to rotate. Use [ and ] keys to decrease and increase the speed of play.

I may revisit this program later in the series, adding more UI decoration and joystick support, etc., but for now, here's the little gane. Fun fact: it is pretty playable on remote X connections too!