├── .gitignore
├── README.md
├── constants.py
├── docs
├── decrypt.png
├── hardware.png
├── manual.css
└── manual.html
├── gameplay.py
├── gamestate.py
├── ggo16.py
├── ggo16.spec
├── media
├── bezel.png
├── bezel_off.png
├── chip.png
├── cpu.png
├── fonts
│ ├── Arrows.ttf
│ ├── Gobotronic.otf
│ ├── LCDMU___.TTF
│ ├── METRO-DF.TTF
│ ├── PigpenCipher.otf
│ ├── Sansation_Regular.ttf
│ ├── circlethings.ttf
│ └── whitrabt.ttf
├── icon.ico
├── icon.png
├── levels.json
├── login
│ ├── archery.png
│ ├── baseball.png
│ ├── basketball.png
│ ├── beer.png
│ ├── boats.png
│ ├── books.png
│ ├── cars.png
│ ├── cats.png
│ ├── computers.png
│ ├── dogs.png
│ ├── flowers.png
│ ├── food.png
│ ├── horses.png
│ ├── music.png
│ ├── planes.png
│ ├── skateboarding.png
│ ├── soccer.png
│ ├── tennis.png
│ └── wine.png
├── motherboard3.png
├── motherboard4.png
└── resistor.png
├── menu
├── __init__.py
├── level.py
├── mainmenu.py
├── menu.py
├── pause.py
└── splash.py
├── mouse.py
├── programs
├── __init__.py
├── decrypt.py
├── hardware.py
├── hexedit.py
├── imagepassword.py
├── minehunt.py
├── network.py
├── password.py
└── program.py
├── resources.py
├── terminal.py
├── timer.py
├── tools
├── decrypt_image.py
└── imagepassword_test.py
└── util.py
/.gitignore:
--------------------------------------------------------------------------------
1 | progress.json
2 | build
3 | dist
4 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # The Terminal
2 |
3 | An entry into the Github 2016 Game-Off game jam, The Terminal is a cooperative hacking
4 | game in the style of [Keep Talking and Nobody Explodes](http://keeptalkinggame.com).
5 |
6 | The aim of the game is to hack into a computer terminal by following the
7 | instructions in the hacking manual, before the system locks you out.
8 |
9 | Only one player can see and interact with the computer, but is not allowed to
10 | view the hacking manual. The other players can view the manual, but are not
11 | allowed to see the computer. The two groups must communicate with each other
12 | in order to gain access before timer runs out.
13 |
14 | ## Setup
15 | ### From the source (all platforms)
16 | * Install python3 and the pygame module.
17 | * Clone the repository: git clone https://github.com/juzley/theterminal
18 | * Change into the directory containing the repository.
19 | * Launch the game: python3 ggo16.py
20 | * The manual can be found in the docs dir of the repository (docs/manual.html), or at http://juzley.github.io/TheTerminal/manual.html
21 |
22 | ### Pre-built binary (windows only)
23 | * Download a pre-built archive from the [github releases page](https://github.com/Juzley/theterminal/releases).
24 | * Extract the files from the archive and launch theterminal.exe.
25 | * The manual can be found in the docs dir of the archive (docs/manual.html), or at http://juzley.github.io/TheTerminal/manual.html
26 |
27 |
28 |
--------------------------------------------------------------------------------
/constants.py:
--------------------------------------------------------------------------------
1 | """Various constants for use throughout the game."""
2 |
3 | TEXT_COLOUR = (20, 200, 20)
4 | TEXT_COLOUR_RED = (200, 20, 20)
5 | TEXT_COLOUR_WHITE = (255, 255, 255)
6 |
7 | TERMINAL_FONT = 'media/fonts/whitrabt.ttf'
8 | TERMINAL_TEXT_SIZE = 16
9 |
10 | GAMENAME = "The Terminal"
11 | VERSION = 0.1
12 | VERSION_STRING = 'v{}'.format(VERSION)
13 | MANUAL_URL = 'https://juzley.github.io/game-off-2016/manual.html'
14 |
--------------------------------------------------------------------------------
/docs/decrypt.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Juzley/TheTerminal/363e3ecd9e50946e96ba172800dc2bf9ab73d4bb/docs/decrypt.png
--------------------------------------------------------------------------------
/docs/hardware.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Juzley/TheTerminal/363e3ecd9e50946e96ba172800dc2bf9ab73d4bb/docs/hardware.png
--------------------------------------------------------------------------------
/docs/manual.css:
--------------------------------------------------------------------------------
1 |
2 | * {
3 | font-family: courier;
4 | }
5 |
6 | body {
7 | background: #EEE;
8 | }
9 |
10 | img {
11 | display: block;
12 | margin: auto;
13 | }
14 |
15 | .command {
16 | font-style: italic;
17 | }
18 |
19 | table.alternate tr:nth-child(odd), ol.alternate li:nth-child(odd) {
20 | background: #EEE;
21 | }
22 |
23 | table.alternate th {
24 | background: #CCC;
25 | font-weight: bold;
26 | }
27 |
28 | table.alternate th, table.alternate td {
29 | text-align: left;
30 | border: 1px solid;
31 | padding-left: 5px;
32 | padding-right: 5px;
33 | }
34 |
35 | a:link {
36 | text-decoration: none;
37 | color: #444;
38 | }
39 |
40 | a:hover {
41 | font-weight: bold;
42 | color: #444;
43 | }
44 |
45 | a:visited {
46 | color: #444;
47 | }
48 |
49 | #manual {
50 | max-width: 20cm;
51 | margin: auto;
52 | }
53 |
54 | div.section {
55 | background: white;
56 | margin-top: 20px;
57 | margin-bottom: 20px;
58 | padding-left: 5px;
59 | padding-right: 5px;
60 | padding-bottom: 10px;
61 | }
62 |
63 | .section em {
64 | font-weight: bold;
65 | }
66 |
67 | .puzzle-section a {
68 | display: block;
69 | text-align: center;
70 | margin-top: 10px;
71 | }
72 |
73 | .puzzle-section h3 {
74 | text-align: center;
75 | }
76 |
77 | .puzzle-section p.intro {
78 | font-style: italic;
79 | }
80 |
81 | .puzzle-section table {
82 | border-collapse: collapse;
83 | max-width: 90%;
84 | margin: auto;
85 | }
86 |
87 | div.network-diagrams {
88 | text-align: center;
89 | }
90 |
91 | div.network-diagram {
92 | display: inline-block;
93 | margin-top: 5px;
94 | margin-bottom: 0px;
95 | min-width: 30%;
96 | background-color: #CCC;
97 | }
98 |
99 | .network-diagram span.ip {
100 | display: block;
101 | font-style: italic;
102 | }
103 |
104 | .network-diagram pre {
105 | background-color: #EEE;
106 | text-align: center;
107 | margin-top: 1px;
108 | }
109 |
110 | .network-diagram span.node {
111 | color: #AAA;
112 | }
113 |
114 | .network-diagram span.firewall {
115 | color: #C11;
116 | }
117 |
118 | .network-diagram span.src {
119 | color: #1C1;
120 | }
121 |
122 | .network-diagram span.dst {
123 | color: #11C;
124 | }
125 |
126 | .network-diagram span.gateway {
127 | color: #990;
128 | }
129 |
130 | div.minehunt-diagrams {
131 | text-align: center;
132 | }
133 |
134 | div.minehunt-diagram {
135 | display: inline-block;
136 | margin-top: 10px;
137 | min-width: 40%;
138 | }
139 |
140 | .minehunt-diagram td, td.minehunt-diagram {
141 | background-color: #DDD;
142 | border: 1px solid;
143 | margin: 1px;
144 | width: 20px;
145 | height: 20px;
146 | padding: 0;
147 | text-align: center;
148 | }
149 |
150 | .minehunt-diagram div.bomb {
151 | color: black;
152 | font-weight: bold;
153 | }
154 |
155 | .minehunt-diagram div.finish {
156 | color: red;
157 | font-weight: bold;
158 | }
159 |
160 | .minehunt-diagram span.count {
161 | display: block;
162 | font-style: italic;
163 | }
164 |
165 | .minehunt-diagram span.time {
166 | display: block;
167 | font-style: italic;
168 | }
169 |
170 | .minehunt-diagram span.time em {
171 | font-weight: bold;
172 | }
173 |
174 | #minehunt-key td {
175 | border: 1px solid;
176 | padding: 2px;
177 | }
178 |
179 | div.resistor {
180 | margin: auto;
181 | padding-top: 1px;
182 | padding-bottom: 1px;
183 | }
184 |
185 | .resistor div {
186 | width: 6px;
187 | height: 1em;
188 | margin-left: 1px;
189 | margin-right: 1px;
190 | display: inline-block;
191 | font-weight: bold;
192 | font-size: 2em;
193 | }
194 |
195 | .resistor div.red {
196 | color: red;
197 | }
198 |
199 | .resistor div.yellow {
200 | color: yellow;
201 | }
202 |
203 | .resistor div.green {
204 | color: green;
205 | }
206 |
207 | .resistor div.blue {
208 | color: blue;
209 | }
210 |
211 | .resistor div.black {
212 | color: black;
213 | }
214 |
215 | @media print {
216 | .puzzle-section {
217 | page-break-before: always;
218 | }
219 |
220 | table {
221 | page-break-inside: avoid;
222 | }
223 |
224 | img {
225 | page-break-inside: avoid;
226 | }
227 | }
228 |
229 |
230 |
--------------------------------------------------------------------------------
/docs/manual.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
10 |
11 |
The Terminal - Hacking Manual
12 |
13 |
Overview
14 |
15 |
The terminal is a local multiplayer hacking game inspired by
16 | "Keep Talking and Nobody Explodes".
17 | The aim of the game is to hack into a computer terminal by following the
18 | instructions in the hacking manual, before the system locks you out.
19 |
20 |
Only one person can see and interact with the computer, but is not allowed
21 | to view the hacking manual. The other players can view the manual, but are not
22 | allowed to see the computer. The two groups must communicate with each other
23 | in order to gain access before timer runs out.
24 |
25 |
Game version
26 |
27 |
It is important that the versions of your game and this manual match. The
28 | game version is shown in the bottom right of the screen in the main menu. This
29 | manual is for version 0.1 (V0.1) of the game.
30 |
31 |
Getting started
32 |
33 |
When you first start the a game, you will be presented with a terminal,
34 | at which you can type commands. Different commands launch different programs
35 | which you will need to use to gain access to the system - these programs may
36 | be text-based, requiring keyboard input, or graphical, in which case you will
37 | use the mouse.
38 |
39 |
Ultimately, the login command is used to log in
40 | to the terminal, and will prompt you for a password or similar form of
41 | authentication. On some systems, you will find you cannot execute the login
42 | command until other forms of security have been disabled - entering
43 | login will give you an idea of what further steps
44 | are required.
45 |
46 |
The help command lists the available commands on
47 | each particular terminal - if you get stuck it is a good idea to try working
48 | your way through this list. See the next section in this manual for
49 | instructions for the different programs you will encounter.
50 |
51 |
If you wish to exit a program prematurely (i.e. without completing the
52 | objective of the program), you may do so by pressing ctrl and c on your
53 | keyboard - you can always run it again later.
54 |
55 |
Hacking
56 |
The sections that follow describe the various tasks you will have to
57 | master in order to be a successful hacker. The list below gives links to each
58 | task.
59 |
68 |
69 |
70 |
71 |
Guessing Passwords
72 |
We have recovered a shredded list of passwords from a nearby
73 | dumpster. Despite our best efforts to piece it back together, it is still hard
74 | to make out the passwords. Below are our best guesses for the passwords of each
75 | user - perhaps with a bit of extra work you can work which are correct?
76 |
77 |
You will have a number of attempts to log in before the password is reset.
78 | Using our cracked login program, you will be able to see how many characters
79 | were correct - the right letter in the right position - for each attempt. You
80 | should be able to work out the password using this information.
81 |
82 |
Inputting the incorrect password too many times will lead to a password
83 | reset, and you'll have to start again.
84 |
85 |
86 | User | Possible Passwords |
87 |
88 | root |
89 | flask, great, force, gleam, brick, flute, blast, feast, flick,
90 | flank |
91 |
92 |
93 | ro0t |
94 | tusks, blush, askew, train, asset, burns, tries, turns, basks,
95 | busks |
96 |
97 |
98 | rewt |
99 | maple, pearl, lapel, myths, cycle, apple, ladle, ample, maize,
100 | capel |
101 |
102 |
103 | 00142331 |
104 | trice, racer, tours, glaze, trail, raise, slick, track, grace,
105 | trace |
106 |
107 |
108 | 00143231 |
109 | court, truce, fords, flirt, cruel, craft, tours, chart, fours,
110 | count |
111 |
112 |
113 | 01043231 |
114 | eagle, ariel, glare, gains, earns, gauge, angle, early, agile,
115 | engle |
116 |
117 |
118 |
Back to top
119 |
120 |
121 |
122 |
Guessing Visual Passwords
123 |
Some organisations use a visual password system. Users select
124 | images to identify themselves, and this selection is then used for
125 | authentication. Fortunately for us, people usually pick images based on the
126 | things that they like...
127 |
128 |
The visual login process will present you with four images - three from the
129 | user's selected images, and one which was not selected by the user. Use the
130 | table below to identify the user based on the images you are presented with,
131 | and select the three correct images
132 |
133 |
Selecting an incorrect image will result in a temporary lockout.
134 |
135 |
136 | User | Likes |
137 |
138 | User A |
139 | Cars, Cats, Dogs, Planes, Music, Flowers |
140 |
141 |
142 | User B |
143 | Skateboarding, Horses, Music, Dogs, Food, Wine |
144 |
145 |
146 | User C |
147 | Books, Computers, Cats, Archery, Boats, Cars |
148 |
149 |
150 | User D |
151 | Soccer, Basketball, Baseball, Archery, Skateboarding, Tennis |
152 |
153 |
154 | User E |
155 | Books, Wine, Planes, Computers, Music, Horses |
156 |
157 |
158 | User F |
159 | Soccer, Tennis, Flowers, Cats, Boats, Archery |
160 |
161 |
162 | User G |
163 | Food, Wine, Beer, Planes, Basketball, Baseball |
164 |
165 |
166 | User H |
167 | Cars, Beer, Soccer, Tennis, Food, Computers |
168 |
169 |
170 |
Back to top
171 |
172 |
173 |
174 |
Cracking Programs
175 |
You'll sometimes need to modify the code of programs on the
176 | computer you're attempting to gain access to in order to bypass security
177 | checks. Brush off your programming skills and open your hex editor...
178 |
179 |
A program is made up of a number of lines of code, where each line consists
180 | of 6 numbers. A hex editor will allow you to modify the lines of code
181 | one-by-one, replacing one value in the line with another value. Note
182 | that in some cases, you will need to remember the value that you are replacing
183 | for following lines.
184 |
185 |
Follow the rules below in order to determine the correct actions for each
186 | line. If you don't follow the instructions below carefully, the system will
187 | detect your attempts to modify the program, and revert it to its original
188 | state.
189 |
190 |
191 | - If this is the first line in the program go to (d), else go to (b)
192 | - If this is the last line go to (j), else go to (c)
193 | - If you made a change to the previous line, go to (g), else go to
194 | (d)
195 | - If the line has one or more 9s, replace the last number in the line by
196 | 7, else go to (e)
197 | - If the line contains one or more zeros, replace the first occurence
198 | with 9, else go to (f)
199 | - If the line contains more than 3 odd numbers, reduce the first odd
200 | number by 1, else go to (k)
201 | - If the line has more than one zeros, replace the first number in the
202 | line with the number that was replaced in the previous line, else go
203 | to (h)
204 | - If the line contains more than one 9s, replace the first occurence with
205 | the number of the column in the last line which was edited, else go to
206 | (i)
207 | - If the line contains one or more values which match the value inserted
208 | in the previous line, replace the first occurence with zero, else go
209 | to (k)
210 | - Replace the final value with the number of lines (excluding this line)
211 | which have been modified.
212 | - Do not edit the line.
213 |
214 |
Back to top
215 |
216 |
217 |
218 |
Decrypting Filesystems
219 |
Some systems you encounter will have encrypted filesystems,
220 | which will need to be unencrypted before you can progress further with a hack.
221 | A disgruntled employee at one of the big security firms leaked some of the
222 | encryption keys, which should allow you to progress
223 |
224 |
Our decryption program will present you with a section of encrypted data.
225 | Use the table below to enter the correct plain-text data - decryption can then
226 | take place based on this information.
227 |
228 |
Entering the incorrect key will cause data corruption, and recovering from
229 | this will waste valuable time.
230 |
231 |

232 |
Back to top
233 |
234 |
235 |
236 |
Disabling Hardware Security
237 |
Some of our target devices have hardware security support in
238 | the form of on-board expansion chips. These will prevent us from gaining access
239 | unless we can somehow disable them. Luckily we were able to 'obtain' a copy of
240 | the hardware specifications, which should help you bypass this protection.
241 |
242 |
Each system may have one or more security chip on its motherboard, with an
243 | associated pull-down resistor. In order to bypass the security system, you
244 | must remove the chip, its pull-down resistor, or both, based on the information
245 | in the below tables. Removing a chip or resistor is achieved by simply
246 | left-clicking on it.
247 |
248 |

249 |
250 |
Once you have the correct hardware configuration, click the power button
251 | to the bottom left of the screen to restart the computer. Booting the system
252 | with an incorrect hardware configuration may affect the system clock, which may
253 | reduce the amount of time you have before you are logged out.
254 |
255 |
Table 1:
256 | This table shows the values of the different resistors, based on the coloured
257 | markings on the resistor.
258 |
259 |
260 | Markings | Value |
261 |
262 |
263 |
264 | -
265 | -
266 | -
267 | -
268 |
269 | |
270 | 10 kOhms |
271 |
272 |
273 |
274 |
275 | -
276 | -
277 | -
278 | -
279 |
280 | |
281 | 20 kOhms |
282 |
283 |
284 |
285 |
286 | -
287 | -
288 | -
289 | -
290 |
291 | |
292 | 30 kOhms |
293 |
294 |
295 |
296 |
297 | -
298 | -
299 | -
300 | -
301 |
302 | |
303 | 40 kOhms |
304 |
305 |
306 |
307 |
308 | -
309 | -
310 | -
311 | -
312 |
313 | |
314 | 50 kOhms |
315 |
316 |
317 |
318 |
319 | -
320 | -
321 | -
322 | -
323 |
324 | |
325 | 60 kOhms |
326 |
327 |
328 |
329 |
Table 2:
330 | Use the last character of the security chip's identifier and the value of the
331 | chip's pull-down resistor to determine the correct action.
332 |
333 |
334 |
335 | Resistor Value |
336 | Odd number |
337 | Event number |
338 | Zero |
339 | Letter |
340 |
341 | 10 kOhms | N | C | R | B |
342 | 20 kOhms | C | C | B | R |
343 | 30 kOhms | R | R | C | C |
344 | 40 kOhms | B | N | R | C |
345 | 50 kOhms | T3, row 1 | T3, row 4 | N | T3,
346 | row 3 |
347 | 60 kOhms | T3, row 5 | C | T3, row 2 | T3,
348 | row 6 |
349 |
350 |
351 |
Table 3:
352 | Use the row given by table 2, and the first character of the terminal name to
353 | determine the correct action.
354 |
355 |
356 |
357 | Row |
358 | Odd number |
359 | Even number |
360 | Zero |
361 | Letter |
362 |
363 | Row 1 | C | N | R | C |
364 | Row 2 | R | C | R | C |
365 | Row 3 | R | B | C | R |
366 | Row 4 | C | R | C | R |
367 | Row 5 | N | C | R | B |
368 | Row 6 | C | C | C | R |
369 |
370 |
371 |
Key:
372 |
373 | - N: remove nothing.
374 | - C: remove the chip.
375 | - R: remove the resistor.
376 | - B: remove both.
377 | - T3, row#: Consult table 3, looking at the specified row.
378 |
379 |
Back to top
380 |
381 |
382 |
383 |
Network Management
384 |
Some systems obtain login information from a remote
385 | authentication server. The easiest way around this is to reconfigure the
386 | network so that they are connected to one of our servers instead!
387 |
388 |
The network manager allows you to make network connections between devices
389 | in a network. Each device is represented as a symbol in a grid. Your goal is
390 | to use the arrow keys to draw a path from the computer you are attempting to
391 | hack (the source, S) to our rogue authentication server (the destination, D).
392 | You cannot visit a node in the network grid more than once.
393 |
394 |
395 |
Some networks contain gateway switches (G). When connecting the source to
396 | the destination, you must make sure to go through all such switches on the way.
397 | Most networks also contain firewalls (x) - our server cannot communicate
398 | through these devices and so they must be avoided.
399 |
400 |
401 |
Attempts to configure the network such that it doesn't meet the above
402 | requirements will fail, and the network configuration will roll back to its
403 | previous state.
404 |
405 |
Below are some possible network configurations you will encounter. The
406 | source and destination devices are identified by IP addresses, which are
407 | included with the diagram.
408 |
409 |
410 |
411 |
Src IP: 192.168.1.15
412 |
Dst IP: 10.0.0.1
413 |
414 | S . . . x x
415 | x . . . x x
416 | x x G x x x
417 | . . . . G .
418 | G . . . . .
419 | x x x x x D
420 |
421 |
422 |
423 |
424 |
Src IP: 192.168.1.15
425 |
Dst IP: 11.0.0.1
426 |
427 | S . . G x x
428 | . . . . x x
429 | . x . . x x
430 | . . x . G .
431 | G . . x . .
432 | x x . . . D
433 |
434 |
435 |
436 |
437 |
Src IP: 192.168.1.50
438 |
Dst IP: 11.0.0.1
439 |
440 | S . . G x x
441 | . . . . . x
442 | . x . . G x
443 | . . x . x .
444 | G . . . . .
445 | x x . . . D
446 |
447 |
448 |
449 |
450 |
Src IP: 192.168.1.100
451 |
Dst IP: 12.0.0.1
452 |
453 | . x D x x .
454 | . x . . x .
455 | x x x . x x
456 | . . . . . .
457 | . x x x x .
458 | . . . S . G
459 |
460 |
461 |
462 |
463 |
Src IP: 192.168.1.100
464 |
Dst IP: 12.0.0.2
465 |
466 | . x x D x .
467 | . x . . x .
468 | x x . x x x
469 | . . . . . .
470 | . x x x x .
471 | G . S . . .
472 |
473 |
474 |
475 |
476 |
Src IP: 10.10.10.11
477 |
Dst IP: 10.10.10.7
478 |
479 | D . . . x x
480 | . x x . x x
481 | . x x G x x
482 | . . . . . x
483 | x . x x . x
484 | x G . . . S
485 |
486 |
487 |
488 |
Back to top
489 |
490 |
491 |
492 |
Minehunt
493 |
The minehunt game is a poorly written Minesweeper clone,
494 | installed on many of our target systems. The aim is to clear all the squares
495 | not containing mines, without uncovering a mine. Flags can be placed to mark
496 | known mine locations. Winning the game is not your aim here, however: analysis
497 | Minehunt's assembly code has revealed that it is possible to gain elavated
498 | permissions if certain actions are performed in the right order
499 |
500 |
To trigger the exploit in the Minehunt game, follow the below steps:
501 |
502 | - Use the mine count and size of board to determine which layout you
503 | have been presented with.
504 | - Place flags (right click) on all but one of the mines, as indicated
505 | below.
506 | - Detonate (left click) the final mine. With some board layouts, the time
507 | at which the final mine is detonated (whether the number of seconds in
508 | the counter is odd or even) is critical in triggering the exploit.
509 |
510 |
511 |
512 | Below are all possible board layouts you may encounter.
513 | Use the following key to determine which mines should be flagged, and which
514 | should be detonated:
515 |
516 |
517 |
518 | o |
519 | The location of a mine that should be flagged |
520 | x |
521 | The location of the mine that should be detonated |
522 |
523 |
524 |
525 |
526 |
8x10, 8 mines
527 |
End when time is odd
528 |
529 | . . . . . o . . . . |
530 | o . . . . . . . . . |
531 | . . . . . . . . . o |
532 | . . . o . . . . . . |
533 | . . . . . . . . . . |
534 | o x . . . . . o . . |
535 | . . . . . . . . . . |
536 | . . . . . . . . o . |
537 |
538 |
539 |
540 |
541 |
8x10, 9 mines
542 |
End when time is even
543 |
544 | . . . . . o . . . . |
545 | o o . . . . . . . . |
546 | . . . . . . . . . . |
547 | . . . o . . . . . . |
548 | . . . . . . . . o . |
549 | o o . . . . . . x . |
550 | . . . . . . . . . . |
551 | . . . . . . . . . o |
552 |
553 |
554 |
555 |
556 |
8x10, 10 mines
557 |
No time restrictions
558 |
559 | . o . . . o . . . . |
560 | o o . . . . . . . . |
561 | . . . . . . . . . . |
562 | . . . o . . . . . . |
563 | . . . . . . . . . . |
564 | o x . . . . . . o . |
565 | . . . . . . . . . . |
566 | . o . . o . . . . . |
567 |
568 |
569 |
570 |
571 |
8x9, 7 mines
572 |
End when time is even
573 |
574 | o . . . . o . . . |
575 | . . . . . . . . . |
576 | . . . . . . . . . |
577 | . . . x . . . . . |
578 | . . . . . . . . . |
579 | o o . . . . . o . |
580 | . . . . . . . . . |
581 | . . . . . . . . o |
582 |
583 |
584 |
585 |
586 |
8x9, 11 mines
587 |
End when time is odd
588 |
589 | o . . . . o . . . |
590 | o . . . . . . . . |
591 | . . . . . . . x o |
592 | . . . o . . . . . |
593 | . . . . . . . . . |
594 | o o . . . . . o . |
595 | . . . . . . . . . |
596 | . o . . . . . . o |
597 |
598 |
599 |
600 |
601 |
8x9, 9 mines
602 |
No time restrictions
603 |
604 | o . . . . o . . . |
605 | . . . . . . . . . |
606 | . . . . . . . . . |
607 | . . . o . . . . . |
608 | . . . . . . . . . |
609 | o x . . . . . o . |
610 | o o . . . . . . . |
611 | . . . . . . . . o |
612 |
613 |
614 |
615 |
Back to top
616 |
617 |
618 |
619 |
651 |
652 |
653 |
--------------------------------------------------------------------------------
/gameplay.py:
--------------------------------------------------------------------------------
1 | """Implementation of the core gameplay."""
2 |
3 | import random
4 |
5 | import pygame
6 |
7 | import timer
8 | import util
9 | import menu
10 | import constants
11 | from gamestate import GameState
12 | from terminal import Terminal
13 | from resources import load_font
14 |
15 |
16 | class SuccessState(GameState):
17 |
18 | """Gamestate implementation for the success screen."""
19 |
20 | _FONT = constants.TERMINAL_FONT
21 | _WAIT_TIME = 1000
22 | _MAIN_TEXT_HEIGHT = 50
23 | _CONTINUE_TEXT_HEIGHT = 20
24 | _SPACING = 10
25 |
26 | def __init__(self, mgr, terminal):
27 | """Initialize the class."""
28 | self._mgr = mgr
29 | self._timer = timer.Timer()
30 | self._terminal = terminal
31 |
32 | font = load_font(SuccessState._FONT,
33 | SuccessState._MAIN_TEXT_HEIGHT)
34 | self._login_text = font.render('Access Granted', True,
35 | constants.TEXT_COLOUR)
36 |
37 | font = load_font(SuccessState._FONT,
38 | SuccessState._CONTINUE_TEXT_HEIGHT)
39 | self._continue_text = font.render('Press any key to continue', True,
40 | constants.TEXT_COLOUR)
41 |
42 | self._login_text_coords = util.center_align(
43 | self._login_text.get_rect().w,
44 | self._login_text.get_rect().h + self._continue_text.get_rect().h)
45 | coords = util.center_align(self._continue_text.get_rect().w, 0)
46 | self._continue_text_coords = (coords[0],
47 | self._login_text_coords[1] +
48 | self._login_text.get_rect().h +
49 | SuccessState._SPACING)
50 |
51 | def draw(self):
52 | """Draw the losing screen."""
53 | self._terminal.draw_bezel()
54 | pygame.display.get_surface().blit(self._login_text,
55 | self._login_text_coords)
56 |
57 | if self._timer.time >= SuccessState._WAIT_TIME:
58 | pygame.display.get_surface().blit(self._continue_text,
59 | self._continue_text_coords)
60 |
61 | def run(self, events):
62 | """Run the win-game screen."""
63 | self._timer.update()
64 | if self._timer.time >= SuccessState._WAIT_TIME:
65 | if len([e for e in events if e.type == pygame.KEYDOWN]) > 0:
66 | # Return to the main menu.
67 | self._mgr.pop_until(menu.MainMenu)
68 |
69 |
70 | class LostState(GameState):
71 |
72 | """Gamestate implementation for the defeat screen."""
73 |
74 | _WAIT_TIME = 2000
75 | _FONT = constants.TERMINAL_FONT
76 | _MAIN_TEXT_HEIGHT = 50
77 | _CONTINUE_TEXT_HEIGHT = 20
78 | _SPACING = 10
79 |
80 | def __init__(self, mgr, terminal):
81 | """Initialize the class."""
82 | self._mgr = mgr
83 | self._timer = timer.Timer()
84 | self._terminal = terminal
85 |
86 | font = load_font(LostState._FONT,
87 | LostState._MAIN_TEXT_HEIGHT)
88 | self._login_text = font.render('You have been locked out', True,
89 | constants.TEXT_COLOUR_RED)
90 |
91 | font = load_font(LostState._FONT,
92 | LostState._CONTINUE_TEXT_HEIGHT)
93 | self._continue_text = font.render('Press any key to continue', True,
94 | constants.TEXT_COLOUR_RED)
95 |
96 | self._login_text_coords = util.center_align(
97 | self._login_text.get_rect().w,
98 | self._login_text.get_rect().h + self._continue_text.get_rect().h)
99 | coords = util.center_align(self._continue_text.get_rect().w, 0)
100 | self._continue_text_coords = (coords[0],
101 | self._login_text_coords[1] +
102 | self._login_text.get_rect().h +
103 | LostState._SPACING)
104 |
105 | def draw(self):
106 | """Draw the losing screen."""
107 | self._terminal.draw_bezel()
108 | pygame.display.get_surface().blit(self._login_text,
109 | self._login_text_coords)
110 |
111 | if self._timer.time >= LostState._WAIT_TIME:
112 | pygame.display.get_surface().blit(self._continue_text,
113 | self._continue_text_coords)
114 |
115 | def run(self, events):
116 | """Run the lost-game screen."""
117 | self._timer.update()
118 | if self._timer.time >= LostState._WAIT_TIME:
119 | if len([e for e in events if e.type == pygame.KEYDOWN]) > 0:
120 | # Return to the main menu.
121 | self._mgr.pop_until(menu.MainMenu)
122 |
123 |
124 | class GameplayState(GameState):
125 |
126 | """Gamestate implementation for the core gameplay."""
127 |
128 | def __init__(self, mgr, level_info):
129 | """Initialize the class."""
130 | self._level_info = level_info
131 |
132 | # The level file specifies programs with groups, where each group
133 | # contains a list of possible programs, and the number of programs to
134 | # use from that group. Here we pick the programs that we're going to
135 | # use from each group - we end up with a data structure looking
136 | # something like the following:
137 | # groups = {
138 | # 'group_name_1': {
139 | # 'program_name_1': 'program_class_1',
140 | # 'program_name_2': 'program_class_2',
141 | # },
142 | # 'group_name_2': {
143 | # 'program_name_3': 'program_class_3'
144 | # }
145 | # }
146 | groups = {}
147 | for group_name, group_info in level_info['program_groups'].items():
148 | # Pick the programs we're going to use for this game.
149 | groups[group_name] = {name: cls for (name, cls) in
150 | random.sample(group_info['programs'],
151 | group_info['program_count'])}
152 |
153 | # Now that we've picked which programs to use, we can set up the
154 | # dependencies between programs.
155 | depends = {}
156 | for group_name, group_info in level_info['program_groups'].items():
157 | group_programs = groups[group_name]
158 |
159 | if 'dependent_on' in group_info:
160 | program_list = []
161 | for d in group_info['dependent_on']:
162 | dependent_group_programs = groups[d]
163 | program_list.extend(list(dependent_group_programs.keys()))
164 |
165 | for program in group_programs.keys():
166 | depends[program] = program_list
167 |
168 | # Finally get the flatten the groups into a list of programs
169 | programs = {}
170 | for g in groups.values():
171 | programs.update(g)
172 |
173 | self._terminal = Terminal(
174 | programs=programs,
175 | time=level_info['time'],
176 | depends=depends)
177 | self._mgr = mgr
178 |
179 | def run(self, events):
180 | """Run the game."""
181 | for e in events:
182 | if e.type == pygame.KEYDOWN:
183 | if e.key == pygame.K_ESCAPE:
184 | self._terminal.paused = True
185 | self._mgr.push(menu.PauseMenu(self._mgr,
186 | self._terminal))
187 | else:
188 | self._terminal.on_keypress(e.key, e.unicode)
189 | elif e.type == pygame.KEYUP:
190 | self._terminal.on_keyrelease()
191 | elif e.type == pygame.MOUSEBUTTONDOWN:
192 | self._terminal.on_mouseclick(e.button, e.pos)
193 | elif e.type == pygame.MOUSEMOTION:
194 | self._terminal.on_mousemove(e.pos)
195 | elif e.type == pygame.ACTIVEEVENT:
196 | self._terminal.on_active_event(util.ActiveEvent(e.state,
197 | e.gain))
198 |
199 | if not self._terminal.paused:
200 | self._terminal.run()
201 |
202 | # The player is locked out, switch to the Lost gamestate.
203 | if self._terminal.locked:
204 | # Push so that we can restart the game if required by just popping
205 | # again.
206 | self._mgr.push(LostState(self._mgr, self._terminal))
207 |
208 | # The player has succeeded, switch to the success gamestate.
209 | if self._terminal.completed():
210 | # Don't need to return to the game, so replace this gamestate with
211 | # the success screen.
212 | menu.LevelMenu.completed_level(self._level_info['id'])
213 | self._mgr.replace(SuccessState(self._mgr, self._terminal))
214 |
215 | def draw(self):
216 | """Draw the game."""
217 | self._terminal.draw()
218 |
--------------------------------------------------------------------------------
/gamestate.py:
--------------------------------------------------------------------------------
1 | """Module responsible for switching between different gamestates."""
2 |
3 |
4 | class GameState:
5 |
6 | """Base class for gamestates."""
7 |
8 | def run(self, events):
9 | """Run the gamestate."""
10 | pass
11 |
12 | def draw(self):
13 | """Draw the gamestate."""
14 |
15 |
16 | class GameStateManager:
17 |
18 | """Class to manage running and switching between gamestates."""
19 |
20 | def __init__(self):
21 | """Initialize the class."""
22 | # _states is a stack of gamestates, implemented as a list where the
23 | # end of the list is the top of the stack. The current gamestate,
24 | # therefore, is at the end of the list.
25 | self._states = []
26 |
27 | def push(self, gamestate):
28 | """Push a new gamestate onto the stack."""
29 | self._states.append(gamestate)
30 |
31 | def replace(self, gamestate):
32 | """Replace the current gamestate with a new gamestate."""
33 | self.pop()
34 | self.push(gamestate)
35 |
36 | def pop(self):
37 | """Pop the current gamestate off the stack, and move to the next one."""
38 | if self._states:
39 | self._states.pop()
40 |
41 | def pop_until(self, cls):
42 | """Pop until the current state is an instance of a given class."""
43 | while self._states and not isinstance(self._states[-1], cls):
44 | self._states.pop()
45 |
46 | def run(self, events):
47 | """Run the current gamestate."""
48 | if self._states:
49 | self._states[-1].run(events)
50 |
51 | def draw(self):
52 | """Draw the current gamestate."""
53 | if self._states:
54 | self._states[-1].draw()
55 |
56 | def empty(self):
57 | """Indicate whether there are any active gamestates."""
58 | return len(self._states) == 0
59 |
--------------------------------------------------------------------------------
/ggo16.py:
--------------------------------------------------------------------------------
1 | """Entry point for the game."""
2 |
3 |
4 | import pygame
5 | import random
6 |
7 | import constants
8 | import mouse
9 | from gamestate import GameStateManager
10 | from menu import SplashScreen
11 | from resources import load_image
12 |
13 |
14 | def setup():
15 | """Perform initial setup."""
16 | pygame.init()
17 | pygame.display.set_mode([800, 600],
18 | pygame.DOUBLEBUF | pygame.HWSURFACE,
19 | 24)
20 | pygame.display.set_icon(load_image("media/icon.png"))
21 | pygame.display.set_caption(constants.GAMENAME)
22 | mouse.current.set_cursor(mouse.Cursor.ARROW)
23 | random.seed()
24 |
25 |
26 | def run():
27 | """Run the game loop."""
28 | gamestates = GameStateManager()
29 | gamestates.push(SplashScreen(gamestates))
30 |
31 | running = True
32 | while running:
33 | events = pygame.event.get()
34 | gamestates.run(events)
35 |
36 | if any(e.type == pygame.QUIT for e in events) or gamestates.empty():
37 | # An empty GameStateManager indicates that the main menu was popped,
38 | # and we should exit.
39 | running = False
40 | else:
41 | screen = pygame.display.get_surface()
42 | screen.fill((0, 0, 0))
43 | gamestates.draw()
44 | pygame.display.flip()
45 |
46 |
47 | if __name__ == '__main__':
48 | setup()
49 | run()
50 |
--------------------------------------------------------------------------------
/ggo16.spec:
--------------------------------------------------------------------------------
1 | # -*- mode: python -*-
2 |
3 | block_cipher = None
4 |
5 |
6 | a = Analysis(['ggo16.py'],
7 | pathex=['/home/jupriest/game-off-2016'],
8 | binaries=None,
9 | datas=[('media', 'media')],
10 | hiddenimports=[],
11 | hookspath=[],
12 | runtime_hooks=[],
13 | excludes=[],
14 | win_no_prefer_redirects=False,
15 | win_private_assemblies=False,
16 | cipher=block_cipher)
17 | pyz = PYZ(a.pure, a.zipped_data,
18 | cipher=block_cipher)
19 | exe = EXE(pyz,
20 | a.scripts,
21 | a.binaries,
22 | a.zipfiles,
23 | a.datas,
24 | name='theterminal',
25 | debug=False,
26 | strip=False,
27 | upx=True,
28 | console=False,
29 | icon='media\icon.ico')
30 |
--------------------------------------------------------------------------------
/media/bezel.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Juzley/TheTerminal/363e3ecd9e50946e96ba172800dc2bf9ab73d4bb/media/bezel.png
--------------------------------------------------------------------------------
/media/bezel_off.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Juzley/TheTerminal/363e3ecd9e50946e96ba172800dc2bf9ab73d4bb/media/bezel_off.png
--------------------------------------------------------------------------------
/media/chip.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Juzley/TheTerminal/363e3ecd9e50946e96ba172800dc2bf9ab73d4bb/media/chip.png
--------------------------------------------------------------------------------
/media/cpu.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Juzley/TheTerminal/363e3ecd9e50946e96ba172800dc2bf9ab73d4bb/media/cpu.png
--------------------------------------------------------------------------------
/media/fonts/Arrows.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Juzley/TheTerminal/363e3ecd9e50946e96ba172800dc2bf9ab73d4bb/media/fonts/Arrows.ttf
--------------------------------------------------------------------------------
/media/fonts/Gobotronic.otf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Juzley/TheTerminal/363e3ecd9e50946e96ba172800dc2bf9ab73d4bb/media/fonts/Gobotronic.otf
--------------------------------------------------------------------------------
/media/fonts/LCDMU___.TTF:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Juzley/TheTerminal/363e3ecd9e50946e96ba172800dc2bf9ab73d4bb/media/fonts/LCDMU___.TTF
--------------------------------------------------------------------------------
/media/fonts/METRO-DF.TTF:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Juzley/TheTerminal/363e3ecd9e50946e96ba172800dc2bf9ab73d4bb/media/fonts/METRO-DF.TTF
--------------------------------------------------------------------------------
/media/fonts/PigpenCipher.otf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Juzley/TheTerminal/363e3ecd9e50946e96ba172800dc2bf9ab73d4bb/media/fonts/PigpenCipher.otf
--------------------------------------------------------------------------------
/media/fonts/Sansation_Regular.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Juzley/TheTerminal/363e3ecd9e50946e96ba172800dc2bf9ab73d4bb/media/fonts/Sansation_Regular.ttf
--------------------------------------------------------------------------------
/media/fonts/circlethings.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Juzley/TheTerminal/363e3ecd9e50946e96ba172800dc2bf9ab73d4bb/media/fonts/circlethings.ttf
--------------------------------------------------------------------------------
/media/fonts/whitrabt.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Juzley/TheTerminal/363e3ecd9e50946e96ba172800dc2bf9ab73d4bb/media/fonts/whitrabt.ttf
--------------------------------------------------------------------------------
/media/icon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Juzley/TheTerminal/363e3ecd9e50946e96ba172800dc2bf9ab73d4bb/media/icon.ico
--------------------------------------------------------------------------------
/media/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Juzley/TheTerminal/363e3ecd9e50946e96ba172800dc2bf9ab73d4bb/media/icon.png
--------------------------------------------------------------------------------
/media/levels.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "id": 0,
4 | "name": "Level 1 - Learning To Hack",
5 | "cmd": "connect learning.ssh",
6 | "time": 180,
7 | "program_groups": {
8 | "login": {
9 | "program_count": 1,
10 | "dependent_on": ["others"],
11 | "programs": [
12 | ["login", "PasswordGuess"]
13 | ]
14 | },
15 | "others": {
16 | "program_count": 1,
17 | "programs": [
18 | ["decrypt", "Decrypt"]
19 | ]
20 | }
21 | }
22 | },
23 | {
24 | "id": 1,
25 | "name": "Level 2 - Cracking",
26 | "cmd": "connect cracking.ssh",
27 | "time": 180,
28 | "requires": [0],
29 | "program_groups": {
30 | "login": {
31 | "program_count": 1,
32 | "dependent_on": ["others"],
33 | "programs": [
34 | ["login", "PasswordGuess"],
35 | ["login", "ImagePassword"]
36 | ]
37 | },
38 | "others": {
39 | "program_count": 2,
40 | "programs": [
41 | ["hexedit", "HexEditor"],
42 | ["decrypt", "Decrypt"]
43 | ]
44 | }
45 | }
46 | },
47 | {
48 | "id": 2,
49 | "name": "Level 3 - Hardware Hacking",
50 | "cmd": "connect hardware.ssh",
51 | "time": 180,
52 | "requires": [1],
53 | "program_groups": {
54 | "login": {
55 | "program_count": 1,
56 | "dependent_on": ["hardware", "others"],
57 | "programs": [
58 | ["login", "PasswordGuess"],
59 | ["login", "ImagePassword"]
60 | ]
61 | },
62 | "hardware": {
63 | "program_count": 1,
64 | "programs": [
65 | ["suspend", "HardwareInspect"]
66 | ]
67 | },
68 | "others": {
69 | "program_count": 1,
70 | "programs": [
71 | ["hexedit", "HexEditor"],
72 | ["decrypt", "Decrypt"]
73 | ]
74 | }
75 | }
76 | },
77 | {
78 | "id": 3,
79 | "name": "Level 4 - Network Design",
80 | "cmd": "connect network.ssh",
81 | "time": 180,
82 | "requires": [1],
83 | "program_groups": {
84 | "login": {
85 | "program_count": 1,
86 | "dependent_on": ["network", "others"],
87 | "programs": [
88 | ["login", "PasswordGuess"],
89 | ["login", "ImagePassword"]
90 | ]
91 | },
92 | "network": {
93 | "program_count": 1,
94 | "programs": [
95 | ["network", "NetworkManager"]
96 | ]
97 | },
98 | "others": {
99 | "program_count": 1,
100 | "programs": [
101 | ["hexedit", "HexEditor"],
102 | ["decrypt", "Decrypt"]
103 | ]
104 | }
105 | }
106 | },
107 | {
108 | "id": 4,
109 | "name": "Level 5 - MineHunt!",
110 | "cmd": "connect minehunt.ssh",
111 | "time": 180,
112 | "requires": [1],
113 | "program_groups": {
114 | "login": {
115 | "program_count": 1,
116 | "dependent_on": ["minehunt", "others"],
117 | "programs": [
118 | ["login", "PasswordGuess"],
119 | ["login", "ImagePassword"]
120 | ]
121 | },
122 | "minehunt": {
123 | "program_count": 1,
124 | "programs": [
125 | ["minehunt", "MineHunt"]
126 | ]
127 | },
128 | "others": {
129 | "program_count": 1,
130 | "programs": [
131 | ["hexedit", "HexEditor"],
132 | ["decrypt", "Decrypt"]
133 | ]
134 | }
135 | }
136 | },
137 | {
138 | "id": 5,
139 | "name": "Level 6 - Ready For Anything",
140 | "cmd": "connect rdy.ssh",
141 | "time": 180,
142 | "requires": [2, 3, 4],
143 | "program_groups": {
144 | "login": {
145 | "program_count": 1,
146 | "dependent_on": ["others"],
147 | "programs": [
148 | ["login", "PasswordGuess"],
149 | ["login", "ImagePassword"]
150 | ]
151 | },
152 | "others": {
153 | "program_count": 2,
154 | "programs": [
155 | ["minehunt", "MineHunt"],
156 | ["network", "NetworkManager"],
157 | ["suspend", "HardwareInspect"],
158 | ["hexedit", "HexEditor"],
159 | ["decrypt", "Decrypt"]
160 | ]
161 | }
162 | }
163 | },
164 | {
165 | "id": 6,
166 | "name": "Level 7 - The Hacker's Marathon",
167 | "cmd": "connect mar4thon.ssh",
168 | "time": 300,
169 | "requires": [5],
170 | "program_groups": {
171 | "login": {
172 | "program_count": 1,
173 | "dependent_on": ["others"],
174 | "programs": [
175 | ["login", "PasswordGuess"],
176 | ["login", "ImagePassword"]
177 | ]
178 | },
179 | "others": {
180 | "program_count": 5,
181 | "programs": [
182 | ["minehunt", "MineHunt"],
183 | ["network", "NetworkManager"],
184 | ["suspend", "HardwareInspect"],
185 | ["hexedit", "HexEditor"],
186 | ["decrypt", "Decrypt"]
187 | ]
188 | }
189 | }
190 | },
191 | {
192 | "id": 7,
193 | "name": "Level 8 - Speedy Software",
194 | "cmd": "connect fastlockout.ssh",
195 | "time": 60,
196 | "requires": [6],
197 | "program_groups": {
198 | "login": {
199 | "program_count": 1,
200 | "dependent_on": ["others"],
201 | "programs": [
202 | ["login", "PasswordGuess"],
203 | ["login", "ImagePassword"]
204 | ]
205 | },
206 | "others": {
207 | "program_count": 1,
208 | "programs": [
209 | ["minehunt", "MineHunt"],
210 | ["network", "NetworkManager"],
211 | ["hexedit", "HexEditor"],
212 | ["decrypt", "Decrypt"]
213 | ]
214 | }
215 | }
216 | }
217 | ]
218 |
219 |
--------------------------------------------------------------------------------
/media/login/archery.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Juzley/TheTerminal/363e3ecd9e50946e96ba172800dc2bf9ab73d4bb/media/login/archery.png
--------------------------------------------------------------------------------
/media/login/baseball.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Juzley/TheTerminal/363e3ecd9e50946e96ba172800dc2bf9ab73d4bb/media/login/baseball.png
--------------------------------------------------------------------------------
/media/login/basketball.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Juzley/TheTerminal/363e3ecd9e50946e96ba172800dc2bf9ab73d4bb/media/login/basketball.png
--------------------------------------------------------------------------------
/media/login/beer.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Juzley/TheTerminal/363e3ecd9e50946e96ba172800dc2bf9ab73d4bb/media/login/beer.png
--------------------------------------------------------------------------------
/media/login/boats.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Juzley/TheTerminal/363e3ecd9e50946e96ba172800dc2bf9ab73d4bb/media/login/boats.png
--------------------------------------------------------------------------------
/media/login/books.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Juzley/TheTerminal/363e3ecd9e50946e96ba172800dc2bf9ab73d4bb/media/login/books.png
--------------------------------------------------------------------------------
/media/login/cars.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Juzley/TheTerminal/363e3ecd9e50946e96ba172800dc2bf9ab73d4bb/media/login/cars.png
--------------------------------------------------------------------------------
/media/login/cats.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Juzley/TheTerminal/363e3ecd9e50946e96ba172800dc2bf9ab73d4bb/media/login/cats.png
--------------------------------------------------------------------------------
/media/login/computers.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Juzley/TheTerminal/363e3ecd9e50946e96ba172800dc2bf9ab73d4bb/media/login/computers.png
--------------------------------------------------------------------------------
/media/login/dogs.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Juzley/TheTerminal/363e3ecd9e50946e96ba172800dc2bf9ab73d4bb/media/login/dogs.png
--------------------------------------------------------------------------------
/media/login/flowers.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Juzley/TheTerminal/363e3ecd9e50946e96ba172800dc2bf9ab73d4bb/media/login/flowers.png
--------------------------------------------------------------------------------
/media/login/food.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Juzley/TheTerminal/363e3ecd9e50946e96ba172800dc2bf9ab73d4bb/media/login/food.png
--------------------------------------------------------------------------------
/media/login/horses.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Juzley/TheTerminal/363e3ecd9e50946e96ba172800dc2bf9ab73d4bb/media/login/horses.png
--------------------------------------------------------------------------------
/media/login/music.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Juzley/TheTerminal/363e3ecd9e50946e96ba172800dc2bf9ab73d4bb/media/login/music.png
--------------------------------------------------------------------------------
/media/login/planes.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Juzley/TheTerminal/363e3ecd9e50946e96ba172800dc2bf9ab73d4bb/media/login/planes.png
--------------------------------------------------------------------------------
/media/login/skateboarding.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Juzley/TheTerminal/363e3ecd9e50946e96ba172800dc2bf9ab73d4bb/media/login/skateboarding.png
--------------------------------------------------------------------------------
/media/login/soccer.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Juzley/TheTerminal/363e3ecd9e50946e96ba172800dc2bf9ab73d4bb/media/login/soccer.png
--------------------------------------------------------------------------------
/media/login/tennis.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Juzley/TheTerminal/363e3ecd9e50946e96ba172800dc2bf9ab73d4bb/media/login/tennis.png
--------------------------------------------------------------------------------
/media/login/wine.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Juzley/TheTerminal/363e3ecd9e50946e96ba172800dc2bf9ab73d4bb/media/login/wine.png
--------------------------------------------------------------------------------
/media/motherboard3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Juzley/TheTerminal/363e3ecd9e50946e96ba172800dc2bf9ab73d4bb/media/motherboard3.png
--------------------------------------------------------------------------------
/media/motherboard4.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Juzley/TheTerminal/363e3ecd9e50946e96ba172800dc2bf9ab73d4bb/media/motherboard4.png
--------------------------------------------------------------------------------
/media/resistor.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Juzley/TheTerminal/363e3ecd9e50946e96ba172800dc2bf9ab73d4bb/media/resistor.png
--------------------------------------------------------------------------------
/menu/__init__.py:
--------------------------------------------------------------------------------
1 | """Initialization for the menu package."""
2 |
3 | # Bring any menus to be used from outside the menu package into the menu
4 | # namespace
5 | from menu.mainmenu import MainMenu
6 | from menu.pause import PauseMenu
7 | from menu.level import LevelMenu
8 | from menu.splash import SplashScreen
9 |
--------------------------------------------------------------------------------
/menu/level.py:
--------------------------------------------------------------------------------
1 | """Level select menu."""
2 |
3 |
4 | import json
5 | import programs
6 | from . import menu
7 | from enum import Enum, unique
8 | from gameplay import GameplayState
9 | from resources import make_path
10 |
11 |
12 | class LevelMenu(menu.CLIMenu):
13 |
14 | """The level select menu."""
15 |
16 | @unique
17 | class Items(Enum):
18 | BACK = 1
19 |
20 | _LEVELS_FILE = 'media/levels.json'
21 | _PROGRESS_FILE = 'progress.json'
22 |
23 | def __init__(self, mgr):
24 | """Initialize the class."""
25 | # Load levels from the level file.
26 | with open(make_path(LevelMenu._LEVELS_FILE)) as f:
27 | self._levels = json.load(f)
28 |
29 | # The program class names are represented in the JSON as strings,
30 | # we need to convert them to the corresponding class objects.
31 | for lvl in self._levels:
32 | for group in lvl['program_groups'].values():
33 | for program_info in group['programs']:
34 | program_info[1] = getattr(programs, program_info[1])
35 |
36 | # Load progress information.
37 | progress = LevelMenu._get_progress()
38 | completed = progress.get('completed', [])
39 |
40 | # Build the menu text
41 | buf = [
42 | '$ cd levels',
43 | '$ ls',
44 | menu.CLIMenuItem(' ..', '$ cd ..', LevelMenu.Items.BACK)
45 | ]
46 |
47 | # Add each level as a menu item
48 | for idx, lvl in enumerate(self._levels):
49 | # Work out whether this level is accessible.
50 | disabled = len([r for r in lvl.get('requires', [])
51 | if r not in completed]) > 0
52 | text = ' [{}] {}'.format(
53 | 'x' if lvl['id'] in completed else ' ',
54 | lvl['name'])
55 |
56 | item = menu.CLIMenuItem(text=text,
57 | cmd='$ {}'.format(lvl['cmd']),
58 | item=idx,
59 | disabled=disabled)
60 | buf.append(item)
61 |
62 | # We want to start with the latest available level selected.
63 | enabled = [i for i in buf if
64 | isinstance(i, menu.CLIMenuItem) and not i.disabled]
65 | if enabled:
66 | enabled[-1].selected = True
67 |
68 | super().__init__(mgr, buf)
69 |
70 | @staticmethod
71 | def _get_progress():
72 | """Load the current level progress from disk."""
73 | try:
74 | with open(LevelMenu._PROGRESS_FILE, 'r') as f:
75 | return json.load(f)
76 | except (FileNotFoundError, ValueError):
77 | # The file may not be found if this is the first time the game is
78 | # played or the user hasn't completed any levels. Also ignore any
79 | # JSON parsing errors.
80 | return {}
81 |
82 | @staticmethod
83 | def completed_level(lvl_id):
84 | """Mark a level as completed."""
85 | progress = LevelMenu._get_progress()
86 | completed = progress.get('completed', [])
87 | if lvl_id not in completed:
88 | completed.append(lvl_id)
89 | progress['completed'] = completed
90 |
91 | with open(LevelMenu._PROGRESS_FILE, 'w') as f:
92 | json.dump(progress, f)
93 |
94 | def _on_choose(self, item):
95 | if item == LevelMenu.Items.BACK:
96 | # Return to the main menu.
97 | self._mgr.pop()
98 | else:
99 | # If this isn't an item from enum of items, assume that the user
100 | # clicked on a level - in this case 'item' contains the index of
101 | # the level in the level list.
102 | self._mgr.replace(GameplayState(self._mgr, self._levels[item]))
103 |
--------------------------------------------------------------------------------
/menu/mainmenu.py:
--------------------------------------------------------------------------------
1 | """Main Menu Implementation."""
2 |
3 |
4 | import constants
5 | from .menu import CLIMenu, CLIMenuItem
6 | from .level import LevelMenu
7 | from enum import Enum, unique
8 |
9 |
10 | class MainMenu(CLIMenu):
11 |
12 | """The main menu."""
13 |
14 | @unique
15 | class Items(Enum):
16 | START_GAME = 1
17 | QUIT = 2
18 |
19 | def __init__(self, mgr):
20 | """Initialize the class."""
21 | buf = [
22 | '-' * 60,
23 | '',
24 | 'Welcome to {}'.format(constants.GAMENAME),
25 | '',
26 | '-' * 60,
27 | '',
28 | '$ ls',
29 | CLIMenuItem(' start', '$ start', MainMenu.Items.START_GAME),
30 | CLIMenuItem(' exit', '$ exit', MainMenu.Items.QUIT)
31 | ]
32 | super().__init__(mgr, buf)
33 |
34 | def _on_choose(self, item):
35 | if item == MainMenu.Items.START_GAME:
36 | self._mgr.push(LevelMenu(self._mgr))
37 | elif item == MainMenu.Items.QUIT:
38 | # The main menu should be the last gamestate on the stack, so
39 | # popping it should cause the game to exit.
40 | self._mgr.pop()
41 |
--------------------------------------------------------------------------------
/menu/menu.py:
--------------------------------------------------------------------------------
1 | """Base classes for the menu system."""
2 |
3 |
4 | import pygame
5 | import util
6 | import mouse
7 | import constants
8 | from gamestate import GameState
9 | from resources import load_font
10 |
11 |
12 | class MenuItem:
13 |
14 | """A single item in a menu."""
15 |
16 | def __init__(self, item_id, pos, text, text_size,
17 | colour=(255, 255, 255), selected_colour=(255, 255, 255),
18 | align=util.Align.CENTER):
19 | """Initialize the class."""
20 | self.item_id = item_id
21 | self._pos = pos
22 |
23 | font = load_font(constants.TERMINAL_FONT, text_size)
24 | self._text = font.render(text, True, colour)
25 | self._selected_text = font.render(text, True, selected_colour)
26 |
27 | # Handle alignment
28 | text_width = self._text.get_rect()[2]
29 | surface_width = pygame.display.get_surface().get_rect()[2]
30 | if align == util.Align.LEFT:
31 | self._pos = (0, self._pos[1])
32 | elif align == util.Align.CENTER:
33 | self._pos = (int((surface_width / 2) - (text_width / 2)),
34 | self._pos[1])
35 | else:
36 | self._pos = (surface_width - text_width, self._pos[1])
37 |
38 | def collidepoint(self, pos):
39 | """Determine whether a given point is within this menu item."""
40 | return self._text.get_rect().move(self._pos).collidepoint(pos)
41 |
42 | def draw(self, selected):
43 | """Draw the menu item."""
44 | screen = pygame.display.get_surface()
45 |
46 | if selected:
47 | screen.blit(self._selected_text, self._pos)
48 | else:
49 | screen.blit(self._text, self._pos)
50 |
51 |
52 | class Menu(GameState):
53 |
54 | """
55 | Base class for a single menu screen.
56 |
57 | Note that this is a subclass of gamestate - the intention is that each
58 | menu screen is a separate gamestate.
59 | """
60 |
61 | def __init__(self, items):
62 | """Initialize the class."""
63 | self._items = items
64 | self._selected_index = 0
65 |
66 | def run(self, events):
67 | """Handle events."""
68 | for event in events:
69 | if event.type == pygame.MOUSEBUTTONDOWN:
70 | self._on_mouseclick(event)
71 | elif event.type == pygame.KEYDOWN:
72 | self._on_keypress(event)
73 | elif event.type == pygame.MOUSEMOTION:
74 | self._on_mousemove(event)
75 |
76 | def draw(self):
77 | """Draw the menu."""
78 | for idx, item in enumerate(self._items):
79 | item.draw(idx == self._selected_index)
80 |
81 | def _on_keypress(self, event):
82 | """Handle a keypress."""
83 | if event.key in [pygame.K_UP, pygame.K_LEFT]:
84 | if self._selected_index > 0:
85 | self._selected_index -= 1
86 | elif event.key in [pygame.K_DOWN, pygame.K_RIGHT]:
87 | if self._selected_index < len(self._items) - 1:
88 | self._selected_index += 1
89 | elif event.key in [pygame.K_RETURN, pygame.K_KP_ENTER]:
90 | mouse.current.set_cursor(mouse.Cursor.ARROW)
91 | self._on_choose(self._items[self._selected_index])
92 |
93 | def _on_mouseclick(self, event):
94 | """Handle a mouse click."""
95 | if event.button == mouse.Button.LEFT:
96 | for item in [i for i in self._items if i.collidepoint(event.pos)]:
97 | mouse.current.set_cursor(mouse.Cursor.ARROW)
98 | self._on_choose(item)
99 |
100 | def _on_mousemove(self, event):
101 | """Handle mousemove event."""
102 | over_item = False
103 | for idx, item in enumerate(self._items):
104 | if item.collidepoint(event.pos):
105 | self._selected_index = idx
106 | over_item = True
107 | if over_item:
108 | mouse.current.set_cursor(mouse.Cursor.HAND)
109 | else:
110 | mouse.current.set_cursor(mouse.Cursor.ARROW)
111 |
112 | def _on_choose(self, item):
113 | """Handle activation of a menu item."""
114 | pass
115 |
116 |
117 | class CLIMenuItem:
118 |
119 | """Class representing an item in a CLI Menu."""
120 |
121 | def __init__(self, text, cmd="", item=None, disabled=False, selected=False):
122 | """Initialize the class."""
123 | self.text = text
124 | self.cmd = cmd
125 | self.item = item
126 | self.disabled = disabled
127 | self.selected = selected
128 |
129 |
130 | class CLIMenu(GameState):
131 |
132 | """A menu designed to look like a CLI."""
133 |
134 | _TEXT_SIZE = constants.TERMINAL_TEXT_SIZE
135 | _TEXT_FONT = constants.TERMINAL_FONT
136 | _TEXT_COLOUR = constants.TEXT_COLOUR
137 | _DISABLED_COLOUR = (100, 100, 100)
138 | _TEXT_START = (45, 50)
139 | _CMD_TEXT_POS = (45, 525)
140 |
141 | def __init__(self, mgr, entries):
142 | """Initialize the class."""
143 | super().__init__()
144 | self._mgr = mgr
145 |
146 | self._bezel = util.render_bezel(constants.VERSION_STRING)
147 | self._font = load_font(CLIMenu._TEXT_FONT, CLIMenu._TEXT_SIZE)
148 | self._selected_index = 0
149 | self._items = []
150 | self._cmds = {}
151 |
152 | # Create a '<' image to mark the selected item.
153 | self._select_marker = self._font.render(' <', True,
154 | CLIMenu._TEXT_COLOUR)
155 | self._buf = []
156 | y_coord = CLIMenu._TEXT_START[1]
157 | for entry in entries:
158 | # Render all the text up front, so that we can use the resulting
159 | # surfaces for hit-detection - we store a tuple containing:
160 | # - The surface,
161 | # - Its coordinates,
162 | # - The menu item it represents, if any
163 | colour = CLIMenu._TEXT_COLOUR
164 | disabled = False
165 | if isinstance(entry, CLIMenuItem):
166 | line = entry.text
167 | item = entry.item
168 |
169 | if entry.disabled:
170 | colour = CLIMenu._DISABLED_COLOUR
171 | disabled = True
172 | else:
173 | self._items.append(item)
174 |
175 | # If this entry should start selected, set the
176 | # selected-index appropriately.
177 | if entry.selected:
178 | self._selected_index = len(self._items) - 1
179 |
180 | # If there's a command string associated with this item,
181 | # render the text and store it in a dictionary mapping the
182 | # item ID to the cmd text
183 | if entry.cmd:
184 | self._cmds[item] = self._font.render(
185 | entry.cmd, True, CLIMenu._TEXT_COLOUR)
186 | else:
187 | line = entry
188 | item = None
189 |
190 | text = self._font.render(line, True, colour)
191 | self._buf.append((text, (CLIMenu._TEXT_START[0], y_coord), item,
192 | disabled))
193 | y_coord += CLIMenu._TEXT_SIZE
194 |
195 | def run(self, events):
196 | """Handle events."""
197 | for event in events:
198 | if event.type == pygame.MOUSEBUTTONDOWN:
199 | self._on_mouseclick(event)
200 | elif event.type == pygame.MOUSEMOTION:
201 | self._on_mousemove(event)
202 | elif event.type == pygame.KEYDOWN:
203 | self._on_keypress(event)
204 |
205 | def draw(self):
206 | """Draw the menu."""
207 | selected_item = self._items[self._selected_index]
208 |
209 | # Draw the text
210 | for line, coords, item, disabled in self._buf:
211 | if line:
212 | pygame.display.get_surface().blit(line, coords)
213 |
214 | if item == selected_item and self._highlight_selection():
215 | pygame.display.get_surface().blit(
216 | self._select_marker,
217 | (coords[0] + line.get_rect().w, coords[1]))
218 |
219 | # Draw the command string
220 | if selected_item in self._cmds:
221 | pygame.display.get_surface().blit(self._cmds[selected_item],
222 | CLIMenu._CMD_TEXT_POS)
223 |
224 | # Draw the bezel
225 | pygame.display.get_surface().blit(self._bezel, self._bezel.get_rect())
226 |
227 | @staticmethod
228 | def _highlight_selection():
229 | """Override to control selection highlighting."""
230 | return True
231 |
232 | def _hit_item(self, pos):
233 | """Determine whether a given point hits a menu item."""
234 | # Only consider the lines associated with menu items.
235 | for line, coords, item, disabled in [l for l in self._buf
236 | if l[2] is not None]:
237 | if not disabled and line.get_rect().move(coords).collidepoint(pos):
238 | return item
239 |
240 | return None
241 |
242 | def _on_keypress(self, event):
243 | if event.key in [pygame.K_UP, pygame.K_LEFT]:
244 | if self._selected_index > 0:
245 | self._selected_index -= 1
246 | elif event.key in [pygame.K_DOWN, pygame.K_RIGHT]:
247 | if self._selected_index < len(self._items) - 1:
248 | self._selected_index += 1
249 | elif event.key in [pygame.K_RETURN, pygame.K_KP_ENTER]:
250 | mouse.current.set_cursor(mouse.Cursor.ARROW)
251 | self._on_choose(self._items[self._selected_index])
252 |
253 | def _on_mouseclick(self, event):
254 | """Determine whether we've clicked on a menu item."""
255 | item = self._hit_item(event.pos)
256 | if item is not None:
257 | mouse.current.set_cursor(mouse.Cursor.ARROW)
258 | self._on_choose(item)
259 |
260 | def _on_mousemove(self, event):
261 | """Determine if we've moused over a menu item."""
262 | item = self._hit_item(event.pos)
263 | if item is not None:
264 | self._selected_index = self._items.index(item)
265 | mouse.current.set_cursor(mouse.Cursor.HAND)
266 | else:
267 | mouse.current.set_cursor(mouse.Cursor.ARROW)
268 |
269 | def _on_choose(self, item):
270 | """Handle activation of a menu item."""
271 | pass
272 |
--------------------------------------------------------------------------------
/menu/pause.py:
--------------------------------------------------------------------------------
1 | """Pause Menu implementation."""
2 |
3 | from .menu import Menu, MenuItem
4 | from .mainmenu import MainMenu
5 | from enum import Enum, unique
6 |
7 |
8 | class PauseMenu(Menu):
9 |
10 | """Class defining the pause menu."""
11 |
12 | @unique
13 | class Items(Enum):
14 | RESUME = 1
15 | QUIT = 2
16 |
17 | def __init__(self, mgr, terminal):
18 | """Initialize the class."""
19 | super().__init__(items=[
20 | MenuItem(item_id=PauseMenu.Items.RESUME,
21 | pos=(0, 200),
22 | text='Resume',
23 | text_size=50,
24 | colour=(50, 50, 50),
25 | selected_colour=(20, 200, 20)),
26 | MenuItem(item_id=PauseMenu.Items.QUIT,
27 | pos=(0, 260),
28 | text='Quit',
29 | text_size=50,
30 | colour=(50, 50, 50),
31 | selected_colour=(20, 200, 20))
32 | ])
33 |
34 | self._terminal = terminal
35 | self._mgr = mgr
36 |
37 | def _on_choose(self, item):
38 | """Handle activation of a menu item."""
39 | if item.item_id == PauseMenu.Items.RESUME:
40 | self._terminal.paused = False
41 | self._mgr.pop()
42 | elif item.item_id == PauseMenu.Items.QUIT:
43 | self._mgr.pop_until(MainMenu)
44 |
45 | def run(self, events):
46 | """Run the pause gamestate."""
47 | # Run the terminal so that the timer updates.
48 | self._terminal.run()
49 | super().run(events)
50 |
51 | def draw(self):
52 | """Draw the menu."""
53 | self._terminal.draw_bezel()
54 | super().draw()
55 |
--------------------------------------------------------------------------------
/menu/splash.py:
--------------------------------------------------------------------------------
1 | """Splash screen with manual information."""
2 |
3 | import webbrowser
4 | from enum import Enum, unique
5 | import constants
6 | import timer
7 | from .menu import CLIMenu, CLIMenuItem
8 | from .mainmenu import MainMenu
9 |
10 |
11 | class SplashScreen(CLIMenu):
12 |
13 | """The splash screen."""
14 |
15 | @unique
16 | class Items(Enum):
17 | LAUNCH_MANUAL = 1
18 |
19 | _WAIT_TIME = 1000
20 |
21 | def __init__(self, mgr):
22 | """Initialize the class."""
23 | self._timer = timer.Timer()
24 |
25 | buf = [
26 | '-' * 60,
27 | '',
28 | 'Welcome to {}'.format(constants.GAMENAME),
29 | '',
30 | 'Please read this important notice before you begin',
31 | '',
32 | '-' * 60,
33 | '',
34 | 'This is a cooperative local multiplayer game and must be played',
35 | "with reference to the manual, which can be found in the game's",
36 | 'docs directory, or at: ',
37 | '',
38 | CLIMenuItem('{} (click to view)'.format(constants.MANUAL_URL),
39 | '',
40 | SplashScreen.Items.LAUNCH_MANUAL),
41 | '',
42 | 'Please ensure you have the correct version of the manual to match',
43 | 'your game; this is {} of the game.'.format(
44 | constants.VERSION_STRING),
45 | '',
46 | 'Press any key to continue...'
47 | ]
48 | super().__init__(mgr, buf)
49 |
50 | def run(self, events):
51 | """Handle events."""
52 | self._timer.update()
53 | super().run(events)
54 |
55 | @staticmethod
56 | def _highlight_selection():
57 | """Don't highlight the URL button."""
58 | return False
59 |
60 | def _on_keypress(self, event):
61 | if self._timer.time >= SplashScreen._WAIT_TIME:
62 | self._mgr.replace(MainMenu(self._mgr))
63 |
64 | def _on_mouseclick(self, event):
65 | item = self._hit_item(event.pos)
66 | if item is None:
67 | if self._timer.time >= SplashScreen._WAIT_TIME:
68 | self._mgr.replace(MainMenu(self._mgr))
69 | else:
70 | super()._on_mouseclick(event)
71 |
72 | def _on_choose(self, item):
73 | if item == SplashScreen.Items.LAUNCH_MANUAL:
74 | webbrowser.open(constants.MANUAL_URL)
75 |
--------------------------------------------------------------------------------
/mouse.py:
--------------------------------------------------------------------------------
1 | """Defines various mouse related classes."""
2 |
3 | import pygame
4 |
5 | _HAND_STRINGS = ( # sized 24x24
6 | " XX ",
7 | " X..X ",
8 | " X..X ",
9 | " X..X ",
10 | " X..X ",
11 | " X..XXX ",
12 | " X..X..XXX ",
13 | " X..X..X..XX ",
14 | " X..X..X..X.X ",
15 | "XXX X..X..X..X..X ",
16 | "X..XX........X..X ",
17 | "X...X...........X ",
18 | " X..X...........X ",
19 | " X.X...........X ",
20 | " X.............X ",
21 | " X............X ",
22 | " X...........X ",
23 | " X..........X ",
24 | " X..........X ",
25 | " X........X ",
26 | " X........X ",
27 | " XXXXXXXXXX ",
28 | " ",
29 | " ",
30 | )
31 | _HAND_CURSOR = ((24, 24), (5, 0)) + \
32 | pygame.cursors.compile(_HAND_STRINGS, ".", "X")
33 |
34 |
35 | class Cursor:
36 | """Supported cursors."""
37 | ARROW = 1
38 | HAND = 2
39 |
40 |
41 | class Button:
42 |
43 | """Class representing the different mouse buttons."""
44 |
45 | LEFT = 1
46 | MIDDLE = 2
47 | RIGHT = 3
48 | WHEEL_UP = 4
49 | WHEEL_DOWN = 5
50 |
51 |
52 | class Mouse:
53 |
54 | """Class for tracking and updating cursor."""
55 |
56 | _CURSORS = {
57 | Cursor.ARROW: pygame.cursors.arrow,
58 | Cursor.HAND: _HAND_CURSOR,
59 | }
60 |
61 | def __init__(self):
62 | self._current_cursor = None
63 |
64 | def set_cursor(self, cursor_num):
65 | if self._current_cursor != cursor_num:
66 | pygame.mouse.set_cursor(*Mouse._CURSORS[cursor_num])
67 | self._current_cursor = cursor_num
68 |
69 |
70 | """Current mouse state."""
71 | current = Mouse()
72 |
--------------------------------------------------------------------------------
/programs/__init__.py:
--------------------------------------------------------------------------------
1 | from programs.hardware import HardwareInspect
2 | from programs.hexedit import HexEditor
3 | from programs.password import PasswordGuess
4 | from programs.imagepassword import ImagePassword
5 | from programs.network import NetworkManager
6 | from programs.decrypt import Decrypt
7 | from programs.minehunt import MineHunt
8 |
--------------------------------------------------------------------------------
/programs/decrypt.py:
--------------------------------------------------------------------------------
1 | """Decryption program classes."""
2 |
3 |
4 | import random
5 | from resources import load_font
6 | from . import program
7 |
8 |
9 | class Decrypt(program.TerminalProgram):
10 |
11 | """Program class for the decryption puzzle."""
12 |
13 | # For each font, we have a dictionary mapping from the character in the font
14 | # to what the correct decryption is.
15 | _FONTS = [
16 | ('media/fonts/Arrows.ttf',
17 | {'a': 'a', 'b': 'b', 'c': 'c', 'd': 'd', 'e': 'e', 'f': 'f', 'g': 'g',
18 | 'h': 'h', 'i': 'A', 'j': 'B', 'k': 'C', 'l': 'D', 'm': 'E', 'n': 'F',
19 | 'o': 'G', 'p': 'H', 'q': 'y', 'r': 'z', 's': 'Y', 't': 'Z', 'u': 'm',
20 | 'v': 'n', 'w': 'Q', 'x': 'R', 'y': 'S', 'z': 'T'}),
21 | ('media/fonts/Gobotronic.otf',
22 | {'a': 'n', 'b': 'o', 'c': 'p', 'd': 'q', 'e': 'r', 'f': 's', 'g': 't',
23 | 'h': 'u', 'i': 'v', 'j': 'w', 'k': 'x', 'l': 'y', 'm': 'z', 'n': 'N',
24 | 'o': 'O', 'p': 'P', 'q': 'Q', 'r': 'R', 's': 'S', 't': 'T', 'u': 'U',
25 | 'v': 'V', 'w': 'W', 'x': 'X', 'y': 'Y', 'z': 'Z'}),
26 | ('media/fonts/PigpenCipher.otf',
27 | {'a': 'n', 'b': 'z', 'c': 'q', 'd': 'o', 'e': 'e', 'f': 'g', 'g': 'm',
28 | 'h': 'u', 'i': 'v', 'j': 'p', 'k': 'x', 'l': 'k', 'm': 'f', 'n': 'b',
29 | 'o': 'y', 'p': 'i', 'q': 'l', 'r': 'r', 's': 'c', 't': 't', 'u': 'w',
30 | 'v': 'h', 'w': 'd', 'x': 's', 'y': 'a', 'z': 'j'}),
31 | ('media/fonts/circlethings.ttf',
32 | {'a': 'a', 'b': 'b', 'c': 'c', 'd': 'd', 'e': 'e', 'f': 'f', 'g': 'g',
33 | 'h': 'h', 'i': 'i', 'j': 'j', 'k': 'k', 'l': 'J', 'm': 'm', 'n': 'A',
34 | 'o': 'B', 'p': 'p', 'q': 'q', 'r': 'r', 's': 's', 't': 't', 'u': 'u',
35 | 'v': 'v', 'w': 'w', 'x': 'x', 'y': 'y', 'z': 'z'}),
36 | ]
37 |
38 | _TEXT_SIZE = 40
39 | _MIN_LENGTH = 4
40 | _MAX_LENGTH = 8
41 | _FREEZE_TIME = 2 * 1000
42 |
43 | def __init__(self, terminal):
44 | """Initialize the class."""
45 | super().__init__(terminal)
46 | self._correct = False
47 |
48 | self._fontname = ""
49 | self._cypher = None
50 |
51 | self._enc_string = ""
52 | self._dec_string = ""
53 |
54 | def start(self):
55 | """Start the program."""
56 | self._fontname, self._cypher = random.choice(Decrypt._FONTS)
57 | load_font(self._fontname, Decrypt._TEXT_SIZE)
58 | self._dec_string = ''.join(
59 | [random.choice(list(self._cypher.keys())) for _ in
60 | range(random.randrange(Decrypt._MIN_LENGTH,
61 | Decrypt._MAX_LENGTH + 1))])
62 | self._enc_string = ''.join([self._cypher[e] for e in self._dec_string])
63 | self._terminal.output(['