diff --git a/README.md b/README.md index 09d5951..b3be05c 100644 --- a/README.md +++ b/README.md @@ -16,3 +16,6 @@ as change the background color of the application. ## Triangle Game A simple "Triangle Game" that allows you to move a Roguelike '@' around the window (and off of it). + +## Brick Game +A simple brick break / breakout game. diff --git a/brick/README.md b/brick/README.md new file mode 100644 index 0000000..9df4d81 --- /dev/null +++ b/brick/README.md @@ -0,0 +1,46 @@ + + +# About + +Classic brick game written in Ruby with the awesome [ruby2d](http://www.ruby2d.com) framework. + +![ruby_brick](https://i.ibb.co/VCDbKqX/ezgif-com-video-to-gif.gif) + +> Note: the low frame rate here is due to the GIF recording not the game itself. + +# Install + +Make sure you have installed: + +* [simple2d](https://github.com/simple2d/simple2d): + +``` +brew tap simple2d/tap +brew install simple2d +``` + +* [ruby2d](https://github.com/ruby2d/ruby2d): + +``` +gem install ruby2d +``` + +Then clone the source code to your local: + +``` +git clone https://github.com/mosinski/brick.git +``` + +# Play + +``` +cd brick +ruby brick.rb +``` + +Paddle control: + +* a: left +* d: right + +Original code made by Tiago Guedesn (tiagopog) for Pong game (thanks!) diff --git a/brick/assets/PressStart2P.ttf b/brick/assets/PressStart2P.ttf new file mode 100755 index 0000000..98044e9 Binary files /dev/null and b/brick/assets/PressStart2P.ttf differ diff --git a/brick/brick.rb b/brick/brick.rb new file mode 100644 index 0000000..4448c7e --- /dev/null +++ b/brick/brick.rb @@ -0,0 +1,121 @@ +require 'ruby2d' + +require './lib/ball' +require './lib/paddle' +require './lib/match' + +## +# Window & FPS +## + +set title: 'Brick', + background: 'black', + with: 640, + height: 480, + resizable: false + +fps_display = Text.new(get(:fps).to_i, x: 315, y: 463, size: 12) + +## +# Pad +## + +pad = Paddle.new( + x: get(:width) / 2 - 60 / 2, + y: get(:height) - 15, + width: 60, + height: 10, + speed: 7, + constraints: { x: { min: 0, max: get(:width) - 50 } } +) + +## +# Bricks +## + +bricks = [] + +4.times do |row| + 10.times do |column| + bricks << Paddle.new( + x: get(:width) / 10 * column, + y: 15 * row, + width: 60, + height: 10, + speed: 0, + constraints: {} + ) + end +end + +## +# Ball +## + +ball = Ball.new( + x: pad.x + (pad.width / 2) - 5, + y: 450, + size: 10, + speed: 5 +) + +## +# Pause +## + +pause_display = Text.new( + 'PRESS SPACE', + x: get(:width) / 2 - 110, + y: get(:height) / 2 - 20, + font: 'assets/PressStart2P.ttf', + color: 'gray', + size: 20, + opacity: 1 +) + +## +# Main +## + +match = Match.new + +# Paddle movement +on :key_held do |event| + pad.move(event, left: 'a', right: 'd') + + if match.paused? + ball.x = pad.x + (pad.width / 2) - (ball.width / 2) + end +end + +# Game pause +on :key_down do |event| + if event.key == 'space' + match.paused = false + pause_display.opacity = -1 + end +end + +update do + fps_display.text = get(:fps).to_i + + if match.paused? + next + else + ball.move(window: get(:window), pad: pad, bricks: bricks) + + if ball.scored? + ball.scored.each do |brick| + index = bricks.index(brick) + bricks[index].width = 0 + bricks.delete(brick) + end + elsif ball.failed? + match.paused = !match.paused + pause_display.opacity = 1 + match.restart!(get(:window), ball, pad) + end + end +end + +show diff --git a/brick/lib/ball.rb b/brick/lib/ball.rb new file mode 100644 index 0000000..287ed83 --- /dev/null +++ b/brick/lib/ball.rb @@ -0,0 +1,112 @@ +# Deals with the logic of the brick's ball. +# @author Miłosz Osiński +class Ball < Square + DEFAULTS = { + speed: 3, + direction: { x: 1, y: 1 } + }.freeze + + attr_accessor :speed, :direction, :scored, :scored_at, :failed_at + + # @api public + # @param speed [Integer, Float, nil] ball's speed + # @param direction [Hash, nil] ball's x and y axis directions + # @return [Ball] + def initialize(speed: nil, direction: nil, **args) + super(args) + @speed = speed || DEFAULTS[:speed] + @direction = direction || DEFAULTS[:direction] + end + + # @api public + # @param window [Window] the game's window + # @param pad [Pad] the game's pad + # @param bricks [Array] the game's bricks + # @return [Hash] ball's current position + def move(window:, pad:, bricks:) + self.scored_at = nil + + if edge_collision?(:x, window) + self.direction[:x] *= -1 + elsif edge_collision?(:y, window) + self.failed_at = Time.now if check_edge_collision(:y, window) == :bottom + self.direction[:y] *= -1 + elsif pad_collision?(pad) + self.direction[:y] *= -1 + elsif brick_collision?(bricks).any? + self.scored_at = Time.now + self.scored = brick_collision?(bricks) + self.direction[:y] *= -1 + end + + self.x += direction[:x] * speed + self.y += direction[:y] * speed + + { x: x, y: y } + end + + # @api public + # @return [Boolean] did it score a new point? + def scored? + !scored_at.nil? + end + + # @api public + # @return [Boolean] did it fail? + def failed? + !failed_at.nil? + end + + # @api public + # @param window [Window] the game's window + # @param pad [Pad] the game's pad + # @return [Integer, Float] + def reset_position!(window, pad) + self.x = pad.x + pad.width / 2 - 5 + self.y = 450 + end + + private + + # @api private + # @param axis [Symbol] which axis to check the collision + # @param window [Window] the game's window + # @return [Boolean] + def edge_collision?(axis, window) + !check_edge_collision(axis, window).nil? + end + + # @api private + # @param axis [Symbol] which axis to check the collision + # @param window [Window] the game's window + # @return [Symbol] edge side where ball has collided + def check_edge_collision(axis, window) + if axis == :x && x + width >= window.get(:width) + :right + elsif axis == :x && x <= 0 + :left + elsif axis == :y && y + height >= window.get(:height) + :bottom + elsif axis == :y && y <= 0 + :top + end + end + + # @api private + # @param pad [Pad] the game's pad + # @return [Boolean] has the ball collided with the pad? + def pad_collision?(pad) + y <= pad.y + pad.height && y + height >= pad.y && + [x + width, pad.x + pad.width].min - [x, pad.x].max > 0 + end + + # @api private + # @param bricks [Hash] the game's bricks + # @return [Boolean] has the ball collided with any of the bricks? + def brick_collision?(bricks) + bricks.select do |brick| + y <= brick.y + brick.height && y + height >= brick.y && + [x + width, brick.x + brick.width].min - [x, brick.x].max > 0 + end + end +end diff --git a/brick/lib/match.rb b/brick/lib/match.rb new file mode 100644 index 0000000..78885a9 --- /dev/null +++ b/brick/lib/match.rb @@ -0,0 +1,40 @@ +# Deals with the logic of the brick's match. +# @author Miłosz Osiński +class Match + attr_accessor :paused, :score, :reseted_at + + # @api public + # @return [Match] + def initialize + @score = { left: 0, right: 0 } + @paused = true + end + + # @api public + # @return [Boolaen] + alias paused? paused + + # @api public + # @return [Boolaen] + def wait_to_start? + !reseted_at.nil? + end + + # @api public + # @return [nil] + def check_wait!(current_frame) + self.reseted_at = nil if current_frame - reseted_at >= 60 + end + + # @api public + # @param window [Window] the game's window + # @param ball [Ball] the game's ball + # @param pad [Pad] the game's pad + # @return [Ingeter] + def restart!(window, ball, pad) + ball.scored_at = nil + ball.failed_at = nil + ball.reset_position!(window, pad) + self.reseted_at = window.get(:frames) + end +end diff --git a/brick/lib/paddle.rb b/brick/lib/paddle.rb new file mode 100644 index 0000000..39e138e --- /dev/null +++ b/brick/lib/paddle.rb @@ -0,0 +1,49 @@ +# Deals with the logic of the brick's paddle. +# @author Miłosz Osiński +class Paddle < Rectangle + DEFAULTS = { speed: 3 }.freeze + + attr_accessor :speed, :constraints + + # @api public + # @param speed [Integer, Float, nil] pad's current speed + # @param constraints [Hash] pad's constraints (e.g. max "y") + # @return [Pad] + def initialize(speed: nil, constraints: {}, **args) + super(args) + @speed = speed || DEFAULTS[:speed] + @constraints = constraints + end + + # @api public + # @param event [Ruby2D::Window::KeyEvent] event captured from keyboard + # @param speed [Integer, Float, nil] pad's speed + # @return [Integer, Float] pad's current y position + def move(event, left:, right:) + if move_left?(event, left) + self.x -= speed + elsif move_right?(event, right) + self.x += speed + end + end + + private + + # @api private + # @param event [Ruby2D::Window::KeyEvent] event captured from keyboard + # @param up [String] expected keyboard key for the up movement + # @return [Boolean] is it allowed to move up? + def move_left?(event, left) + min_x = constraints.dig(:x, :min) + event.key == left && (!min_x || x - speed >= min_x) + end + + # @api private + # @param event [Ruby2D::Window::KeyEvent] event captured from keyboard + # @param down [String] expected keyboard key for the down movement + # @return [Boolean] is it allowed to move down? + def move_right?(event, right) + max_x = constraints.dig(:x, :max) + event.key == right && (!max_x || x + speed + height <= max_x) + end +end