Skip to content

Instantly share code, notes, and snippets.

@cyang-el
Last active December 12, 2024 21:01
Show Gist options
  • Save cyang-el/a5c87ddf4816f128354aee242ed99366 to your computer and use it in GitHub Desktop.
Save cyang-el/a5c87ddf4816f128354aee242ed99366 to your computer and use it in GitHub Desktop.
a from scrach space invader game in terminal with Ruby https://asciinema.org/a/694571
require 'io/console'
# RC
module RC
# IOLoop
class IOLoop
def initialize(io, game)
@io = io
@handle_game = game.method(:handle)
@update_game = game.method(:update)
@end_condition = game.method(:end_condition)
@end_game = game.method(:end_by_user)
@render_game = game.method(:render)
end
def get_key_in_nonblock(wait_for_secs = 0.01)
@io.getch if
IO.select([@io],
nil,
nil,
wait_for_secs)
end
def ioloop(update_at = 5)
@io.raw do
update_count = 0
loop do
key_in = get_key_in_nonblock
end_game? key_in
handle key_in
if update_count.eql?(update_at)
in_game_update
update_count = 0
end
render
update_count += 1
end
end
end
def in_game_update
@update_game.call
return unless @end_condition.call
puts "oh no ... aliens won...next try will be better :)\r"
exit
end
def end_game?(key_in)
return unless @end_game.call key_in
exit
end
def handle(key_in)
@handle_game.call key_in unless key_in.nil? || key_in.empty?
end
def render
print "\033[2J" # clear screen
print "\033[H" # cursor to the top left
print "\rPress 'q' to exit.\n"
@render_game.call
print "\r"
end
def start
$stdin.echo = false
ioloop
ensure
$stdin.echo = true
end
end
# Game
class Game
attr_accessor :aliens_y, :aliens_x
def initialize(params)
@key_in = ''
@tick = 0
@ship_size = 3
@game_size_v = 25
@game_size_h = 93
@canvas = Array.new(@game_size_v) { ' ' * @game_size_h }
@ship = params.ship
@boulder = params.boulder
@bullet = params.bullet
# init placements
@canvas[-1] = place_ship @ship_x = @game_size_h / 2
@canvas[-3] = place_boulders
@canvas[-4] = place_boulders
@alien_fleet_depth = 5
@alien_x_move_count = 0
@alien_x_move_direct = 1
@alien_motion_switch = true
@aliens_y = 0
@aliens_x = []
# e.g.
# [
# [[1, 2, 3], [6, 7, 8], [11, 12, 13]],
# [[], [], []]
# ]
init_aliens
@bullets = []
end
def init_aliens
@alien_fleet_depth.times do
@aliens_x << [[11, 12, 13], [34, 35, 36], [57, 58, 59], [80, 81, 82]]
# 10...3...20...3...20...3...20...3...10
end
end
def put_aliens(y)
0.upto(@alien_fleet_depth - 1) do |fleet_row_num|
y = @aliens_y + fleet_row_num
fleet_row = @aliens_x[fleet_row_num]
fleet_row.each do |alien|
head_x, body_x, tail_x = alien
if @alien_motion_switch
@canvas[y][head_x] = '>'
@canvas[y][body_x] = '='
@canvas[y][tail_x] = '<'
else
@canvas[y][head_x] = '<'
@canvas[y][body_x] = '='
@canvas[y][tail_x] = '>'
end
end
end
@alien_motion_switch = !@alien_motion_switch
end
def clear_aliens
0.upto(@alien_fleet_depth - 1) do |fleet_row_num|
fleet_row = @aliens_x[fleet_row_num]
fleet_row.each do |alien|
head_x, body_x, tail_x = alien
@canvas[fleet_row_num + @aliens_y][head_x] = ' '
@canvas[fleet_row_num + @aliens_y][body_x] = ' '
@canvas[fleet_row_num + @aliens_y][tail_x] = ' '
end
end
end
def advance_aliens(alien_x_move_upperbound = 10,
alien_x_move_lowerbound = -11)
clear_aliens
if @alien_x_move_count.eql?(alien_x_move_upperbound)
@alien_x_move_direct = -1
@aliens_y += 1
elsif @alien_x_move_count.eql?(alien_x_move_lowerbound)
@alien_x_move_direct = 1
@aliens_y += 1
end
0.upto(@alien_fleet_depth - 1) do |fleet_row_num|
fleet_row = @aliens_x[fleet_row_num]
fleet_row.each do |alien|
alien[0] += @alien_x_move_direct
alien[1] += @alien_x_move_direct
alien[2] += @alien_x_move_direct
end
end
@alien_x_move_count += @alien_x_move_direct
end
def shoot
x__ = @ship_x
y__ = @game_size_v - 2
@bullets << [x__, y__]
end
def draw_bullets
@bullets.each do |x, y|
unless y.zero?
@canvas[y + 1][x] = ' ' unless (y + 1).eql?(@game_size_v - 1)
@canvas[y][x] = y.eql?(1) ? ' ' : @bullet
end
end
end
def clear_hits(hits)
hits.each do |x, y|
(y - 1..@game_size_v - 2).reverse_each do |yyy|
@canvas[yyy][x] = ' '
end
end
end
def advance_bullets
bullets_remain, hits_boulder = collide_boulder
bullets_remain,
bullets_hits_alien,
aliens_hit = hit_aliens(bullets_remain)
# if [email protected]?
# p bullets_remain.inspect
# raise "here"
# end
@bullets = bullets_remain
.filter { |_, y| y.positive? }
.map { |x, y| [x, y - 1] }
clear_hits(hits_boulder + bullets_hits_alien + aliens_hit)
# clear_hits(hits_boulder)
draw_bullets
end
def hit_aliens(bullets)
bullets_remain = []
aliens_remain = @aliens_x.dup
alien_hits = []
hits = []
bullets.each do |x, y|
y__ = y - @aliens_y
if y__.negative? || y__ >= @alien_fleet_depth
bullets_remain << [x, y]
next
end
row_hits = aliens_remain[y__].filter do |alien|
alien.include? x
end
row_hits.each do |alien|
alien.each do |part_x|
alien_hits << [part_x, y]
end
end
if @aliens_x[y__].any? { |alien| alien.include? x }
hits << [x, y]
else
bullets_remain << [x, y]
end
aliens_remain[y__] = aliens_remain[y__].filter do |alien|
!alien.include?(x)
end
end
@aliens_x = aliens_remain
[bullets_remain, hits, alien_hits]
end
def collide_boulder
bullets_remain = []
hits = []
@bullets.each do |x, y|
if @canvas[y - 1][x].eql?(@boulder)
hits << [x, y]
else
bullets_remain << [x, y]
end
end
[bullets_remain, hits]
end
def update
@tick = @tick < 999_999_999_999 ? @tick + 1 : 0
advance_aliens if (@tick % 4).zero?
advance_bullets
put_aliens(@aliens_y)
end
def place_ship(x__)
left_pad = ' ' * x__
right_pad = ' ' * (@game_size_h - @ship_size - x__)
"#{left_pad}#{@ship}#{right_pad}"
end
def place_boulders
' #######' \
' #######' \
' #######' \
' '
end
def position_ship_with_key_in(key_in)
case key_in
when 'left'
@ship_x -= 1 unless @ship_x.zero?
when 'right'
@ship_x += 1 unless @ship_x + 3 == @game_size_h
end
place_ship @ship_x
end
def end_condition
# aliens hitting ship
@aliens_y >= @game_size_v - 5
end
def handle(key_in)
key = translate_key_in(key_in)
if %w[right left].include? key
@canvas[-1] = position_ship_with_key_in(key)
elsif key.eql? 'space'
shoot
end
@key_in = key
end
def end_by_user(key_in)
# TODO:
# print final score
key_in.eql? 'q'
end
def translate_key_in(key_in)
# left "\e[D"
# right "\e[C"
case key_in
when 'D'
'left'
when 'C'
'right'
when ' '
'space'
when 'q'
'end game'
else
'not supported keypress'
end
end
def show_tick
case (@tick % 4).to_i
when 0
'---'
when 1
' \\ '
when 2
' / '
when 3
' | '
end
end
def render
lines = ['Press left and right to move, space to shoot.'] +
[show_tick] +
["in flight bullets: #{@bullets.inspect}"] +
["keypress: #{@key_in}"] +
['=' * (@game_size_h + 2)] +
@canvas.map { |ln| "|#{ln}|" } +
['=' * (@game_size_h + 2)]
print lines.map { |ln| "\r#{ln}\n" }.join
end
end
# params
class Params
attr_reader :bullet, :ship, :boulder
def initialize
@bullet = '*'
@ship = '║^^'
@boulder = '#'
end
end
end
def main
params = RC::Params.new
loop = RC::IOLoop.new($stdin, RC::Game.new(params))
loop.start
end
main if __FILE__ == $PROGRAM_NAME
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment