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