Skip to content

Instantly share code, notes, and snippets.

@amirrajan
Created April 19, 2026 18:17
Show Gist options
  • Select an option

  • Save amirrajan/129e690a9e858b37f21899f22c4999b0 to your computer and use it in GitHub Desktop.

Select an option

Save amirrajan/129e690a9e858b37f21899f22c4999b0 to your computer and use it in GitHub Desktop.
DragonRuby Game Toolkit - Reference implementation WIP
class Camera
attr :origin,
:viewport_w,
:viewport_h,
:viewport_offset_x,
:viewport_offset_y,
:scale,
:x,
:y,
:target_scale,
:target_x,
:target_y
def initialize(origin:, viewport_w:, viewport_h:, viewport_offset_x: 0, viewport_offset_y: 0,
scale: 1, x: 0, y: 0)
@scale = scale
@x = x
@y = y
@target_scale = scale
@target_x = x
@target_y = y
@origin = origin
@viewport_w = viewport_w
@viewport_h = viewport_h
@viewport_offset_x = viewport_offset_x
@viewport_offset_y = viewport_offset_y
end
def origin_center?
@origin == :center
end
def origin_bottom_left?
@origin == :bottom_left
end
def viewport_h_half
if origin_center?
0
else
@viewport_h.fdiv(2).ceil
end
end
def viewport_w_half
if origin_center?
0
else
@viewport_w.fdiv(2).ceil
end
end
def __to_world_space__ rect
return nil if !rect
x = (rect.x - viewport_w_half + @x * @scale - @viewport_offset_x) / @scale
y = (rect.y - viewport_h_half + @y * @scale - @viewport_offset_y) / @scale
if rect.w
w = rect.w / @scale
h = rect.h / @scale
{ **rect, x: x, y: y, w: w, h: h }
else
{ **rect, x: x, y: y }
end
end
def to_world_space rect
if rect.is_a? Array
rect.map { |r| to_world_space r }
else
__to_world_space__ rect
end
end
def __to_screen_space__ rect
return nil if !rect
x = rect.x * @scale - @x * @scale + viewport_w_half
y = rect.y * @scale - @y * @scale + viewport_h_half
if rect.w
w = rect.w * @scale
h = rect.h * @scale
{ **rect, x: x, y: y, w: w, h: h }
else
{ **rect, x: x, y: y }
end
end
def to_screen_space rect
if rect.is_a? Array
rect.map { |r| to_screen_space r }
else
__to_screen_space__ rect
end
end
def viewport
if origin_center?
{
x: @viewport_offset_x,
y: @viewport_offset_y,
w: @viewport_w,
h: @viewport_h,
anchor_x: 0.5,
anchor_y: 0.5
}
else
{
x: @viewport_offset_x,
y: @viewport_offset_y,
w: @viewport_w,
h: @viewport_h,
}
end
end
def viewport_world
to_world_space viewport
end
def find_all_intersect_viewport os
Geometry.find_all_intersect_rect os
end
def tick
@x = @x.lerp(@target_x, 0.1)
@y = @y.lerp(@target_y, 0.1)
@scale = @scale.lerp(@target_scale, 0.1)
end
end
class Enemy
attr :x, :y, :w, :h, :angle
def initialize(x:, y:, w:, h:)
@x = x
@y = y
@w = w
@h = h
@action = :idle
@action_at = Kernel.tick_count
end
def center
{ x: @x + @w / 2, y: @y + @h / 2 }
end
def frame
Numeric.frame(start_at: @action_at,
count: 6,
hold_for: 4,
repeat: true)
end
def rect
Geometry.rect(x: @x, y: @y, w: @w, h: @h)
end
def center
rect.center
end
def primitives
{
x: @x,
y: @y,
w: @w,
h: @h,
path: "sprites/human/#{frame.frame_index}.png"
}
end
end
class PlayerMenu
attr :player, :buy_miner_rect
def initialize(game:, player:)
@game = game
@player = player
@upgrade_signal_radius_rect = Layout.rect(row: 0, col: 18, w: 2, h: 2)
@buy_miner_rect = Layout.rect(row: 0, col: 20, w: 2, h: 2)
@buy_artillery_rect = Layout.rect(row: 0, col: 22, w: 2, h: 2)
end
def can_mine_mineral_field?
@game.entities.find do |e|
e.type == :mineral_field && Geometry.intersect_rect?(@player.interaction_rect, e)
end
end
def button(button_rect, text, subtext)
[
button_rect.merge(path: :solid,
r: 128,
g: 128,
b: 128),
button_rect.center.merge(text: text,
anchor_x: 0.5,
anchor_y: 0.0,
size_px: 16,
r: 255,
g: 255,
b: 255),
button_rect.center.merge(text: subtext,
anchor_x: 0.5,
anchor_y: 1.0,
size_px: 16,
r: 255,
g: 255,
b: 255)
]
end
def buy_miner_button
button(@buy_miner_rect, "Miner", "$#{@player.miner_cost}")
end
def buy_artillery_button
button(@buy_artillery_rect, "Artillery", "$#{@player.artillery_cost}")
end
def upgrade_signal_radius_button
button(@upgrade_signal_radius_rect, "Boost Signal", "$#{@player.signal_radius_upgrade_cost}")
end
def primitives
[
Layout.rect(row: 0, col: 12, w: 0, h: 0.25)
.merge(text: "Sieze the Means of Production",
anchor_x: 0.5,
anchor_y: -0.5,
size_px: 32,
r: 255,
g: 255,
b: 255),
Layout.rect(row: 0, col: 18, w: 6, h: 0)
.center
.merge(text: "Credits: #{@player.money}",
size_px: 32,
anchor_x: 0.5,
anchor_y: 0,
r: 255,
g: 255,
b: 255),
upgrade_signal_radius_button,
buy_miner_button,
buy_artillery_button,
]
end
def tick inputs
if inputs.mouse.key_up.left
if Geometry.intersect_rect?(inputs.mouse, @buy_miner_rect)
if @player.money >= @player.miner_cost
@player.money -= @player.miner_cost
@player.build_miner!
end
elsif Geometry.intersect_rect?(inputs.mouse, @buy_artillery_rect)
if @player.money >= @player.artillery_cost
@player.money -= @player.artillery_cost
@player.components << Artilery.new(x: @player.x, y: @player.y, w: 8, h: 8, angle: 0, game: @game, slot: @player.components.length)
end
elsif Geometry.intersect_rect?(inputs.mouse, @upgrade_signal_radius_rect)
if @player.money >= @player.signal_radius_upgrade_cost
@player.money -= @player.signal_radius_upgrade_cost
@player.signal_radius_upgrade_count += 1
end
end
end
end
end
class Bot
attr :x, :y, :w, :h, :angle, :action, :action_at
FOLLOW_SPREAD_ANGLES = [0, 25, -25, 50, -50, 75, -75, 89, -89]
FOLLOW_OFFSET_RADII = [60, 80, 80, 45, 45, 100, 100]
def initialize(x:, y:, w:, h:, angle:, game:, slot: 0)
@game = game
@x = x
@y = y
@w = w
@h = h
@angle = angle
@action = :decide
@action_at = Kernel.tick_count
@follow_spread_angle = FOLLOW_SPREAD_ANGLES[slot % FOLLOW_SPREAD_ANGLES.length]
@follow_offset_radius = FOLLOW_OFFSET_RADII[slot % FOLLOW_OFFSET_RADII.length]
@message = nil
@message_at = Kernel.tick_count
@slot = slot
created_messages
message! created_messages[slot % created_messages.length]
end
def created_messages
["Hi!", "Let's go!", "Woo!", "Yay!"]
end
def rect
Geometry.rect(x: @x, y: @y, w: @w, h: @h)
end
def center
rect.center
end
def action! value
return if @action == value
@action = value
@action_at = Kernel.tick_count
end
def player
@game.player
end
def tick
puts "implement tick in subclass: #{self.class}"
end
def message_expired?
return true if !@message
return true if @message_at.elapsed_time > 120
false
end
def message_primitives
return nil if message_expired?
[
{
x: @x + @w / 2,
y: @y + @h + 8,
text: @message,
size_px: 16,
anchor_x: 0.5,
anchor_y: 0,
r: 255, g: 255, b: 255, a: 255
}
]
end
def message! value
return if !message_expired?
@message = value
@message_at = Kernel.tick_count
end
def tick_follow
behind_angle = @game.player.angle + 180 + @follow_spread_angle
target = {
x: @game.player.center.x + behind_angle.to_vector.x * @follow_offset_radius,
y: @game.player.center.y + behind_angle.to_vector.y * @follow_offset_radius
}
dist = Geometry.distance(center, target)
if dist.round > 4
angle_to_target = Geometry.angle(center, target)
delta = Geometry.angle_delta(@angle, angle_to_target)
@angle += delta * 0.1
speed = dist.clamp(0, 48).fdiv(48) * 1.5
@x += @angle.to_vector.x * speed
@y += @angle.to_vector.y * speed
else
action! :decide
end
end
def tick_push
@game.player.components.each do |other|
next if other.equal?(self)
sep_dist = Geometry.distance(center, other.center)
if sep_dist < 16 && sep_dist > 0
@angle += 0.25 * Geometry.angle_delta(@angle, Geometry.angle(center, @game.player.center) + 180)
push_angle = Geometry.angle(other.center, center)
push_strength = (16 - sep_dist) / 16.0
@x += push_angle.to_vector.x * push_strength
@y += push_angle.to_vector.y * push_strength
end
end
player_dist = Geometry.distance(center, @game.player.center)
repulsion_radius = (@game.player.w + @w) / 2 + 8
if player_dist < repulsion_radius && player_dist > 0
@angle += 0.25 * Geometry.angle_delta(@angle, Geometry.angle(center, @game.player.center) + 180)
push_angle = Geometry.angle(@game.player.center, center)
push_strength = (repulsion_radius - player_dist) / repulsion_radius.to_f
@x += push_angle.to_vector.x * push_strength
@y += push_angle.to_vector.y * push_strength
end
end
end
class Artilery < Bot
def bullet_primitives
return nil if !@bullet
{
x: @bullet.x,
y: @bullet.y,
w: @bullet.w,
h: @bullet.h,
path: :solid,
r: 255, g: 128, b: 0,
anchor_x: @bullet.anchor_x,
anchor_y: @bullet.anchor_y,
}
end
def created_messages
["Ready to roll out!", "Artilery online!", "For Aiur!", "Let's get 'em!"]
end
def primitives
[
{
x: @x,
y: @y,
w: @w,
h: @h,
angle: @angle,
path: :solid,
r: 200, g: 100, b: 100
},
message_primitives,
bullet_primitives
]
end
def tick
distance_to_player = Geometry.distance(center, @game.player.center)
if distance_to_player > player.signal_radius && Kernel.tick_count.zmod?(@slot + 1)
@mineral_field = nil
if @action == :attack
selected_message = ["Abort attack!", "Run! Run!", "Tis only a flesh wound!", "Until next time!"][@slot % 4]
message! selected_message
else
selected_message = ["Mom! Come back!", "Mommy!", "I wanna be with you!", "Don't leave me!"][@slot % 4]
message! selected_message
end
action! :follow
end
if @action == :decide
tick_decide
elsif @action == :follow
tick_follow
elsif @action == :attack
tick_attack
end
tick_bullet
tick_push
end
def tick_attack
return if !@enemy
angle_to_target = Geometry.angle(center, @enemy.center)
distance_to_target = Geometry.distance(center, @enemy.center)
$args.outputs.watch "Enemy distance: #{distance_to_target}"
delta = Geometry.angle_delta(@angle, angle_to_target)
@angle += delta * 0.1
if distance_to_target > 64
@x += @angle.to_vector.x * 1.5
@y += @angle.to_vector.y * 1.5
end
if delta.round == 0 && !@bullet
if !player.enemies_within_range.include?(@enemy)
@enemy = nil
selected_message = ["Abort attack!", "Run! Run!", "Tis only a flesh wound!", "Until next time!"].sample
message! selected_message
action! :decide
else
random_tragectory = 45.rand(:sign)
@bullet ||= {
created_at: Kernel.tick_count,
x: center.x,
y: center.y,
w: 16,
h: 16,
dx: (@angle + random_tragectory).to_vector.x * 8,
dy: (@angle + random_tragectory).to_vector.y * 8,
anchor_x: 0.5,
anchor_y: 0.5
}
end
end
end
def tick_bullet
return if !@bullet
@bullet.x += @bullet.dx
@bullet.y += @bullet.dy
angle = Geometry.angle(@bullet, @enemy.center)
@bullet.dx += angle.to_vector.x
@bullet.dy += angle.to_vector.y
@bullet.dx *= 0.9
@bullet.dy *= 0.9
@bullet.w *= 0.95
@bullet.h *= 0.95
@bullet.w = 4 if @bullet.w < 4
@bullet.h = 4 if @bullet.h < 4
if Geometry.intersect_rect?({ x: @bullet.x, y: @bullet.y, w: 1, h: 1 }, @enemy.rect)
@bullet = nil
end
end
def tick_decide
enemy = player.enemies_within_range.sort_by do |e|
Geometry.distance(center, e.center)
end.first
if enemy
@enemy = enemy
action! :attack
selected_message = ["My life for Aiur!", "Take this!", "Engage!"][@slot % 3]
message! selected_message
else
action! :follow
end
end
end
class Miner < Bot
def primitives
[
{
x: @x,
y: @y,
w: @w,
h: @h,
angle: @angle,
path: :solid,
r: 200, g: 200, b: 200
},
message_primitives
]
end
def tick_moving_to_mine
angle_to_mine = Geometry.angle(center, { x: @mineral_field.x + @mineral_field.w / 2, y: @mineral_field.y + @mineral_field.h / 2 })
delta = Geometry.angle_delta(@angle, angle_to_mine)
@angle += delta * 0.1
@x += angle.to_vector.x * 1.5
@y += angle.to_vector.y * 1.5
if Geometry.intersect_rect?(rect, @mineral_field)
action! :mining
end
end
def tick_mining
if @action_at.elapsed_time.zmod?(10)
if !Geometry.intersect_rect?({ x: @x, y: @y, w: @w, h: @h }, @mineral_field)
action! :moving_to_mine
return
end
@mineral_field.quantity -= 1
@game.player.money += 1
@game.player.queue_particle!(x: @mineral_field.x + @mineral_field.w / 2,
y: @mineral_field.y + @mineral_field.h / 2,
r: 209, g: 229, b: 105)
if @mineral_field.quantity <= 0
@mineral_field = nil
action! :decide
end
end
end
def tick_decide
mineral_field = @game.player
.mineral_fields_within_range
.sort_by do |mf|
Geometry.distance(center, Geometry.center(mf))
end
.take(5)
.sample
if mineral_field
@mineral_field = mineral_field
action! :moving_to_mine
selected_message = ["Time to mine!", "Oooo! Shiny!", "Let's goooo!", "Must consume."][@slot % 4]
message! selected_message
else
action! :follow
end
end
def tick
distance_to_player = Geometry.distance(center, @game.player.center)
if distance_to_player > player.signal_radius && Kernel.tick_count.zmod?(@slot + 1)
@mineral_field = nil
action! :follow
selected_message = ["Mom! Come back!", "Mommy!", "I wanna be with you!", "Don't leave me!"][@slot % 4]
message! selected_message
end
if @action == :decide
tick_decide
elsif @action == :follow
tick_follow
elsif @action == :moving_to_mine
tick_moving_to_mine
elsif @action == :mining
tick_mining
end
tick_push
end
end
class Player
attr :x, :y, :w, :h, :angle, :action, :action_at, :particles, :money, :components,
:signal_radius_upgrade_count
def initialize(x:, y:, w:, h:, angle:, game:)
@x = x
@y = y
@w = w
@h = h
@angle = angle
@action = :idle
@signal_radius_upgrade_count = 0
@action_at = Kernel.tick_count
@particles = []
@game = game
@money = 0
@components = []
@components << Miner.new(x: @x, y: @y, w: 8, h: 8, angle: 0, game: @game, slot: @components.length)
@components << Miner.new(x: @x, y: @y, w: 8, h: 8, angle: 0, game: @game, slot: @components.length)
@components << Artilery.new(x: @x, y: @y, w: 8, h: 8, angle: 0, game: @game, slot: @components.length)
end
def signal_radius
128 + @signal_radius_upgrade_count * 32
end
def signal_radius_upgrade_cost
(25 + @signal_radius_upgrade_count * 1.25 * 50).to_i
end
def center
rect.center
end
def rect
Geometry.rect(x: @x, y: @y, w: @w, h: @h)
end
def build_miner!
@components << Miner.new(x: @x, y: @y, w: 8, h: 8, angle: 0, game: @game, slot: @components.length)
end
def action! value
return if @action == value
@action = value
@action_at = Kernel.tick_count
end
def primitives
[
{
**center,
w: signal_radius * 2,
h: signal_radius * 2,
path: "sprites/circle/solid.png",
r: 255, g: 255, b: 255, a: 8,
anchor_x: 0.5, anchor_y: 0.5
},
{
x: @x - @w / 2,
y: @y - @h / 2,
w: @w,
h: @h,
angle: @angle,
path: :solid
},
@particles,
@components.map(&:primitives),
]
end
def mineral_fields_within_range
@game.entities.find_all do |e|
e.type == :mineral_field &&
e.quantity > 0 &&
Geometry.distance(e, center) < signal_radius
end
end
def enemies_within_range
@game.enemies.find_all do |e|
Geometry.distance(e, center) < signal_radius
end
end
def interaction_rect
Geometry.zoom_rect(rect: {
x: (center.x - 16) + @angle.to_vector.x * 16,
y: (center.y - 16) + @angle.to_vector.y * 16,
w: 32,
h: 32,
}, ratio: 0.5)
end
def queue_particle!(x:, y:, r:, g:, b:)
@particles << {
x: x,
y: y,
w: 26, h: 26,
a: 128,
anchor_x: 0.5, anchor_y: 0.5,
dx: (@angle + Numeric.rand(-45..45)).to_vector.x * 20,
dy: (@angle + Numeric.rand(-45..45)).to_vector.y * 20,
path: :solid,
r: r, g: g, b: b
}
end
def tick
if @action == :mining
if @action_at.elapsed_time.zmod?(10)
@mineral_field.quantity -= 2
@money += 2
queue_particle!(x: @mineral_field.x + @mineral_field.w / 2,
y: @mineral_field.y + @mineral_field.h / 2,
r: 238, g: 210, b: 133)
if @mineral_field.quantity <= 0
@mineral_field = nil
action! :idle
end
end
end
@components.each(&:tick)
@particles.each do |particle|
particle.x += particle.dx
particle.y += particle.dy
angle = Geometry.angle(particle, center)
particle.dx += angle.to_vector.x
particle.dy += angle.to_vector.y
particle.dx *= 0.9
particle.dy *= 0.9
particle.w *= 0.95
particle.h *= 0.95
particle.w = 4 if particle.w < 4
particle.h = 4 if particle.h < 4
end
@particles.reject! do |particle|
Geometry.distance(particle, center) < 4
end
end
def mine! mineral_field
@mineral_field = mineral_field
action! :mining
end
def miner_cost
10 + miner_count * 50
end
def artillery_cost
20 + artillery_count * 50
end
def miner_count
@components.count { |c| c.is_a?(Miner) }
end
def artillery_count
@components.count { |c| c.is_a?(Artilery) }
end
end
class Game
attr_gtk
attr :entities, :player_menu, :enemies, :player
def initialize
@player = Player.new(x: 0,
y: 0,
w: 16,
h: 16,
angle: 0,
game: self)
@player_menu = PlayerMenu.new(game: self, player: @player)
@particles = []
@grass_background = {
x: -640,
y: -640,
w: 1280,
h: 1280,
path: :grass_background
}
@entities = load_entities
@enemies = [
Enemy.new(x: 200, y: 200, w: 8, h: 16),
]
@camera = Camera.new origin: :bottom_left,
viewport_w: 620,
viewport_h: 620,
scale: 3.0 - @player.signal_radius.fdiv(512),
x: @player.x,
y: @player.y
end
def mouse_in_world_space
scene_x = inputs.mouse.x - (640 - @camera.viewport_w / 2)
scene_y = inputs.mouse.y - (360 - @camera.viewport_h / 2)
@camera.to_world_space({ x: scene_x, y: scene_y })
end
def mouse_rect_in_world_space_grid_aligned
mouse = mouse_in_world_space
{
x: mouse.x.ifloor(32),
y: mouse.y.ifloor(32),
w: 32,
h: 32
}
end
def load_entities
contents = GTK.read_file("data/entities.txt") || ""
contents.each_line
.map
.with_index do |l, i|
type, x, y, w, h = l.strip.split(",")
type = type.to_sym
x = x.to_i
y = y.to_i
w = w.to_i
h = h.to_i
if type == :mineral_field
{
index: i,
type: :mineral_field,
x: x, y: y,
w: w, h: h,
quantity: 100,
path: "sprites/mineral_fields/#{i % 4}.png"
}
end
end
end
def save_entities entities
contents = entities.map do |e|
"#{e.type},#{e.x},#{e.y},#{e.w},#{e.h}"
end.join("\n")
GTK.write_file("data/entities.txt", contents)
end
def move_player
if inputs.directional_angle
@player.angle += 0.1 * Geometry.angle_delta(@player.angle, inputs.directional_angle)
@player.action! :moving
end
if inputs.directional_vector
@player.x += @player.angle.to_vector.x * 2
@player.y += @player.angle.to_vector.y * 2
@player.action! :moving
end
if !inputs.directional_vector && !inputs.directional_angle
@player.action! :idle if @player.action == :moving
end
# outputs.watch "#{@player.action}"
# outputs.watch "#{@player.money}"
@camera.target_x = @player.center.x
@camera.target_y = @player.center.y
@camera.tick
end
def action_player
if inputs.controller_one.key_down.a || inputs.keyboard.key_down.m
first = @entities.find do |e|
Geometry.intersect_rect?(@player.interaction_rect, e)
end
if first && first.type == :mineral_field
@player.mine! first
end
elsif inputs.keyboard.key_down.b
@player.build_miner!
end
end
def viewport_rect
{
x: 640,
y: 360,
w: @camera.viewport_w,
h: @camera.viewport_h,
anchor_x: 0.5,
anchor_y: 0.5
}
end
def tick_edit_map
if inputs.mouse.key_down.left && Geometry.inside_rect?(inputs.mouse.rect, viewport_rect)
existing_entity = @entities.find do |e|
Geometry.intersect_rect?(mouse_rect_in_world_space_grid_aligned, e)
end
if existing_entity
@entities.reject! { |e| e.equal?(existing_entity) }
save_entities @entities
@entities = load_entities
else
@entities << {
type: :mineral_field,
x: mouse_rect_in_world_space_grid_aligned.x,
y: mouse_rect_in_world_space_grid_aligned.y,
w: 32, h: 32,
quantity: 100,
}
save_entities @entities
@entities = load_entities
end
end
end
def tick
if Kernel.tick_count == 0
outputs[:grass_background].set w: 1280, h: 1280
1280.idiv(32).times do |x|
1280.idiv(32).times do |y|
outputs[:grass_background].primitives << {
x: x * 32, y: y * 32, w: 32, h: 32, path: "sprites/grass/#{Numeric.rand(20)}.png"
}
end
end
end
move_player
action_player
@player.tick
@player_menu.tick(inputs)
tick_edit_map
map_size = 1280
signal_radius = @player.signal_radius * 2.1
desired_signal_radius_px = 640
zoom = desired_signal_radius_px.to_f / signal_radius
@camera.target_scale = zoom
@entities.find_all { |e| e.type == :mineral_field && e.quantity <= 0 }.each do |depleted_field|
depleted_field.path = "sprites/mineral_fields_empty/#{depleted_field.index % 4}.png"
end
render
end
def render
# outputs.watch "#{mouse_in_world_space}"
outputs[:scene].set w: 620, h: 620, background_color: [48, 64, 57]
outputs[:scene].primitives << @camera.to_screen_space(@grass_background)
outputs[:scene].primitives << @camera.to_screen_space(@entities)
outputs[:scene].primitives << @camera.to_screen_space(@player.primitives)
outputs[:scene].primitives << @camera.to_screen_space(@enemies.map(&:primitives))
# outputs[:scene].primitives << @camera.to_screen_space(@player.interaction_rect.merge(path: :solid, r: 255, g: 0, b: 0, a: 128))
# outputs[:scene].primitives << @camera.to_screen_space(mouse_rect_in_world_space_grid_aligned.merge(path: :solid, r: 255, g: 0, b: 0, a: 128))
outputs.background_color = [30, 30, 30]
outputs.primitives << { x: 640,
y: 360,
w: 620,
h: 620,
anchor_x: 0.5,
anchor_y: 0.5,
path: :scene }
outputs.primitives << @player_menu.primitives
# outputs.primitives << Layout.debug_primitives(invert_colors: true, a: 64)
end
end
module Main
attr :game
def boot args
args.state = {}
end
def tick args
@game ||= Game.new
@game.args = args
@game.tick
end
def did_reset args
@game = nil
end
end
# GTK.reset
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment