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