Last active
December 12, 2024 21:01
-
-
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
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
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