v / examples / term.ui
Raw file | 497 loc (462 sloc) | 9.54 KB | Latest commit hash 868908b80
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 (
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]
20struct App {
21mut:
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
31fn (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
45fn (mut a App) start_game() {
46 if a.mode != .game {
47 a.mode = .game
48 }
49 a.game.init()
50}
51
52fn (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
65fn (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
77fn (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
151fn (mut a App) free() {
152 unsafe {
153 a.game.free()
154 free(a.game)
155 }
156}
157
158fn (mut a App) render() {
159 match a.mode {
160 .menu { a.draw_menu() }
161 else { a.draw_game() }
162 }
163}
164
165fn (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
188fn (mut a App) draw_game() {
189 a.game.draw()
190}
191
192struct Player {
193mut:
194 game &Game = unsafe { nil }
195 pos Vec
196 racket_size int = 4
197 score int
198 ai bool
199}
200
201fn (mut p Player) move(x f32, y f32) {
202 p.pos.x += x
203 p.pos.y += y
204}
205
206fn (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
219struct Vec {
220mut:
221 x f32
222 y f32
223}
224
225fn (mut v Vec) set(x f32, y f32) {
226 v.x = x
227 v.y = y
228}
229
230struct Ball {
231mut:
232 pos Vec
233 vel Vec
234 acc Vec
235}
236
237fn (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]
243struct Game {
244mut:
245 app &App = unsafe { nil }
246 players []Player
247 ball Ball
248}
249
250fn (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
258fn (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
267fn (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
279fn (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
291fn (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
339fn (mut g Game) quit() {
340 if g.app.mode != .game {
341 return
342 }
343 g.app.mode = .menu
344}
345
346fn (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
425fn (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
456fn (mut g Game) free() {
457 g.players.clear()
458}
459
460// TODO Remove these wrapper functions when we can assign methods as callbacks
461fn init(mut app App) {
462 app.init()
463}
464
465fn frame(mut app App) {
466 app.frame()
467}
468
469fn cleanup(mut app App) {
470 unsafe {
471 app.free()
472 }
473}
474
475fn fail(error string) {
476 eprintln(error)
477}
478
479fn event(e &ui.Event, mut app App) {
480 app.event(e)
481}
482
483fn 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}