|
1 | 1 | # RichEngine |
2 | 2 |
|
3 | | -Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/rich_engine`. To experiment with that code, run `bin/console` for an interactive prompt. |
| 3 | +RichEngine is a tiny terminal game engine for Ruby. It gives you a simple game loop, a 2D character canvas with colors, non-blocking keyboard input, and a handful of helpers (timers, cooldowns, RNG, enums, matrices) so you can ship playful ASCII games quickly. |
4 | 4 |
|
5 | | -TODO: Delete this and the text above, and describe your gem |
| 5 | +At its core, you subclass `RichEngine::Game`, implement a few lifecycle hooks, and draw to a `Canvas` each frame. |
6 | 6 |
|
7 | | -## Installation |
| 7 | +## Quick start: build a simple game |
8 | 8 |
|
9 | | -Add this line to your application's Gemfile: |
| 9 | +Below is a minimal, complete example showing how to: |
| 10 | +- create a game by subclassing `RichEngine::Game` |
| 11 | +- quit on a key press |
| 12 | +- sleep to cap the frame rate |
| 13 | +- draw text and shapes to the screen |
10 | 14 |
|
11 | 15 | ```ruby |
12 | | -gem 'rich_engine' |
| 16 | +require "rich_engine" |
| 17 | + |
| 18 | +class MyGame < RichEngine::Game |
| 19 | + using RichEngine::StringColors |
| 20 | + |
| 21 | + TITLE = "Catch the Star" |
| 22 | + |
| 23 | + PLAYER_CHAR = "@" |
| 24 | + PLAYER_COLOR = :yellow |
| 25 | + |
| 26 | + ITEM_COLORS = [:green, :magenta, :cyan] |
| 27 | + ITEM_CHAR = "*" |
| 28 | + |
| 29 | + HUD_HEIGHT = 3 |
| 30 | + |
| 31 | + def on_create |
| 32 | + @score = 0 |
| 33 | + @player_x = 2 |
| 34 | + @player_y = field_height / 2 |
| 35 | + @timer = RichEngine::Cooldown.new(5.0) |
| 36 | + spawn_item |
| 37 | + end |
| 38 | + |
| 39 | + # elapsed_time: seconds since last frame (Float) |
| 40 | + # key: last key pressed (Symbol) or nil |
| 41 | + def on_update(elapsed_time, key) |
| 42 | + quit! if key == :q || key == :esc |
| 43 | + |
| 44 | + # Move player with arrow keys |
| 45 | + case key |
| 46 | + when :left then @player_x -= 1 |
| 47 | + when :right then @player_x += 1 |
| 48 | + when :up then @player_y -= 1 |
| 49 | + when :down then @player_y += 1 |
| 50 | + end |
| 51 | + |
| 52 | + # Keep player inside the game field (above the HUD) |
| 53 | + @player_x = @player_x.clamp(0, @width - 1) |
| 54 | + @player_y = @player_y.clamp(0, field_height - 1) |
| 55 | + |
| 56 | + # Game over if time runs out |
| 57 | + @timer.update(elapsed_time) |
| 58 | + if @timer.finished? |
| 59 | + @game_over = true |
| 60 | + quit! |
| 61 | + end |
| 62 | + |
| 63 | + # Pick up item -> +1 point, respawn, reset timer |
| 64 | + if @player_x == @item_x && @player_y == @item_y |
| 65 | + @score += 1 |
| 66 | + spawn_item |
| 67 | + @timer.reset! |
| 68 | + end |
| 69 | + |
| 70 | + # drawing to the canvas |
| 71 | + @canvas.clear |
| 72 | + @canvas.bg = RichEngine::UI::Textures.solid.bright_white |
| 73 | + |
| 74 | + # Item and player |
| 75 | + @canvas.write_string(ITEM_CHAR, x: @item_x, y: @item_y, fg: @item_color, bg: :gray) |
| 76 | + @canvas.write_string(PLAYER_CHAR, x: @player_x, y: @player_y, fg: PLAYER_COLOR, bg: :gray) |
| 77 | + |
| 78 | + # HUD at the bottom rows |
| 79 | + @canvas.write_string(TITLE.ljust(@width), x: 0, y: hud_y, fg: :bright_cyan) |
| 80 | + @canvas.write_string("Score: #{@score}".ljust(@width), x: 0, y: hud_y + 1, fg: :bright_yellow) |
| 81 | + @canvas.write_string("Time: #{format('%.1f', @timer.get)}s".ljust(@width), x: 0, y: hud_y + 2, fg: :bright_green) |
| 82 | + |
| 83 | + # sleep to cap FPS |
| 84 | + sleep 1 / 60.0 |
| 85 | + end |
| 86 | + |
| 87 | + def on_destroy |
| 88 | + puts(@game_over ? "Game over! Final score: #{@score}" : "Thanks for playing! Score: #{@score}") |
| 89 | + end |
| 90 | + |
| 91 | + private |
| 92 | + |
| 93 | + def hud_y |
| 94 | + @height - HUD_HEIGHT |
| 95 | + end |
| 96 | + |
| 97 | + def field_height |
| 98 | + @height - HUD_HEIGHT |
| 99 | + end |
| 100 | + |
| 101 | + def spawn_item |
| 102 | + # Spawn above the HUD so it won't overlap with on-screen text |
| 103 | + @item_x = rand(@width) |
| 104 | + @item_y = rand(field_height) |
| 105 | + @item_color = ITEM_COLORS.sample |
| 106 | + end |
| 107 | +end |
| 108 | + |
| 109 | +MyGame.play(width: 50, height: 12) |
| 110 | +``` |
| 111 | + |
| 112 | +Notes |
| 113 | +- Hooks: |
| 114 | + - `on_create` runs once at start |
| 115 | + - `on_update(elapsed_time, key)` runs every frame |
| 116 | + - `on_destroy` runs when the game exits. |
| 117 | +- Keys: letters are symbols (e.g., `:q`), plus arrows (`:up`, `:down`, `:left`, `:right`), `:space`, `:enter`, `:esc`, `:pg_up`, `:pg_down`, `:home`, `:end`. |
| 118 | +- Drawing: all drawing happens on `@canvas`. Call `@canvas.clear` each frame if you want to redraw from scratch. |
| 119 | +- Rendering is handled for you. `Game` will flush the canvas to the terminal after each `on_update`. |
| 120 | + |
| 121 | +## Canvas essentials |
| 122 | + |
| 123 | +`@canvas` exposes a few handy methods: |
| 124 | +- `write_string(str, x:, y:, fg: :white, bg: :transparent)` — write colored text; pass a single color or an array (arrays will cycle per character) |
| 125 | +- `draw_rect(x:, y:, width:, height:, char: "█", color: :white)` — draw a filled rectangle |
| 126 | +- `draw_circle(x:, y:, radius:, char: "█", color: :white)` — draw a filled circle |
| 127 | +- `draw_sprite(sprite, x: 0, y: 0, fg: :white)` — draw a multi-line string as a sprite; spaces are transparent |
| 128 | +- `clear` — clear the entire canvas; `bg=` changes the background fill character and clears |
| 129 | + |
| 130 | +Colors are provided via a refinement used internally by the canvas. For text, prefer the `fg:` and `bg:` options on `write_string`. |
| 131 | + |
| 132 | +## Helpers you can use |
| 133 | + |
| 134 | +All helpers live under `RichEngine::...` and are independent utilities you can use inside your game code. |
| 135 | + |
| 136 | +### Timer |
| 137 | + |
| 138 | +- `Timer` accumulates elapsed time; you drive it by calling `update(dt)` with the `elapsed_time` from `on_update`. |
| 139 | +- `Timer.every(seconds:)` returns a small scheduler that fires a block at a fixed interval. |
| 140 | + |
| 141 | +```ruby |
| 142 | +tick = RichEngine::Timer.new |
| 143 | + |
| 144 | +def on_update(dt, _key) |
| 145 | + tick.update(dt) |
| 146 | + if tick.get > 2 |
| 147 | + # do something every ~2 seconds |
| 148 | + tick.reset! |
| 149 | + end |
| 150 | +end |
| 151 | + |
| 152 | +# Fixed interval |
| 153 | +spawn = RichEngine::Timer.every(seconds: 0.5) |
| 154 | +def on_update(dt, _key) |
| 155 | + spawn.update(dt) |
| 156 | + spawn.when_ready { spawn_enemy! } |
| 157 | +end |
13 | 158 | ``` |
14 | 159 |
|
15 | | -And then execute: |
| 160 | +### Cooldown |
16 | 161 |
|
17 | | - $ bundle install |
| 162 | +Track a fixed delay and check if it’s ready. |
18 | 163 |
|
19 | | -Or install it yourself as: |
| 164 | +```ruby |
| 165 | +shoot_cd = RichEngine::Cooldown.new(0.25) # seconds |
| 166 | + |
| 167 | +def on_update(dt, key) |
| 168 | + shoot_cd.update(dt) |
| 169 | + if key == :space && shoot_cd.ready? |
| 170 | + shoot! |
| 171 | + shoot_cd.reset! |
| 172 | + end |
| 173 | +end |
| 174 | +``` |
| 175 | + |
| 176 | +### Chance (random helpers) |
| 177 | + |
| 178 | +```ruby |
| 179 | +RichEngine::Chance.of(0.2) # 20% chance |
| 180 | +RichEngine::Chance.of(20) # also 20% (percent form) |
| 181 | +RichEngine::Chance.of_one_in(10) # 1 in 10 chance |
| 182 | +``` |
| 183 | + |
| 184 | +### Enum and Enum::Mixin |
| 185 | + |
| 186 | +Create ergonomic, comparable enums with query methods. |
| 187 | + |
| 188 | +```ruby |
| 189 | +# Standalone enum |
| 190 | +STATE = RichEngine::Enum.new(:state, {idle: 0, running: 1, paused: 2}) |
| 191 | +STATE.idle.value #=> 0 |
| 192 | +STATE.running > STATE.idle #=> true |
| 193 | + |
| 194 | +# In a class via Mixin |
| 195 | +class Player |
| 196 | + include RichEngine::Enum::Mixin |
| 197 | + enum :state, {idle: 0, running: 1, paused: 2} |
| 198 | + |
| 199 | + def initialize |
| 200 | + @state = :idle |
| 201 | + end |
| 202 | + |
| 203 | + def update |
| 204 | + if state.running? |
| 205 | + # ... |
| 206 | + end |
| 207 | + end |
| 208 | +end |
| 209 | +``` |
| 210 | + |
| 211 | +### Matrix (2D grid) |
| 212 | + |
| 213 | +A simple 2D matrix utility with convenience methods. |
20 | 214 |
|
21 | | - $ gem install rich_engine |
| 215 | +```ruby |
| 216 | +grid = RichEngine::Matrix.new(width: 10, height: 5, fill_with: 0) |
| 217 | +grid[2, 3] = 1 |
| 218 | + |
| 219 | +grid.each { |cell| puts cell } |
22 | 220 |
|
23 | | -## Usage |
| 221 | +# Fill regions |
| 222 | +grid.fill(x: 0..2, y: 0..1, with: 9) |
24 | 223 |
|
25 | | -TODO: Write usage instructions here |
| 224 | +# Zip two matrices into pairs |
| 225 | +other = RichEngine::Matrix.new(width: 10, height: 5, fill_with: :a) |
| 226 | +pairs = grid.zip(other) # => matrix of [left, right] |
| 227 | +``` |
26 | 228 |
|
27 | | -## Development |
| 229 | +### UI::Textures (useful glyphs) |
28 | 230 |
|
29 | | -After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment. |
| 231 | +Convenience characters for shading and blocks: `empty`, `solid`, `light_shade`, `medium_shade`, `dark_shade`, `top_half`, `bottom_half`, `left_half`, `right_half`, `plaid`. |
30 | 232 |
|
31 | | -To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org). |
| 233 | +```ruby |
| 234 | +@canvas.draw_rect(x: 10, y: 6, width: 8, height: 2, char: RichEngine::UI::Textures.solid, color: :magenta) |
| 235 | +``` |
32 | 236 |
|
33 | | -## Contributing |
| 237 | +## Examples |
34 | 238 |
|
35 | | -Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/rich_engine. |
| 239 | +See the `examples/` folder for more complete samples: |
| 240 | +- `timer.rb` — using timers and intervals |
| 241 | +- `noise.rb` — colorful random output |
| 242 | +- `background.rb` — background fill and drawing |
| 243 | +- `grains_of_sand.rb` — simple cellular-like simulation |
36 | 244 |
|
| 245 | +## Install and run locally (optional) |
| 246 | + |
| 247 | +Add to a Gemfile, then bundle: |
| 248 | + |
| 249 | +```ruby |
| 250 | +gem "rich_engine" |
| 251 | +``` |
| 252 | + |
| 253 | +```sh |
| 254 | +bundle install |
| 255 | +``` |
| 256 | + |
| 257 | +Or install directly: |
| 258 | + |
| 259 | +```sh |
| 260 | +gem install rich_engine |
| 261 | +``` |
| 262 | + |
| 263 | +Then run one of the examples: |
| 264 | + |
| 265 | +```sh |
| 266 | +ruby examples/timer.rb |
| 267 | +``` |
37 | 268 |
|
38 | 269 | ## License |
39 | 270 |
|
40 | | -The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT). |
| 271 | +MIT |
0 commit comments