// Copyright (c) 2019-2020 Alexander Medvednikov. All rights reserved. // Use of this source code is governed by an MIT license // that can be found in the LICENSE file. // SDL2 port+wrapper, Twintris-like dual-game logic, // and more, by Nicolas Sauzede 2019. module main import rand import time import os import math import sdl import sdl.image as img import sdl.mixer as mix import sdl.ttf as ttf [inline] fn sdl_fill_rect(s &SDL_Surface,r &SDL_Rect,c &SDL_Color){sdl.fill_rect(s,r,c)} const ( Title = 'tVintris' BASE = os.dir( os.real_path( os.executable() ) ) FontName = BASE + '/../../assets/fonts/RobotoMono-Regular.ttf' MusicName = BASE + '/sounds/TwintrisThosenine.mod' SndBlockName = BASE + '/sounds/block.wav' SndLineName = BASE + '/sounds/single.wav' SndDoubleName = BASE + '/sounds/triple.wav' VLogo = BASE + '/images/v-logo_30_30.png' BlockSize = 20 // pixels FieldHeight = 20 // # of blocks FieldWidth = 10 TetroSize = 4 WinWidth = BlockSize * FieldWidth * 3 WinHeight = BlockSize * FieldHeight TimerPeriod = 250 // ms TextSize = 16 AudioBufSize = 1024 P2FIRE = C.SDLK_l P2UP = C.SDLK_UP P2DOWN = C.SDLK_DOWN P2LEFT = C.SDLK_LEFT P2RIGHT = C.SDLK_RIGHT P1FIRE = C.SDLK_s P1UP = C.SDLK_w P1DOWN = C.SDLK_x P1LEFT = C.SDLK_a P1RIGHT = C.SDLK_d NJOYMAX = 2 // joystick name => enter your own device name JOYP1NAME = 'Generic X-Box pad' // following are joystick button number JBP1FIRE = 1 // following are joystick hat value JHP1UP = 1 JHP1DOWN = 4 JHP1LEFT = 8 JHP1RIGHT = 3 // joystick name => enter your own device name JOYP2NAME = 'RedOctane Guitar Hero X-plorer' // following are joystick button number JBP2FIRE = 0 // following are joystick hat value JHP2UP = 4 JHP2DOWN = 1 JHP2LEFT = 8 JHP2RIGHT = 2 ) const ( mix_version = mix.version ttf_version = ttf.version ) const ( // Tetros' 4 possible states are encoded in binaries BTetros = [ // 0000 0 // 0000 0 // 0110 6 // 0110 6 [66, 66, 66, 66], // 0000 0 // 0000 0 // 0010 2 // 0111 7 [27, 131, 72, 232], // 0000 0 // 0000 0 // 0011 3 // 0110 6 [36, 231, 36, 231], // 0000 0 // 0000 0 // 0110 6 // 0011 3 [63, 132, 63, 132], // 0000 0 // 0011 3 // 0001 1 // 0001 1 [311, 17, 223, 74], // 0000 0 // 0011 3 // 0010 2 // 0010 2 [322, 71, 113, 47], // Special case since 15 can't be used // 1111 [1111, 9, 1111, 9], ] // Each tetro has its unique color Colors = [ SDL_Color{byte(0), byte(0), byte(0), byte(0)}, // unused ? SDL_Color{byte(0), byte(0x62), byte(0xc0), byte(0)}, // quad : darkblue 0062c0 SDL_Color{byte(0xca), byte(0x7d), byte(0x5f), byte(0)}, // tricorn : lightbrown ca7d5f SDL_Color{byte(0), byte(0xc1), byte(0xbf), byte(0)}, // short topright : lightblue 00c1bf SDL_Color{byte(0), byte(0xc1), byte(0), byte(0)}, // short topleft : lightgreen 00c100 SDL_Color{byte(0xbf), byte(0xbe), byte(0), byte(0)}, // long topleft : yellowish bfbe00 SDL_Color{byte(0xd1), byte(0), byte(0xbf), byte(0)}, // long topright : pink d100bf SDL_Color{byte(0xd1), byte(0), byte(0), byte(0)}, // longest : lightred d10000 SDL_Color{byte(0), byte(170), byte(170), byte(0)}, // unused ? ] // Background color BackgroundColor = SDL_Color{byte(0), byte(0), byte(0), byte(0)} // Foreground color ForegroundColor = SDL_Color{byte(0), byte(170), byte(170), byte(0)} // Text color TextColor = SDL_Color{byte(0xca), byte(0x7d), byte(0x5f), byte(0)} ) // TODO: type Tetro [TetroSize]struct{ x, y int } struct Block { mut: x int y int } enum GameState { paused running gameover } struct AudioContext { mut: music voidptr volume int waves [3]voidptr } struct SdlContext { pub mut: // VIDEO w int h int window voidptr renderer voidptr screen &SDL_Surface texture voidptr // AUDIO actx AudioContext // JOYSTICKS jnames [2]string jids [2]int // V logo v_logo &SDL_Surface tv_logo voidptr } struct Game { mut: // Score of the current game score int // Count consecutive lines for scoring lines int // State of the current game state GameState // X offset of the game display ofs_x int // keys k_fire int k_up int k_down int k_left int k_right int // joystick ID joy_id int // joystick buttons jb_fire int // joystick hat values jh_up int jh_down int jh_left int jh_right int // game rand seed seed int seed_ini int // Position of the current tetro pos_x int pos_y int // field[y][x] contains the color of the block with (x,y) coordinates // "-1" border is to avoid bounds checking. // -1 -1 -1 -1 // -1 0 0 -1 // -1 0 0 -1 // -1 -1 -1 -1 field [][]int // TODO: tetro Tetro tetro []Block // TODO: tetros_cache []Tetro tetros_cache []Block // Index of the current tetro. Refers to its color. tetro_idx int // Index of the next tetro. Refers to its color. tetro_next int // tetro stats : buckets of drawn tetros tetro_stats []int // total number of drawn tetros tetro_total int // Index of the rotation (0-3) rotation_idx int // SDL2 context for drawing sdl SdlContext // TTF context for font drawing font voidptr } fn (sdlc mut SdlContext) set_sdl_context(w int, h int, title string) { C.SDL_Init(C.SDL_INIT_VIDEO | C.SDL_INIT_AUDIO | C.SDL_INIT_JOYSTICK) C.atexit(C.SDL_Quit) C.TTF_Init() C.atexit(C.TTF_Quit) bpp := 32 sdl.create_window_and_renderer(w, h, 0, &sdlc.window, &sdlc.renderer) // C.SDL_CreateWindowAndRenderer(w, h, 0, voidptr(&sdlc.window), voidptr(&sdlc.renderer)) C.SDL_SetWindowTitle(sdlc.window, title.str) sdlc.w = w sdlc.h = h sdlc.screen = sdl.create_rgb_surface(0, w, h, bpp, 0x00FF0000, 0x0000FF00, 0x000000FF, 0xFF000000) sdlc.texture = C.SDL_CreateTexture(sdlc.renderer, C.SDL_PIXELFORMAT_ARGB8888, C.SDL_TEXTUREACCESS_STREAMING, w, h) C.Mix_Init(C.MIX_INIT_MOD) C.atexit(C.Mix_Quit) if C.Mix_OpenAudio(48000,C.MIX_DEFAULT_FORMAT,2,AudioBufSize) < 0 { println('couldn\'t open audio') } println('opening music $MusicName') sdlc.actx.music = C.Mix_LoadMUS(MusicName.str) sdlc.actx.waves[0] = C.Mix_LoadWAV(SndBlockName.str) sdlc.actx.waves[1] = C.Mix_LoadWAV(SndLineName.str) sdlc.actx.waves[2] = C.Mix_LoadWAV(SndDoubleName.str) sdlc.actx.volume = C.SDL_MIX_MAXVOLUME if C.Mix_PlayMusic(sdlc.actx.music, 1) != -1 { C.Mix_VolumeMusic(sdlc.actx.volume) } njoy := C.SDL_NumJoysticks() for i in 0..njoy { C.SDL_JoystickOpen(i) jn := tos_clone(sdl.joystick_name_for_index(i)) println('JOY NAME $jn') for j in 0..NJOYMAX { if sdlc.jnames[j] == jn { println('FOUND JOYSTICK $j $jn ID=$i') sdlc.jids[j] = i } } } flags := C.IMG_INIT_PNG imgres := img.img_init(flags) if (imgres & flags) != flags { println('error initializing image library.') } println('opening logo $VLogo') sdlc.v_logo = img.load(VLogo) if !isnil(sdlc.v_logo) { // println('got v_logo=$sdlc.v_logo') sdlc.tv_logo = sdl.create_texture_from_surface(sdlc.renderer, sdlc.v_logo) // println('got tv_logo=$sdlc.tv_logo') } C.SDL_JoystickEventState(C.SDL_ENABLE) } fn main() { println('tVintris -- tribute to venerable Twintris') mut game := &Game{ font: 0 } game.sdl.jnames[0] = JOYP1NAME game.sdl.jnames[1] = JOYP2NAME game.sdl.jids[0] = -1 game.sdl.jids[1] = -1 game.sdl.set_sdl_context(WinWidth, WinHeight, Title) game.font = C.TTF_OpenFont(FontName.str, TextSize) seed := time.now().unix mut game2 := &Game{ font: 0 } game2.sdl = game.sdl game2.font = game.font game.joy_id = game.sdl.jids[0] // println('JOY1 id=${game.joy_id}') game2.joy_id = game.sdl.jids[1] // println('JOY2 id=${game2.joy_id}') // delay uses milliseconds so 1000 ms / 30 frames (30fps) roughly = 33.3333 ms/frame time_per_frame := 1000.0 / 30.0 game.k_fire = P1FIRE game.k_up = P1UP game.k_down = P1DOWN game.k_left = P1LEFT game.k_right = P1RIGHT game.jb_fire = JBP1FIRE game.jh_up = JHP1UP game.jh_down = JHP1DOWN game.jh_left = JHP1LEFT game.jh_right = JHP1RIGHT game.ofs_x = 0 game.seed_ini = seed game.init_game() game.state = .running go game.run() // Run the game loop in a new thread game2.k_fire = P2FIRE game2.k_up = P2UP game2.k_down = P2DOWN game2.k_left = P2LEFT game2.k_right = P2RIGHT game2.jb_fire = JBP2FIRE game2.jh_up = JHP2UP game2.jh_down = JHP2DOWN game2.jh_left = JHP2LEFT game2.jh_right = JHP2RIGHT game2.ofs_x = WinWidth * 2 / 3 game2.seed_ini = seed game2.init_game() game2.state = .running go game2.run() // Run the game loop in a new thread mut g := Game{ font: 0 } mut should_close := false mut total_frame_ticks := u64(0) mut total_frames := u32(0) for { total_frames++ start_ticks := sdl.get_perf_counter() g1 := game g2 := game2 // here we determine which game contains most recent state if g1.tetro_total > g.tetro_total { g = *g1 } if g2.tetro_total > g.tetro_total { g = *g2 } g.draw_begin() g1.draw_tetro() g1.draw_field() g2.draw_tetro() g2.draw_field() g.draw_middle() g1.draw_score() g2.draw_score() g.draw_stats() g.draw_v_logo() g.draw_end() // game.handle_events() // CRASHES if done in function ??? evt := SDL_Event{} for 0 < sdl.poll_event(&evt) { match int(evt.@type) { C.SDL_QUIT { should_close = true } C.SDL_KEYDOWN { key := evt.key.keysym.sym if key == C.SDLK_ESCAPE { should_close = true break } game.handle_key(key) game2.handle_key(key) } C.SDL_JOYBUTTONDOWN { jb := int(evt.jbutton.button) joyid := evt.jbutton.which // println('JOY BUTTON $jb $joyid') game.handle_jbutton(jb, joyid) game2.handle_jbutton(jb, joyid) } C.SDL_JOYHATMOTION { jh := int(evt.jhat.hat) jv := int(evt.jhat.value) joyid := evt.jhat.which // println('JOY HAT $jh $jv $joyid') game.handle_jhat(jh, jv, joyid) game2.handle_jhat(jh, jv, joyid) } else {} } } if should_close { break } end_ticks := sdl.get_perf_counter() total_frame_ticks += end_ticks-start_ticks elapsed_time := f64(end_ticks - start_ticks) / f64(sdl.get_perf_frequency()) // current_fps := 1.0 / elapsed_time // should limit system to (1 / time_per_frame) fps sdl.delay(u32(math.floor(time_per_frame - elapsed_time))) } if game.font != voidptr(0) { C.TTF_CloseFont(game.font) } if game.sdl.actx.music != voidptr(0) { C.Mix_FreeMusic(game.sdl.actx.music) } C.Mix_CloseAudio() if game.sdl.actx.waves[0] != voidptr(0) { C.Mix_FreeChunk(game.sdl.actx.waves[0]) } if game.sdl.actx.waves[1] != voidptr(0) { C.Mix_FreeChunk(game.sdl.actx.waves[1]) } if game.sdl.actx.waves[2] != voidptr(0) { C.Mix_FreeChunk(game.sdl.actx.waves[2]) } if !isnil(game.sdl.tv_logo) { sdl.destroy_texture(game.sdl.tv_logo) } if !isnil(game.sdl.v_logo) { sdl.free_surface(game.sdl.v_logo) } } enum Action { idle space fire } fn (game mut Game) handle_key(key int) { // global keys mut action := Action.idle match key { C.SDLK_SPACE { action = .space } game.k_fire { action = .fire } else {} } if action == .space { match game.state { .running { C.Mix_PauseMusic() game.state = .paused } .paused { C.Mix_ResumeMusic() game.state = .running } else {} } } if action == .fire { match game.state { .gameover { game.init_game() game.state = .running } else {} } } if game.state != .running { return } // keys while game is running match key { game.k_up { game.rotate_tetro() } game.k_left { game.move_right(-1) } game.k_right { game.move_right(1) } game.k_down { game.move_tetro() } // drop faster when the player presses else {} } } fn (game mut Game) handle_jbutton(jb int, joyid int) { if joyid != game.joy_id { return } // global buttons mut action := Action.idle match jb { game.jb_fire { action = .fire } else {} } if action == .fire { match game.state { .gameover { game.init_game() game.state = .running } else {} } } } fn (game mut Game) handle_jhat(jh int, jv int, joyid int) { if joyid != game.joy_id { return } if game.state != .running { return } // println('testing hat values.. joyid=$joyid jh=$jh jv=$jv') // hat values while game is running match jv { game.jh_up { game.rotate_tetro() } game.jh_left { game.move_right(-1) } game.jh_right { game.move_right(1) } game.jh_down { game.move_tetro() } // drop faster when the player presses else {} } } fn (g mut Game) init_game() { g.score = 0 g.tetro_total = 0 g.tetro_stats = [0, 0, 0, 0, 0, 0, 0] g.parse_tetros() g.seed = g.seed_ini g.generate_tetro() g.field = [] // Generate the field, fill it with 0's, add -1's on each edge for i in 0..FieldHeight + 2 { mut row := [0].repeat(FieldWidth + 2) row[0] = - 1 row[FieldWidth + 1] = - 1 g.field << row } mut first_row := g.field[0] mut last_row := g.field[FieldHeight + 1] for j in 0..FieldWidth + 2 { first_row[j] = - 1 last_row[j] = - 1 } } fn (g mut Game) parse_tetros() { for b_tetros in BTetros { for b_tetro in b_tetros { for t in parse_binary_tetro(b_tetro) { g.tetros_cache << t } } } } fn (g mut Game) run() { for { if g.state == .running { g.move_tetro() n := g.delete_completed_lines() if n > 0 { g.lines += n } else { if g.lines > 0 { if g.lines > 1 { C.Mix_PlayChannel(0, g.sdl.actx.waves[2], 0) } else if g.lines == 1 { C.Mix_PlayChannel(0, g.sdl.actx.waves[1], 0) } g.score += 10 * g.lines * g.lines g.lines = 0 } } } time.sleep_ms(TimerPeriod) // medium delay between game step } } fn (game mut Game) rotate_tetro() { // Rotate the tetro old_rotation_idx := game.rotation_idx game.rotation_idx++ if game.rotation_idx == TetroSize { game.rotation_idx = 0 } game.get_tetro() if !game.move_right(0) { game.rotation_idx = old_rotation_idx game.get_tetro() } if game.pos_x < 0 { game.pos_x = 1 } } fn (g mut Game) move_tetro() { // Check each block in current tetro for block in g.tetro { y := block.y + g.pos_y + 1 x := block.x + g.pos_x // Reached the bottom of the screen or another block? // TODO: if g.field[y][x] != 0 //if g.field[y][x] != 0 { row := g.field[y] if row[x] != 0 { // The new tetro has no space to drop => end of the game if g.pos_y < 2 { g.state = .gameover g.tetro_total = 0 return } // Drop it and generate a new one g.drop_tetro() g.generate_tetro() C.Mix_PlayChannel(0, g.sdl.actx.waves[0], 0) return } } g.pos_y++ } fn (g mut Game) move_right(dx int) bool { // Reached left/right edge or another tetro? for i in 0..TetroSize { tetro := g.tetro[i] y := tetro.y + g.pos_y x := tetro.x + g.pos_x + dx row := g.field[y] if row[x] != 0 { // Do not move return false } } g.pos_x += dx return true } fn (g &Game) delete_completed_lines() int { mut n := 0 for y := FieldHeight; y >= 1; y-- { n += g.delete_completed_line(y) } return n } fn (g &Game) delete_completed_line(y int) int { for x := 1; x <= FieldWidth; x++ { f := g.field[y] if f[x] == 0 { return 0 } } // Move everything down by 1 position for yy := y - 1; yy >= 1; yy-- { for x := 1; x <= FieldWidth; x++ { mut a := g.field[yy + 1] b := g.field[yy] a[x] = b[x] } } return 1 } // Draw a rand tetro index fn (g mut Game) rand_tetro() int { cur := g.tetro_next g.tetro_next = rand.rand_r(&g.seed) g.tetro_next = g.tetro_next % BTetros.len return cur } // Place a new tetro on top fn (g mut Game) generate_tetro() { g.pos_y = 0 g.pos_x = FieldWidth / 2 - TetroSize / 2 g.tetro_idx = g.rand_tetro() // println('idx=${g.tetro_idx}') g.tetro_stats[g.tetro_idx]+= 2 -1 g.tetro_total++ g.rotation_idx = 0 g.get_tetro() } // Get the right tetro from cache fn (g mut Game) get_tetro() { idx := g.tetro_idx * TetroSize * TetroSize + g.rotation_idx * TetroSize g.tetro = g.tetros_cache[idx .. idx + TetroSize] } fn (g &Game) drop_tetro() { for i in 0..TetroSize { tetro := g.tetro[i] x := tetro.x + g.pos_x y := tetro.y + g.pos_y // Remember the color of each block // TODO: g.field[y][x] = g.tetro_idx + 1 mut row := g.field[y] row[x] = g.tetro_idx + 1 } } fn (g &Game) draw_tetro() { for i in 0..TetroSize { tetro := g.tetro[i] g.draw_block(g.pos_y + tetro.y, g.pos_x + tetro.x, g.tetro_idx + 1) } } fn (g &Game) draw_block(i, j, color_idx int) { rect := SDL_Rect {g.ofs_x + (j - 1) * BlockSize, (i - 1) * BlockSize, BlockSize - 1, BlockSize - 1} col := Colors[color_idx] sdl_fill_rect(g.sdl.screen, &rect, &col) } fn (g &Game) draw_field() { for i := 1; i < FieldHeight + 1; i++ { for j := 1; j < FieldWidth + 1; j++ { f := g.field[i] if f[j] > 0 { g.draw_block(i, j, f[j]) } } } } fn (g &Game) draw_v_logo() { if isnil(g.sdl.tv_logo) { return } texw := 0 texh := 0 C.SDL_QueryTexture(g.sdl.tv_logo, 0, 0, &texw, &texh) dstrect := SDL_Rect { (WinWidth / 2) - (texw / 2), 20, texw, texh } // Currently we can't seem to use sdl.render_copy when we need to pass a nil pointer (eg: srcrect to be NULL) // sdl.render_copy(g.sdl.renderer, tv_logo, 0, &dstrect) C.SDL_RenderCopy(g.sdl.renderer, g.sdl.tv_logo, voidptr(0), voidptr(&dstrect)) } fn (g &Game) draw_text(x int, y int, text string, tcol SDL_Color) { _tcol := C.SDL_Color{tcol.r, tcol.g, tcol.b, tcol.a} tsurf := C.TTF_RenderText_Solid(g.font, text.str, _tcol) ttext := C.SDL_CreateTextureFromSurface(g.sdl.renderer, tsurf) texw := 0 texh := 0 C.SDL_QueryTexture(ttext, 0, 0, &texw, &texh) dstrect := SDL_Rect { x, y, texw, texh } // sdl.render_copy(g.sdl.renderer, ttext, 0, &dstrect) C.SDL_RenderCopy(g.sdl.renderer, ttext, voidptr(0), voidptr(&dstrect)) C.SDL_DestroyTexture(ttext) sdl.free_surface(tsurf) } [inline] fn (g &Game) draw_ptext(x int, y int, text string, tcol SDL_Color) { g.draw_text(g.ofs_x + x, y, text, tcol) } [live] fn (g &Game) draw_begin() { // println('about to clear') C.SDL_RenderClear(g.sdl.renderer) mut rect := SDL_Rect {0,0,g.sdl.w,g.sdl.h} col := SDL_Color{byte(00), byte(00), byte(0), byte(0)} // sdl_fill_rect(g.sdl.screen, &rect, BackgroundColor) sdl_fill_rect(g.sdl.screen, &rect, col) rect = SDL_Rect {BlockSize * FieldWidth + 2,0,2,g.sdl.h} sdl_fill_rect(g.sdl.screen, &rect, ForegroundColor) rect = SDL_Rect {WinWidth - BlockSize * FieldWidth - 4,0,2,g.sdl.h} sdl_fill_rect(g.sdl.screen, &rect, ForegroundColor) mut idx := 0 for st in g.tetro_stats { mut s := 10 if g.tetro_total > 0 { s += 90 * st / g.tetro_total } w := BlockSize h := s * 4 * w / 100 rect = SDL_Rect {(WinWidth - 7 * (w + 1)) / 2 + idx * (w + 1), WinHeight * 3 / 4 - h, w, h} sdl_fill_rect(g.sdl.screen, &rect, Colors[idx + 1]) idx++ } } fn (g &Game) draw_middle() { C.SDL_UpdateTexture(g.sdl.texture, 0, g.sdl.screen.pixels, g.sdl.screen.pitch) // sdl.render_copy(g.sdl.renderer, g.sdl.texture, voidptr(0), voidptr(0)) C.SDL_RenderCopy(g.sdl.renderer, g.sdl.texture, voidptr(0), voidptr(0)) } fn (g &Game) draw_score() { if g.font != voidptr(0) { g.draw_ptext(1, 2, 'score: ' + g.score.str() + ' nxt=' + g.tetro_next.str(), TextColor) if g.state == .gameover { g.draw_ptext(1, WinHeight / 2 + 0 * TextSize, 'Game Over', TextColor) g.draw_ptext(1, WinHeight / 2 + 2 * TextSize, 'FIRE to restart', TextColor) } else if g.state == .paused { g.draw_ptext(1, WinHeight / 2 + 0 * TextSize, 'Game Paused', TextColor) g.draw_ptext(1, WinHeight / 2 + 2 * TextSize, 'SPACE to resume', TextColor) } } } fn (g &Game) draw_stats() { if g.font != voidptr(0) { g.draw_text(WinWidth / 3 + 10, WinHeight * 3 / 4 + 0 * TextSize, 'stats: ' + g.tetro_total.str() + ' tetros', TextColor) mut stats := '' for st in g.tetro_stats { mut s := 0 if g.tetro_total > 0 { s = 100 * st / g.tetro_total } stats += ' ' stats += s.str() } g.draw_text(WinWidth / 3 - 8, WinHeight * 3 / 4 + 2 * TextSize, stats, TextColor) } } fn (g &Game) draw_end() { C.SDL_RenderPresent(g.sdl.renderer) } fn parse_binary_tetro(t_ int) []Block { mut t := t_ res := [Block{}].repeat(4) mut cnt := 0 horizontal := t == 9// special case for the horizontal line for i := 0; i <= 3; i++ { // Get ith digit of t p := int(math.pow(10, 3 - i)) mut digit := t / p t %= p // Convert the digit to binary for j := 3; j >= 0; j-- { bin := digit % 2 digit /= 2 if bin == 1 || (horizontal && i == TetroSize - 1) { // TODO: res[cnt].x = j // res[cnt].y = i mut point := &res[cnt] point.x = j point.y = i cnt++ } } } return res }