I recently got an issue request to build games in Glimmer DSL for LibUI, so I went ahead and built Glimmer Tetris.
Of course, I followed the Glimmer Process in building it, so I released the following version changes of Glimmer DSL for LibUI along the way:
0.2.20:
- Improve examples/tetris.rb with menus, high score dialog, and options
- Prevent examples/tetris.rb `window` from being resized
- Support `window` `resizable` property (`resizable false` means one cannot resize `window`)
- Support calling `window.content_size = [x, y]` as an alternative to `window.set_content_size(x, y)`
- Fix issue with hooking `on_content_size_changed` listener to `window`
- Fix issue with using `window` `content_size` property getter
0.2.19:
- Improve examples/tetris.rb with a score board (indicating next Tetromino, score, level, and lines)
- Add instant down action to examples/tetris.rb upon hitting the space button
0.2.18:
- Support `polygon` (closed figure of lines), `polyline` (open figure of lines), and `polybezier` (open figure of beziers) shape keywords to use under `path`
- Improve examples/tetris.rb with bevel block 3D look and restarting upon game over
- Update examples/area_gallery.rb to add uses of `polygon`, `polyline`, and `polybezier`
- Refactor examples/histogram.rb to utilize new `polygon` and `polyline` keywords
- Support `area` `request_auto_redraw`, `pause_auto_redraw`, and `resume_auto_redraw`, operations, and `auto_redraw_enabled` property.
0.2.17:
- Tetris example - basic version with simple color squares
Screenshots:
Code:
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# From: https://github.com/AndyObtiva/glimmer-dsl-libui#tetris | |
require 'glimmer-dsl-libui' | |
require_relative 'tetris/model/game' | |
class Tetris | |
include Glimmer | |
BLOCK_SIZE = 25 | |
BEVEL_CONSTANT = 20 | |
COLOR_GRAY = {r: 192, g: 192, b: 192} | |
def initialize | |
@game = Model::Game.new | |
end | |
def launch | |
create_gui | |
register_observers | |
@game.start! | |
@main_window.show | |
end | |
def create_gui | |
menu_bar | |
@main_window = window('Glimmer Tetris') { | |
content_size Model::Game::PLAYFIELD_WIDTH * BLOCK_SIZE, Model::Game::PLAYFIELD_HEIGHT * BLOCK_SIZE + 98 | |
resizable false | |
vertical_box { | |
label { # filler | |
stretchy false | |
} | |
score_board(block_size: BLOCK_SIZE) { | |
stretchy false | |
} | |
@playfield_blocks = playfield(playfield_width: Model::Game::PLAYFIELD_WIDTH, playfield_height: Model::Game::PLAYFIELD_HEIGHT, block_size: BLOCK_SIZE) | |
} | |
} | |
end | |
def register_observers | |
Glimmer::DataBinding::Observer.proc do |game_over| | |
if game_over | |
@pause_menu_item.enabled = false | |
show_game_over_dialog | |
else | |
@pause_menu_item.enabled = true | |
start_moving_tetrominos_down | |
end | |
end.observe(@game, :game_over) | |
Model::Game::PLAYFIELD_HEIGHT.times do |row| | |
Model::Game::PLAYFIELD_WIDTH.times do |column| | |
Glimmer::DataBinding::Observer.proc do |new_color| | |
Glimmer::LibUI.queue_main do | |
color = Glimmer::LibUI.interpret_color(new_color) | |
block = @playfield_blocks[row][column] | |
block[:background_square].fill = color | |
block[:top_bevel_edge].fill = {r: color[:r] + 4*BEVEL_CONSTANT, g: color[:g] + 4*BEVEL_CONSTANT, b: color[:b] + 4*BEVEL_CONSTANT} | |
block[:right_bevel_edge].fill = {r: color[:r] - BEVEL_CONSTANT, g: color[:g] - BEVEL_CONSTANT, b: color[:b] - BEVEL_CONSTANT} | |
block[:bottom_bevel_edge].fill = {r: color[:r] - BEVEL_CONSTANT, g: color[:g] - BEVEL_CONSTANT, b: color[:b] - BEVEL_CONSTANT} | |
block[:left_bevel_edge].fill = {r: color[:r] - BEVEL_CONSTANT, g: color[:g] - BEVEL_CONSTANT, b: color[:b] - BEVEL_CONSTANT} | |
block[:border_square].stroke = new_color == Model::Block::COLOR_CLEAR ? COLOR_GRAY : color | |
end | |
end.observe(@game.playfield[row][column], :color) | |
end | |
end | |
Model::Game::PREVIEW_PLAYFIELD_HEIGHT.times do |row| | |
Model::Game::PREVIEW_PLAYFIELD_WIDTH.times do |column| | |
Glimmer::DataBinding::Observer.proc do |new_color| | |
Glimmer::LibUI.queue_main do | |
color = Glimmer::LibUI.interpret_color(new_color) | |
block = @preview_playfield_blocks[row][column] | |
block[:background_square].fill = color | |
block[:top_bevel_edge].fill = {r: color[:r] + 4*BEVEL_CONSTANT, g: color[:g] + 4*BEVEL_CONSTANT, b: color[:b] + 4*BEVEL_CONSTANT} | |
block[:right_bevel_edge].fill = {r: color[:r] - BEVEL_CONSTANT, g: color[:g] - BEVEL_CONSTANT, b: color[:b] - BEVEL_CONSTANT} | |
block[:bottom_bevel_edge].fill = {r: color[:r] - BEVEL_CONSTANT, g: color[:g] - BEVEL_CONSTANT, b: color[:b] - BEVEL_CONSTANT} | |
block[:left_bevel_edge].fill = {r: color[:r] - BEVEL_CONSTANT, g: color[:g] - BEVEL_CONSTANT, b: color[:b] - BEVEL_CONSTANT} | |
block[:border_square].stroke = new_color == Model::Block::COLOR_CLEAR ? COLOR_GRAY : color | |
end | |
end.observe(@game.preview_playfield[row][column], :color) | |
end | |
end | |
Glimmer::DataBinding::Observer.proc do |new_score| | |
Glimmer::LibUI.queue_main do | |
@score_label.text = new_score.to_s | |
end | |
end.observe(@game, :score) | |
Glimmer::DataBinding::Observer.proc do |new_lines| | |
Glimmer::LibUI.queue_main do | |
@lines_label.text = new_lines.to_s | |
end | |
end.observe(@game, :lines) | |
Glimmer::DataBinding::Observer.proc do |new_level| | |
Glimmer::LibUI.queue_main do | |
@level_label.text = new_level.to_s | |
end | |
end.observe(@game, :level) | |
end | |
def menu_bar | |
menu('Game') { | |
@pause_menu_item = check_menu_item('Pause') { | |
enabled false | |
on_clicked do | |
@game.paused = @pause_menu_item.checked? | |
end | |
} | |
menu_item('Restart') { | |
on_clicked do | |
@game.restart! | |
end | |
} | |
separator_menu_item | |
menu_item('Exit') { | |
on_clicked do | |
exit(0) | |
end | |
} | |
quit_menu_item if OS.mac? | |
} | |
menu('View') { | |
menu_item('Show High Scores') { | |
on_clicked do | |
show_high_scores | |
end | |
} | |
menu_item('Clear High Scores') { | |
on_clicked { | |
@game.clear_high_scores! | |
} | |
} | |
} | |
menu('Options') { | |
radio_menu_item('Instant Down on Up Arrow') { | |
on_clicked do | |
@game.instant_down_on_up = true | |
end | |
} | |
radio_menu_item('Rotate Right on Up Arrow') { | |
on_clicked do | |
@game.rotate_right_on_up = true | |
end | |
} | |
radio_menu_item('Rotate Left on Up Arrow') { | |
on_clicked do | |
@game.rotate_left_on_up = true | |
end | |
} | |
} | |
menu('Help') { | |
if OS.mac? | |
about_menu_item { | |
on_clicked do | |
show_about_dialog | |
end | |
} | |
end | |
menu_item('About') { | |
on_clicked do | |
show_about_dialog | |
end | |
} | |
} | |
end | |
def playfield(playfield_width: , playfield_height: , block_size: , &extra_content) | |
blocks = [] | |
vertical_box { | |
padded false | |
playfield_height.times.map do |row| | |
blocks << [] | |
horizontal_box { | |
padded false | |
playfield_width.times.map do |column| | |
blocks.last << block(row: row, column: column, block_size: block_size) | |
end | |
} | |
end | |
extra_content&.call | |
} | |
blocks | |
end | |
def block(row: , column: , block_size: , &extra_content) | |
block = {} | |
bevel_pixel_size = 0.16 * block_size.to_f | |
color = Glimmer::LibUI.interpret_color(Model::Block::COLOR_CLEAR) | |
area { | |
block[:background_square] = path { | |
square(0, 0, block_size) | |
fill color | |
} | |
block[:top_bevel_edge] = path { | |
polygon(0, 0, block_size, 0, block_size - bevel_pixel_size, bevel_pixel_size, bevel_pixel_size, bevel_pixel_size) | |
fill r: color[:r] + 4*BEVEL_CONSTANT, g: color[:g] + 4*BEVEL_CONSTANT, b: color[:b] + 4*BEVEL_CONSTANT | |
} | |
block[:right_bevel_edge] = path { | |
polygon(block_size, 0, block_size - bevel_pixel_size, bevel_pixel_size, block_size - bevel_pixel_size, block_size - bevel_pixel_size, block_size, block_size) | |
fill r: color[:r] - BEVEL_CONSTANT, g: color[:g] - BEVEL_CONSTANT, b: color[:b] - BEVEL_CONSTANT | |
} | |
block[:bottom_bevel_edge] = path { | |
polygon(block_size, block_size, 0, block_size, bevel_pixel_size, block_size - bevel_pixel_size, block_size - bevel_pixel_size, block_size - bevel_pixel_size) | |
fill r: color[:r] - BEVEL_CONSTANT, g: color[:g] - BEVEL_CONSTANT, b: color[:b] - BEVEL_CONSTANT | |
} | |
block[:left_bevel_edge] = path { | |
polygon(0, 0, 0, block_size, bevel_pixel_size, block_size - bevel_pixel_size, bevel_pixel_size, bevel_pixel_size) | |
fill r: color[:r] - BEVEL_CONSTANT, g: color[:g] - BEVEL_CONSTANT, b: color[:b] - BEVEL_CONSTANT | |
} | |
block[:border_square] = path { | |
square(0, 0, block_size) | |
stroke COLOR_GRAY | |
} | |
on_key_down do |key_event| | |
case key_event | |
in ext_key: :down | |
@game.down! | |
in key: ' ' | |
@game.down!(instant: true) | |
in ext_key: :up | |
case @game.up_arrow_action | |
when :instant_down | |
@game.down!(instant: true) | |
when :rotate_right | |
@game.rotate!(:right) | |
when :rotate_left | |
@game.rotate!(:left) | |
end | |
in ext_key: :left | |
@game.left! | |
in ext_key: :right | |
@game.right! | |
in modifier: :shift | |
@game.rotate!(:right) | |
in modifier: :control | |
@game.rotate!(:left) | |
else | |
# Do Nothing | |
end | |
end | |
extra_content&.call | |
} | |
block | |
end | |
def score_board(block_size: , &extra_content) | |
vertical_box { | |
horizontal_box { | |
label # filler | |
@preview_playfield_blocks = playfield(playfield_width: Model::Game::PREVIEW_PLAYFIELD_WIDTH, playfield_height: Model::Game::PREVIEW_PLAYFIELD_HEIGHT, block_size: block_size) | |
label # filler | |
} | |
horizontal_box { | |
label # filler | |
grid { | |
stretchy false | |
label('Score') { | |
left 0 | |
top 0 | |
halign :fill | |
} | |
@score_label = label { | |
left 0 | |
top 1 | |
halign :center | |
} | |
label('Lines') { | |
left 1 | |
top 0 | |
halign :fill | |
} | |
@lines_label = label { | |
left 1 | |
top 1 | |
halign :center | |
} | |
label('Level') { | |
left 2 | |
top 0 | |
halign :fill | |
} | |
@level_label = label { | |
left 2 | |
top 1 | |
halign :center | |
} | |
} | |
label # filler | |
} | |
extra_content&.call | |
} | |
end | |
def start_moving_tetrominos_down | |
Glimmer::LibUI.timer(@game.delay) do | |
@game.down! if !@game.game_over? && !@game.paused? | |
end | |
end | |
def show_game_over_dialog | |
Glimmer::LibUI.queue_main do | |
msg_box('Game Over!', "Score: #{@game.high_scores.first.score}\nLines: #{@game.high_scores.first.lines}\nLevel: #{@game.high_scores.first.level}") | |
@game.restart! | |
end | |
end | |
def show_high_scores | |
Glimmer::LibUI.queue_main do | |
if @game.high_scores.empty? | |
high_scores_string = "No games have been scored yet." | |
else | |
high_scores_string = @game.high_scores.map do |high_score| | |
"#{high_score.name} | Score: #{high_score.score} | Lines: #{high_score.lines} | Level: #{high_score.level}" | |
end.join("\n") | |
end | |
msg_box('High Scores', high_scores_string) | |
end | |
end | |
def show_about_dialog | |
Glimmer::LibUI.queue_main do | |
msg_box('About', 'Glimmer Tetris - Glimmer DSL for LibUI Example - Copyright (c) 2021 Andy Maleh') | |
end | |
end | |
end | |
Tetris.new.launch |
Happy Glimmering!
No comments:
Post a Comment