1 | // Copyright (c) 2020 Lars Pontoppidan. All rights reserved. |
2 | // Use of this source code is governed by the MIT license distributed with this software. |
3 | import term |
4 | import term.ui |
5 | import time |
6 | |
7 | enum Mode { |
8 | menu |
9 | game |
10 | } |
11 | |
12 | const ( |
13 | player_one = 1 // Human control this racket |
14 | player_two = 0 // Take over this AI controller |
15 | white = ui.Color{255, 255, 255} |
16 | orange = ui.Color{255, 140, 0} |
17 | ) |
18 | |
19 | [heap] |
20 | struct App { |
21 | mut: |
22 | tui &ui.Context = unsafe { nil } |
23 | mode Mode = Mode.menu |
24 | width int |
25 | height int |
26 | game &Game = unsafe { nil } |
27 | dt f32 |
28 | ticks i64 |
29 | } |
30 | |
31 | fn (mut a App) init() { |
32 | a.game = &Game{ |
33 | app: a |
34 | } |
35 | w, h := a.tui.window_width, a.tui.window_height |
36 | a.width = w |
37 | a.height = h |
38 | term.erase_del_clear() |
39 | term.set_cursor_position( |
40 | x: 0 |
41 | y: 0 |
42 | ) |
43 | } |
44 | |
45 | fn (mut a App) start_game() { |
46 | if a.mode != .game { |
47 | a.mode = .game |
48 | } |
49 | a.game.init() |
50 | } |
51 | |
52 | fn (mut a App) frame() { |
53 | ticks := time.ticks() |
54 | a.dt = f32(ticks - a.ticks) / 1000.0 |
55 | a.width, a.height = a.tui.window_width, a.tui.window_height |
56 | if a.mode == .game { |
57 | a.game.update() |
58 | } |
59 | a.tui.clear() |
60 | a.render() |
61 | a.tui.flush() |
62 | a.ticks = ticks |
63 | } |
64 | |
65 | fn (mut a App) quit() { |
66 | if a.mode != .menu { |
67 | a.game.quit() |
68 | return |
69 | } |
70 | term.set_cursor_position( |
71 | x: 0 |
72 | y: 0 |
73 | ) |
74 | exit(0) |
75 | } |
76 | |
77 | fn (mut a App) event(e &ui.Event) { |
78 | match e.typ { |
79 | .mouse_move { |
80 | if a.mode != .game { |
81 | return |
82 | } |
83 | // TODO mouse movement for real Pong sharks |
84 | // a.game.move_player(player_one, 0, -1) |
85 | } |
86 | .key_down { |
87 | match e.code { |
88 | .escape, .q { |
89 | a.quit() |
90 | } |
91 | .w { |
92 | if a.mode != .game { |
93 | return |
94 | } |
95 | a.game.move_player(player_one, 0, -1) |
96 | } |
97 | .a { |
98 | if a.mode != .game { |
99 | return |
100 | } |
101 | a.game.move_player(player_one, 0, -1) |
102 | } |
103 | .s { |
104 | if a.mode != .game { |
105 | return |
106 | } |
107 | a.game.move_player(player_one, 0, 1) |
108 | } |
109 | .d { |
110 | if a.mode != .game { |
111 | return |
112 | } |
113 | a.game.move_player(player_one, 0, 1) |
114 | } |
115 | .left { |
116 | if a.mode != .game { |
117 | return |
118 | } |
119 | a.game.move_player(player_two, 0, -1) |
120 | } |
121 | .right { |
122 | if a.mode != .game { |
123 | return |
124 | } |
125 | a.game.move_player(player_two, 0, 1) |
126 | } |
127 | .up { |
128 | if a.mode != .game { |
129 | return |
130 | } |
131 | a.game.move_player(player_two, 0, -1) |
132 | } |
133 | .down { |
134 | if a.mode != .game { |
135 | return |
136 | } |
137 | a.game.move_player(player_two, 0, 1) |
138 | } |
139 | .enter, .space { |
140 | if a.mode == .menu { |
141 | a.start_game() |
142 | } |
143 | } |
144 | else {} |
145 | } |
146 | } |
147 | else {} |
148 | } |
149 | } |
150 | |
151 | fn (mut a App) free() { |
152 | unsafe { |
153 | a.game.free() |
154 | free(a.game) |
155 | } |
156 | } |
157 | |
158 | fn (mut a App) render() { |
159 | match a.mode { |
160 | .menu { a.draw_menu() } |
161 | else { a.draw_game() } |
162 | } |
163 | } |
164 | |
165 | fn (mut a App) draw_menu() { |
166 | cx := int(f32(a.width) * 0.5) |
167 | y025 := int(f32(a.height) * 0.25) |
168 | y075 := int(f32(a.height) * 0.75) |
169 | cy := int(f32(a.height) * 0.5) |
170 | // |
171 | a.tui.set_color(white) |
172 | a.tui.bold() |
173 | a.tui.draw_text(cx - 2, y025, 'VONG') |
174 | a.tui.reset() |
175 | a.tui.draw_text(cx - 13, y025 + 1, '(A game of Pong written in V)') |
176 | // |
177 | a.tui.set_color(white) |
178 | a.tui.bold() |
179 | a.tui.draw_text(cx - 3, cy + 1, 'START') |
180 | a.tui.reset() |
181 | // |
182 | a.tui.draw_text(cx - 9, y075 + 1, 'Press SPACE to start') |
183 | a.tui.reset() |
184 | a.tui.draw_text(cx - 5, y075 + 3, 'ESC to Quit') |
185 | a.tui.reset() |
186 | } |
187 | |
188 | fn (mut a App) draw_game() { |
189 | a.game.draw() |
190 | } |
191 | |
192 | struct Player { |
193 | mut: |
194 | game &Game = unsafe { nil } |
195 | pos Vec |
196 | racket_size int = 4 |
197 | score int |
198 | ai bool |
199 | } |
200 | |
201 | fn (mut p Player) move(x f32, y f32) { |
202 | p.pos.x += x |
203 | p.pos.y += y |
204 | } |
205 | |
206 | fn (mut p Player) update() { |
207 | if !p.ai { |
208 | return |
209 | } |
210 | if isnil(p.game) { |
211 | return |
212 | } |
213 | // dt := p.game.app.dt |
214 | ball := unsafe { &p.game.ball } |
215 | // Evil AI that eventually will take over the world |
216 | p.pos.y = ball.pos.y - int(f32(p.racket_size) * 0.5) |
217 | } |
218 | |
219 | struct Vec { |
220 | mut: |
221 | x f32 |
222 | y f32 |
223 | } |
224 | |
225 | fn (mut v Vec) set(x f32, y f32) { |
226 | v.x = x |
227 | v.y = y |
228 | } |
229 | |
230 | struct Ball { |
231 | mut: |
232 | pos Vec |
233 | vel Vec |
234 | acc Vec |
235 | } |
236 | |
237 | fn (mut b Ball) update(dt f32) { |
238 | b.pos.x += b.vel.x * b.acc.x * dt |
239 | b.pos.y += b.vel.y * b.acc.y * dt |
240 | } |
241 | |
242 | [heap] |
243 | struct Game { |
244 | mut: |
245 | app &App = unsafe { nil } |
246 | players []Player |
247 | ball Ball |
248 | } |
249 | |
250 | fn (mut g Game) move_player(id int, x int, y int) { |
251 | mut p := unsafe { &g.players[id] } |
252 | if p.ai { // disable AI when moved |
253 | p.ai = false |
254 | } |
255 | p.move(x, y) |
256 | } |
257 | |
258 | fn (mut g Game) init() { |
259 | if g.players.len == 0 { |
260 | g.players = []Player{len: 2, init: Player{ // <- BUG omitting the init will result in smaller racket sizes??? |
261 | game: g |
262 | }} |
263 | } |
264 | g.reset() |
265 | } |
266 | |
267 | fn (mut g Game) reset() { |
268 | mut i := 0 |
269 | for mut p in g.players { |
270 | p.score = 0 |
271 | if i != player_one { |
272 | p.ai = true |
273 | } |
274 | i++ |
275 | } |
276 | g.new_round() |
277 | } |
278 | |
279 | fn (mut g Game) new_round() { |
280 | mut i := 0 |
281 | for mut p in g.players { |
282 | p.pos.x = if i == 0 { 3 } else { g.app.width - 2 } |
283 | p.pos.y = f32(g.app.height) * 0.5 - f32(p.racket_size) * 0.5 |
284 | i++ |
285 | } |
286 | g.ball.pos.set(f32(g.app.width) * 0.5, f32(g.app.height) * 0.5) |
287 | g.ball.vel.set(-8, -15) |
288 | g.ball.acc.set(2.0, 1.0) |
289 | } |
290 | |
291 | fn (mut g Game) update() { |
292 | dt := g.app.dt |
293 | mut b := unsafe { &g.ball } |
294 | for mut p in g.players { |
295 | p.update() |
296 | // Keep rackets within the game area |
297 | if p.pos.y <= 0 { |
298 | p.pos.y = 1 |
299 | } |
300 | if p.pos.y + p.racket_size >= g.app.height { |
301 | p.pos.y = g.app.height - p.racket_size - 1 |
302 | } |
303 | // Check ball collision |
304 | // Player left side |
305 | if p.pos.x < f32(g.app.width) * 0.5 { |
306 | // Racket collision |
307 | if b.pos.x <= p.pos.x + 1 { |
308 | if b.pos.y >= p.pos.y && b.pos.y <= p.pos.y + p.racket_size { |
309 | b.vel.x *= -1 |
310 | } |
311 | } |
312 | // Behind racket |
313 | if b.pos.x < p.pos.x { |
314 | g.players[1].score++ |
315 | g.new_round() |
316 | } |
317 | } else { |
318 | // Player right side |
319 | if b.pos.x >= p.pos.x - 1 { |
320 | if b.pos.y >= p.pos.y && b.pos.y <= p.pos.y + p.racket_size { |
321 | b.vel.x *= -1 |
322 | } |
323 | } |
324 | if b.pos.x > p.pos.x { |
325 | g.players[0].score++ |
326 | g.new_round() |
327 | } |
328 | } |
329 | } |
330 | if b.pos.x <= 1 || b.pos.x >= g.app.width { |
331 | b.vel.x *= -1 |
332 | } |
333 | if b.pos.y <= 2 || b.pos.y >= g.app.height { |
334 | b.vel.y *= -1 |
335 | } |
336 | b.update(dt) |
337 | } |
338 | |
339 | fn (mut g Game) quit() { |
340 | if g.app.mode != .game { |
341 | return |
342 | } |
343 | g.app.mode = .menu |
344 | } |
345 | |
346 | fn (mut g Game) draw_big_digit(px f32, py f32, digit int) { |
347 | // TODO use draw_line or draw_point to fix tearing with non-monospaced terminal fonts |
348 | mut gfx := g.app.tui |
349 | x, y := int(px), int(py) |
350 | match digit { |
351 | 0 { |
352 | gfx.draw_text(x, y + 0, '█████') |
353 | gfx.draw_text(x, y + 1, '█ █') |
354 | gfx.draw_text(x, y + 2, '█ █') |
355 | gfx.draw_text(x, y + 3, '█ █') |
356 | gfx.draw_text(x, y + 4, '█████') |
357 | } |
358 | 1 { |
359 | gfx.draw_text(x + 3, y + 0, '█') |
360 | gfx.draw_text(x + 3, y + 1, '█') |
361 | gfx.draw_text(x + 3, y + 2, '█') |
362 | gfx.draw_text(x + 3, y + 3, '█') |
363 | gfx.draw_text(x + 3, y + 4, '█') |
364 | } |
365 | 2 { |
366 | gfx.draw_text(x, y + 0, '█████') |
367 | gfx.draw_text(x, y + 1, ' █') |
368 | gfx.draw_text(x, y + 2, '█████') |
369 | gfx.draw_text(x, y + 3, '█') |
370 | gfx.draw_text(x, y + 4, '█████') |
371 | } |
372 | 3 { |
373 | gfx.draw_text(x, y + 0, '█████') |
374 | gfx.draw_text(x, y + 1, ' ██') |
375 | gfx.draw_text(x, y + 2, ' ████') |
376 | gfx.draw_text(x, y + 3, ' ██') |
377 | gfx.draw_text(x, y + 4, '█████') |
378 | } |
379 | 4 { |
380 | gfx.draw_text(x, y + 0, '█ █') |
381 | gfx.draw_text(x, y + 1, '█ █') |
382 | gfx.draw_text(x, y + 2, '█████') |
383 | gfx.draw_text(x, y + 3, ' █') |
384 | gfx.draw_text(x, y + 4, ' █') |
385 | } |
386 | 5 { |
387 | gfx.draw_text(x, y + 0, '█████') |
388 | gfx.draw_text(x, y + 1, '█') |
389 | gfx.draw_text(x, y + 2, '█████') |
390 | gfx.draw_text(x, y + 3, ' █') |
391 | gfx.draw_text(x, y + 4, '█████') |
392 | } |
393 | 6 { |
394 | gfx.draw_text(x, y + 0, '█████') |
395 | gfx.draw_text(x, y + 1, '█') |
396 | gfx.draw_text(x, y + 2, '█████') |
397 | gfx.draw_text(x, y + 3, '█ █') |
398 | gfx.draw_text(x, y + 4, '█████') |
399 | } |
400 | 7 { |
401 | gfx.draw_text(x, y + 0, '█████') |
402 | gfx.draw_text(x, y + 1, ' █') |
403 | gfx.draw_text(x, y + 2, ' █') |
404 | gfx.draw_text(x, y + 3, ' █') |
405 | gfx.draw_text(x, y + 4, ' █') |
406 | } |
407 | 8 { |
408 | gfx.draw_text(x, y + 0, '█████') |
409 | gfx.draw_text(x, y + 1, '█ █') |
410 | gfx.draw_text(x, y + 2, '█████') |
411 | gfx.draw_text(x, y + 3, '█ █') |
412 | gfx.draw_text(x, y + 4, '█████') |
413 | } |
414 | 9 { |
415 | gfx.draw_text(x, y + 0, '█████') |
416 | gfx.draw_text(x, y + 1, '█ █') |
417 | gfx.draw_text(x, y + 2, '█████') |
418 | gfx.draw_text(x, y + 3, ' █') |
419 | gfx.draw_text(x, y + 4, '█████') |
420 | } |
421 | else {} |
422 | } |
423 | } |
424 | |
425 | fn (mut g Game) draw() { |
426 | mut gfx := g.app.tui |
427 | gfx.set_bg_color(white) |
428 | // Border |
429 | gfx.draw_empty_rect(1, 1, g.app.width, g.app.height) |
430 | // Center line |
431 | gfx.draw_dashed_line(int(f32(g.app.width) * 0.5), 0, int(f32(g.app.width) * 0.5), |
432 | int(g.app.height)) |
433 | border := 1 |
434 | mut y, mut x := 0, 0 |
435 | for p in g.players { |
436 | x = int(p.pos.x) |
437 | y = int(p.pos.y) |
438 | gfx.reset_bg_color() |
439 | gfx.set_color(white) |
440 | if x < f32(g.app.width) * 0.5 { |
441 | g.draw_big_digit(f32(g.app.width) * 0.25, 3, p.score) |
442 | } else { |
443 | g.draw_big_digit(f32(g.app.width) * 0.75, 3, p.score) |
444 | } |
445 | gfx.reset_color() |
446 | gfx.set_bg_color(white) |
447 | // Racket |
448 | gfx.draw_line(x, y + border, x, y + p.racket_size) |
449 | } |
450 | // Ball |
451 | gfx.draw_point(int(g.ball.pos.x), int(g.ball.pos.y)) |
452 | // gfx.draw_text(22,2,'$g.ball.pos') |
453 | gfx.reset_bg_color() |
454 | } |
455 | |
456 | fn (mut g Game) free() { |
457 | g.players.clear() |
458 | } |
459 | |
460 | // TODO Remove these wrapper functions when we can assign methods as callbacks |
461 | fn init(mut app App) { |
462 | app.init() |
463 | } |
464 | |
465 | fn frame(mut app App) { |
466 | app.frame() |
467 | } |
468 | |
469 | fn cleanup(mut app App) { |
470 | unsafe { |
471 | app.free() |
472 | } |
473 | } |
474 | |
475 | fn fail(error string) { |
476 | eprintln(error) |
477 | } |
478 | |
479 | fn event(e &ui.Event, mut app App) { |
480 | app.event(e) |
481 | } |
482 | |
483 | fn main() { |
484 | mut app := &App{} |
485 | app.tui = ui.init( |
486 | user_data: app |
487 | init_fn: init |
488 | frame_fn: frame |
489 | cleanup_fn: cleanup |
490 | event_fn: event |
491 | fail_fn: fail |
492 | capture_events: true |
493 | hide_cursor: true |
494 | frame_rate: 60 |
495 | ) |
496 | app.tui.run()! |
497 | } |