rich_engine 0.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/examples/timer.rb ADDED
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rich_engine"
4
+
5
+ class TimerExample < RichEngine::Game
6
+ def on_create
7
+ @timer = RichEngine::Timer.new
8
+ @canvas.bg = "·"
9
+ end
10
+
11
+ def on_update(elapsed_time, key)
12
+ quit! if key == :q
13
+
14
+ @timer.update(elapsed_time)
15
+ @canvas.clear
16
+ @canvas.write_string("Elapsed: #{@timer.get.round(1)}s", x: 1, y: 1)
17
+
18
+ quit! if @timer.get > 10
19
+ end
20
+ end
21
+
22
+ TimerExample.play
@@ -0,0 +1,130 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "string_colors"
4
+
5
+ module RichEngine
6
+ class Canvas
7
+ using StringColors
8
+
9
+ attr_reader :canvas, :bg
10
+
11
+ def initialize(width, height, bg: " ")
12
+ @width = width
13
+ @height = height
14
+ @bg = bg
15
+ clear
16
+ end
17
+
18
+ def dimensions
19
+ [@width, @height]
20
+ end
21
+
22
+ def each_coord(&block)
23
+ (0...@width).each do |x|
24
+ (0...@height).each do |y|
25
+ block.call(x, y)
26
+ end
27
+ end
28
+ end
29
+
30
+ def rows
31
+ @canvas.each_slice(@width)
32
+ end
33
+
34
+ def draw_sprite(sprite, x: 0, y: 0, fg: :white)
35
+ sprite.split("\n").each.with_index do |line, i|
36
+ line.each_char.with_index do |char, j|
37
+ next if char == " "
38
+
39
+ self[x + j, y + i] = char.fg(fg)
40
+ end
41
+ end
42
+ end
43
+
44
+ def write_string(str, x: 0, y: 0, fg: :white, bg: :transparent)
45
+ if x == :center
46
+ x = (@width - str.length) / 2
47
+ end
48
+
49
+ if y == :center
50
+ y = (@height - str.length) / 2
51
+ end
52
+
53
+ fg = Array(fg).cycle
54
+ bg = Array(bg).cycle
55
+
56
+ str.to_s.each_char.with_index do |char, i|
57
+ self[x + i, y] = char.fg(fg.next).bg(bg.next)
58
+ end
59
+ end
60
+
61
+ def draw_rect(x:, y:, width:, height:, char: "█", color: :white)
62
+ x = x.round
63
+ y = y.round
64
+ width = width.round
65
+ height = height.round
66
+
67
+ (x..(x + width - 1)).each do |x_pos|
68
+ (y..(y + height - 1)).each do |y_pos|
69
+ self[x_pos, y_pos] = char.fg(color)
70
+ end
71
+ end
72
+ end
73
+
74
+ def draw_circle(x:, y:, radius:, char: "█", color: :white)
75
+ x = x.round
76
+ y = y.round
77
+
78
+ (x - radius..x + radius).each do |x_pos|
79
+ (y - radius..y + radius).each do |y_pos|
80
+ next if (x_pos - x)**2 + (y_pos - y)**2 > radius**2
81
+
82
+ self[x_pos, y_pos] = char.fg(color)
83
+ end
84
+ end
85
+ end
86
+
87
+ def out_of_bounds?(x, y)
88
+ return true if x < 0
89
+ return true if x >= @width
90
+ return true if y < 0
91
+ return true if y >= @height
92
+
93
+ false
94
+ end
95
+
96
+ def [](x, y)
97
+ x = x.round
98
+ y = y.round
99
+
100
+ @canvas[at(x, y)]
101
+ end
102
+
103
+ def []=(x, y, value)
104
+ x = x.round
105
+ y = y.round
106
+ return if out_of_bounds?(x, y)
107
+
108
+ @canvas[at(x, y)] = value
109
+ end
110
+
111
+ def clear
112
+ @canvas = create_blank_canvas
113
+ end
114
+
115
+ def bg=(bg)
116
+ @bg = bg
117
+ clear
118
+ end
119
+
120
+ private
121
+
122
+ def at(x, y)
123
+ y * @width + x
124
+ end
125
+
126
+ def create_blank_canvas
127
+ (0...(@width * @height)).map { @bg }
128
+ end
129
+ end
130
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RichEngine
4
+ module Chance
5
+ def self.of(value, rand_gen: method(:rand))
6
+ percent = if value > 1
7
+ value / 100.0
8
+ else
9
+ value
10
+ end
11
+
12
+ rand_gen.call < percent
13
+ end
14
+
15
+ def self.of_one_in(value, rand_gen: method(:rand))
16
+ of(1 / value.to_f, rand_gen: rand_gen)
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RichEngine
4
+ class Cooldown
5
+ def initialize(target_time)
6
+ @timer = 0
7
+ @target_time = target_time
8
+ end
9
+
10
+ def update(dt)
11
+ @timer += dt
12
+ end
13
+
14
+ def get
15
+ @timer
16
+ end
17
+
18
+ def reset!
19
+ @timer = 0
20
+ end
21
+
22
+ def finished?
23
+ @timer >= @target_time
24
+ end
25
+ alias_method :ready?, :finished?
26
+ end
27
+ end
@@ -0,0 +1,21 @@
1
+ module RichEngine
2
+ class Enum
3
+ module Mixin
4
+ def self.included(base)
5
+ base.extend ClassMethods
6
+ end
7
+
8
+ module ClassMethods
9
+ def enum(name, enum_options, enum_name: "#{name}s")
10
+ define_singleton_method(enum_name) do
11
+ Enum.new(name, enum_options)
12
+ end
13
+
14
+ define_method(name) do
15
+ self.class.public_send(enum_name).public_send(instance_variable_get("@#{name}"))
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,49 @@
1
+ module RichEngine
2
+ class Enum
3
+ class Value
4
+ include Comparable
5
+
6
+ attr_reader :enum, :selected
7
+
8
+ def initialize(enum:, selected:)
9
+ @enum = enum
10
+ @selected = selected
11
+
12
+ check_selected_is_a_valid_option
13
+ define_query_methods
14
+
15
+ freeze
16
+ end
17
+
18
+ def value
19
+ @enum[@selected]
20
+ end
21
+
22
+ def <=>(other)
23
+ raise ArgumentError, "Can't compare values from different enums" if enum != other.enum
24
+
25
+ value <=> other.value
26
+ end
27
+
28
+ def ==(other)
29
+ return @enum == other.enum && selected == other.selected if other.is_a? self.class
30
+
31
+ super
32
+ end
33
+
34
+ private
35
+
36
+ def check_selected_is_a_valid_option
37
+ msg = "Unknown enum value `#{@selected}`. Options are `#{@enum.options.keys}`"
38
+
39
+ raise(ArgumentError, msg) unless @enum.options.has_key? @selected
40
+ end
41
+
42
+ def define_query_methods
43
+ @enum.options.each do |enum_option, _value|
44
+ define_singleton_method("#{enum_option}?") { enum_option == @selected }
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,29 @@
1
+ require_relative "./enum/value"
2
+ require_relative "./enum/mixin"
3
+
4
+ module RichEngine
5
+ class Enum
6
+ attr_reader :name, :options
7
+
8
+ def initialize(name, options)
9
+ @name = name
10
+ @options = if options.respond_to? :each_pair
11
+ options
12
+ else
13
+ options.map.with_index.to_h
14
+ end
15
+
16
+ @options.each_pair do |option, _value|
17
+ define_singleton_method(option) do
18
+ Enum::Value.new(enum: self, selected: option)
19
+ end
20
+ end
21
+
22
+ freeze
23
+ end
24
+
25
+ def [](option)
26
+ @options.fetch(option)
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,105 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "canvas"
4
+ require_relative "io"
5
+
6
+ module RichEngine
7
+ # Example:
8
+ #
9
+ # class MyGame < RichEngine::Game
10
+ # def on_create
11
+ # @title = "My Awesome Game"
12
+ # end
13
+ #
14
+ # def on_update(elapsed_time, key)
15
+ # quit! if key == :q
16
+ #
17
+ # @canvas.write_string(@title, x: 1, y: 1)
18
+ # end
19
+ # end
20
+ #
21
+ # MyGame.play
22
+ #
23
+ class Game
24
+ class Exit < StandardError; end
25
+
26
+ def initialize(width, height)
27
+ @width = width
28
+ @height = height
29
+ @config = {screen_width: @width, screen_height: @height}
30
+ @io = RichEngine::IO.new(width, height)
31
+ @canvas = RichEngine::Canvas.new(width, height)
32
+ end
33
+
34
+ def self.play(width: 50, height: 10)
35
+ new(width, height).play
36
+ end
37
+
38
+ def play
39
+ prepare_screen
40
+ on_create
41
+
42
+ previous_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
43
+
44
+ loop do
45
+ current_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
46
+ elapsed_time = current_time - previous_time
47
+ previous_time = current_time
48
+
49
+ check_game_exit do
50
+ game_loop(elapsed_time)
51
+ end
52
+ end
53
+
54
+ on_destroy
55
+ ensure
56
+ restore_screen
57
+ end
58
+
59
+ def on_create
60
+ end
61
+
62
+ def on_update(_elapsed_time, _key)
63
+ end
64
+
65
+ def on_destroy
66
+ end
67
+
68
+ def quit!
69
+ raise Exit
70
+ end
71
+
72
+ def game_loop(elapsed_time)
73
+ key = read_input
74
+ on_update(elapsed_time, key)
75
+ render
76
+ end
77
+
78
+ private
79
+
80
+ def prepare_screen
81
+ Terminal.clear
82
+ Terminal.hide_cursor
83
+ Terminal.disable_echo
84
+ end
85
+
86
+ def restore_screen
87
+ Terminal.display_cursor
88
+ Terminal.enable_echo
89
+ end
90
+
91
+ def read_input
92
+ @io.read_async
93
+ end
94
+
95
+ def render(use_caching: true)
96
+ @io.write(@canvas.canvas, use_caching: use_caching)
97
+ end
98
+
99
+ def check_game_exit
100
+ yield
101
+ rescue Exit
102
+ raise StopIteration
103
+ end
104
+ end
105
+ end
@@ -0,0 +1,110 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "timeout"
4
+ require "io/console"
5
+
6
+ module RichEngine
7
+ class IO
8
+ Signal.trap("INT") { raise Game::Exit }
9
+
10
+ def initialize(width, height)
11
+ @screen_width = width
12
+ @screen_height = height
13
+ delete_cache
14
+ end
15
+
16
+ def write(canvas, use_caching:)
17
+ delete_cache unless use_caching
18
+
19
+ with_caching(canvas) do
20
+ Terminal::Cursor.goto(0, 0)
21
+ output = build_output(canvas)
22
+ $stdout.write output
23
+ end
24
+ end
25
+
26
+ def read_async
27
+ key = $stdin.read_nonblock(2)
28
+ _c1, c2 = key.chars
29
+
30
+ if c2 && csi?(key)
31
+ c3, c4 = $stdin.read_nonblock(2).chars
32
+
33
+ if digit?(c3)
34
+ symbolize_key("#{key}#{c3}#{c4}")
35
+ else
36
+ symbolize_key("#{key}#{c3}")
37
+ end
38
+ else
39
+ symbolize_key(key)
40
+ end
41
+ rescue ::IO::WaitReadable
42
+ nil
43
+ end
44
+
45
+ private
46
+
47
+ def delete_cache
48
+ @canvas_cache = nil
49
+ end
50
+
51
+ def with_caching(canvas)
52
+ return :cache_hit if canvas == @canvas_cache
53
+
54
+ yield
55
+ @canvas_cache = canvas
56
+
57
+ :cache_miss
58
+ end
59
+
60
+ def build_output(canvas)
61
+ output = +""
62
+
63
+ i = 0
64
+ while i < canvas_size
65
+ output << "#{canvas[i...(i + @screen_width)].join}\n"
66
+
67
+ i += @screen_width
68
+ end
69
+
70
+ output
71
+ end
72
+
73
+ def canvas_size
74
+ @canvas_size ||= @screen_height * @screen_width
75
+ end
76
+
77
+ def symbolize_key(key)
78
+ return key.downcase.to_sym unless key.start_with?("\e", " ", "\n")
79
+
80
+ case key
81
+ when "\e[A" then :up
82
+ when "\e[B" then :down
83
+ when "\e[C" then :right
84
+ when "\e[D" then :left
85
+ when "\e" then :esc
86
+ when " " then :space
87
+ when "\n" then :enter
88
+ when "\e[2~" then :insert
89
+ when "\e[3~" then :delete
90
+ when "\e[5~" then :pg_up
91
+ when "\e[6~" then :pg_down
92
+ when "\e[H" then :home
93
+ when "\e[F" then :end
94
+ else raise "Unknown key #{key.inspect}" if ENV["DEBUG"] == "all"
95
+ end
96
+ end
97
+
98
+ def escape?(char)
99
+ char == "\e"
100
+ end
101
+
102
+ def csi?(str)
103
+ str == "\e["
104
+ end
105
+
106
+ def digit?(char)
107
+ char.between?("0", "9")
108
+ end
109
+ end
110
+ end
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RichEngine
4
+ class Matrix
5
+ # TODO: implement Enumerable
6
+
7
+ attr_accessor :vec
8
+
9
+ def initialize(width: 1, height: 1, fill_with: nil)
10
+ @vec = Array.new(width) { Array.new(height) { fill_with } }
11
+ end
12
+
13
+ def [](x, y)
14
+ @vec[x][y]
15
+ end
16
+
17
+ def []=(x, y, value)
18
+ @vec[x][y] = value
19
+ end
20
+
21
+ def any?(&block)
22
+ @vec.any? { |row| row.any?(&block) }
23
+ end
24
+
25
+ def each
26
+ @vec.each do |row|
27
+ row.each do |tile|
28
+ yield(tile)
29
+ end
30
+ end
31
+ end
32
+
33
+ def map(&block)
34
+ @vec.map do |row|
35
+ row.map { |value| block.call(value) }
36
+ end
37
+ end
38
+
39
+ def zip(other)
40
+ new_matrix = Matrix.new
41
+ new_matrix.vec = @vec.map.with_index do |row, i|
42
+ row.map.with_index { |value, j| [value, other[i, j]] }
43
+ end
44
+
45
+ new_matrix
46
+ end
47
+
48
+ def each_with_indexes
49
+ @vec.each_with_index do |row, i|
50
+ row.each_with_index do |tile, j|
51
+ yield(tile, i, j)
52
+ end
53
+ end
54
+ end
55
+
56
+ def fill(x:, y:, with:)
57
+ xs = Iterable(x)
58
+ ys = Iterable(y)
59
+
60
+ xs.each do |x|
61
+ ys.each do |y|
62
+ @vec[x][y] = with
63
+ end
64
+ end
65
+ end
66
+
67
+ private
68
+
69
+ def Iterable(value)
70
+ value.respond_to?(:each) ? value : [value]
71
+ end
72
+ end
73
+ end