summaryrefslogtreecommitdiff
path: root/docs/projects/sirtet.md
blob: 7003c78f9c2cdbdc738ab71813973599b26afead (plain)
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
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
# SIRTET

2022-06-17

I saw my mother play a mobile game the other day. She invited me to play
along. It was one of these Tetris knockoffs where you drag one of the
three given tetromino-like pieces (although there are many more, some
aren't even continuous) onto an 8x8 map without overlapping, and each time
you fill a row or column it clears up with a pleasant explosion effect.
When you manage to place all three, they give you three more. The game is
over once none of your pieces can be placed. This is fun and all, but
there were ads after each game. I began to ponder: Can I make it in C?

Of course I can. The map is just an 8x8 array (but I wish to make the size
adjustable), and the pieces are just smaller arrays with some metadata.
This just sounds like another CS101 assignment. I just need to be really
careful with pointers and better yet, double pointers.

## First prototype

The initial design is that the map is an array of pointers representing
rows, which point to arrays representing blocks of each row. Each block is
a `char`, which is a space (`0x20`) for vacant blocks and a plus sign
(`0x2b`) otherwise. Both the `char`s and `char*`s were manually malloc'd
so as to avoid variable length arrays (VLA).

As to the pieces, I defined `struct piece` with three attributes:

- Height `int h`
- Width `int w`
- `char* blocks`

The length of `blocks` is `h * w + 1` including the null terminator, and
represent the flattened shape of the piece. Examples are:

- `+` ← a single block
- `++` ← 1x2 or 2x1, depending on `h` and `w`
- `++++  ` ← a 2x3 L shape (see below)

(For consistency, when I say `m x n`, I mean `m` rows by `n` columns.)

I came up with 17 shapes, but a problem arises. If you need a slim
L shape, there are four directions you can rotate it into; moreover, you
can transpose each of them, making it eight:

```
++   ++   +     +
+     +   +     +
+     +   ++   ++

+++  +    +++    +
+    +++    +  +++
```

We can either define all the rotated and transposed versions for these 17
shapes, or implement some rotation and transposition functions, which
turned out not difficult to write. They basically did these four things:

- Take a `struct piece*`, denoted `pc`
- Make a backup of `pc->blocks` in local variable `old`
- Carry out the rotation/transposition from `old` to `pc->blocks`
- Swap `pc->h` and `pc->w`

When handing out a piece to the player I just need to

- Draw a random piece from the pool of 17
- Rotate it 0 to 3 times
- Transpose it 0 or 1 time
- Put its pointer somewhere in the player's array of `struct piece*`s

It turns out, since the pool of 17 should be (and is) immutable, I have to
make a copy before I do anything to them. It is malloc'd and thus has to
be free'd when it is placed on the map.

The rest of the code is just `if`s and `for` loops. Within a few hours
I was able to throw together a stdin-stdout version of the game, which
I named SIRTET (its relationship to Tetris is left to the reader to
decide). Here is a demo of the first two steps of the game:

```
$ ./sirtet
 --------
|        |
|        |
|        |
|        |
|        |
|        |
|        |
|        |
 --------
Piece 0
++
++

Piece 1
   +
++++

Piece 2
++
++
++

Input [piece #] [row #] [col #]: 0 0 0
 --------
|++      |
|++      |
|        |
|        |
|        |
|        |
|        |
|        |
 --------
Piece 1
   +
++++

Piece 2
++
++
++

Input [piece #] [row #] [col #]: 2 2 0
 --------
|++      |
|++      |
|++      |
|++      |
|++      |
|        |
|        |
|        |
 --------
Piece 1
   +
++++

Input [piece #] [row #] [col #]:

```

And guess what? Zero memory leak! Check out this beauty:

```
$ valgrind --leak-check=full --show-leak-kinds=all ./sirtet
==46064== Memcheck, a memory error detector
==46064== Copyright (C) 2002-2022, and GNU GPL'd, by Julian Seward et al.
==46064== Using Valgrind-3.19.0 and LibVEX; rerun with -h for copyright info
==46064== Command: ./sirtet
==46064== 
 --------
|        |
   ....
|        |
 --------
Piece 0
+++
+++
+++

...

Input [piece #] [row #] [col #]: 0 0 0

[15 steps later]

Game over
==46064==
==46064== HEAP SUMMARY:
==46064==     in use at exit: 0 bytes in 0 blocks
==46064==   total heap usage: 115 allocs, 115 frees, 3,069 bytes allocated
==46064==
==46064== All heap blocks were freed -- no leaks are possible
==46064==
==46064== For lists of detected and suppressed errors, rerun with: -s
==46064== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0)
```

I know it's a tiny accomplishment, but it made my day.

Now, a few problems. This `Input [piece #] [row #] [col #]: ` prompt isn't
super friendly, and ideally I should be able to control everything with
a consistent keybinding. And having a map that appears twice as high as
it's wide sure isn't pretty. That's when I thought of ncurses.

## ncurses

I've always wanted to use ncurses somewhere. My CS professor promised
a bonus if I had used ncurses for a course project called One Card, an Uno
knockoff (why are all my projects knockoffs?), but I was struggling to
stop it from segfaulting. Now that I've tackled those, I can finally try
it out. I got my tutorial at [tldp](https://tldp.org/HOWTO/NCURSES-Programming-HOWTO/).

It turns out pretty straightforward. Instead of `printf`ing, you just `mvprintw`
(where `mv` stands for move and `w` stands for window) by supplying a pair of
coordinates; the same goes for other functions. To draw a solid block at `(y,
x)`, all I had to do is print two inverted spaces:

```
mvaddch(y, x, (' ' | A_REVERSE));
addch(' ' | A_REVERSE);
```

The second line did not need a `mv` because the `mvaddch` above
automatically moves the `x` coordinate to the right by one. To draw
a rectangle, I just put `ACS_HLINE`, `ACS_VLINE`, `ACS_ULCORNER` etc. at
all the right places. ncurses provided macros and functions for colors
too.

Soon, I was able to draw the map, the piece hovering above to be placed,
and the rest of the pieces.

I also managed to react to the player's keystrokes with `getch()`.
I defined three sets of navigation keys: arrow keys, wasd, and of course
the vim user loyalty, hjkl. The player may also press `[` and `]` to
switch pieces. There were a lot of switch-cases and bound checking.

Finally I used `getopt.h` for the first time to parse CLI arguments, aka
`argv`. The concept of storing argument values in global variables
honestly surprised me, but having done some AVR, I got over it very soon.
You can customize the map size (both height and width independently), and
the maximum number of pieces you have at once. My editor plugin warns me
about insecurities in `sscanf`, but I disregarded them.

I tried valgrind on the ncurses version, but the results were monstrous.
It was later revealed to me that ncurses does a lot of memory management
itself and inevitably it will confuse valgrind. ncurses does provide
a workaround to debug memory usage. Anyway, at this point there is no
longer any point. I proceeded to add a few fancy things like game stats
and called this project complete.

## Demo

<video controls><source src="../img/sirtet/demo.mp4"></video>

MPEG-4 Video (252 KiB)

## Room for improvement

This whole project was made with a premise in mind that no one else would
play it. If anyone else were to play it, I would have made a few
adjustments:

- `--no-color` option, i.e. use monochrome indicators other than red/green
- Highscore
- `--help` option