rgame 0.4.0-x86_64-linux-gnu

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.
Files changed (239) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +251 -0
  3. data/LICENSE +26 -0
  4. data/README.md +325 -0
  5. data/docs/api/README.md +224 -0
  6. data/docs/api/app.md +249 -0
  7. data/docs/api/assets.md +463 -0
  8. data/docs/api/audio.md +249 -0
  9. data/docs/api/cli.md +259 -0
  10. data/docs/api/components.md +1112 -0
  11. data/docs/api/drawing.md +398 -0
  12. data/docs/api/examples.md +263 -0
  13. data/docs/api/game.md +219 -0
  14. data/docs/api/images.md +118 -0
  15. data/docs/api/input.md +476 -0
  16. data/docs/api/internals.md +289 -0
  17. data/docs/api/localization.md +285 -0
  18. data/docs/api/scene_graph.md +539 -0
  19. data/docs/api/signals.md +147 -0
  20. data/docs/api/systems.md +231 -0
  21. data/docs/api/text.md +133 -0
  22. data/docs/api/tile_maps.md +237 -0
  23. data/docs/api/toolbox.md +493 -0
  24. data/docs/api/ui.md +927 -0
  25. data/docs/api/values.md +316 -0
  26. data/examples/assets/README.md +322 -0
  27. data/examples/assets/blip.ogg +0 -0
  28. data/examples/assets/glyphs.json +5 -0
  29. data/examples/assets/glyphs.png +0 -0
  30. data/examples/assets/hero.json +12 -0
  31. data/examples/assets/hero.png +0 -0
  32. data/examples/assets/icons.json +13 -0
  33. data/examples/assets/icons.png +0 -0
  34. data/examples/assets/music.ogg +0 -0
  35. data/examples/assets/skills.json +10 -0
  36. data/examples/assets/skills.png +0 -0
  37. data/examples/assets/tileset.png +0 -0
  38. data/examples/assets/tileset.tsx +65 -0
  39. data/examples/assets/town.tmx +26 -0
  40. data/examples/assets/ui.json +11 -0
  41. data/examples/assets/ui.png +0 -0
  42. data/examples/collision/locales/en.yml +8 -0
  43. data/examples/collision/main.rb +316 -0
  44. data/examples/collision_tiles/locales/en.yml +9 -0
  45. data/examples/collision_tiles/main.rb +274 -0
  46. data/examples/fullscreen/locales/en.yml +10 -0
  47. data/examples/fullscreen/main.rb +216 -0
  48. data/examples/game_menu/locales/en.yml +8 -0
  49. data/examples/game_menu/main.rb +170 -0
  50. data/examples/input_glyphs/locales/en.yml +14 -0
  51. data/examples/input_glyphs/main.rb +213 -0
  52. data/examples/jump_topdown/locales/en.yml +9 -0
  53. data/examples/jump_topdown/main.rb +178 -0
  54. data/examples/localization/locales/de.yml +12 -0
  55. data/examples/localization/locales/en.yml +13 -0
  56. data/examples/localization/main.rb +158 -0
  57. data/examples/menu_navigation/locales/en.yml +23 -0
  58. data/examples/menu_navigation/main.rb +365 -0
  59. data/examples/music/locales/en.yml +7 -0
  60. data/examples/music/main.rb +134 -0
  61. data/examples/pathfinding/locales/en.yml +17 -0
  62. data/examples/pathfinding/main.rb +298 -0
  63. data/examples/pooling/locales/en.yml +7 -0
  64. data/examples/pooling/main.rb +259 -0
  65. data/examples/quick_wheel/locales/en.yml +16 -0
  66. data/examples/quick_wheel/main.rb +184 -0
  67. data/examples/radial_menu/locales/en.yml +16 -0
  68. data/examples/radial_menu/main.rb +184 -0
  69. data/examples/save_load/locales/en.yml +11 -0
  70. data/examples/save_load/main.rb +207 -0
  71. data/examples/save_load_ids/locales/en.yml +11 -0
  72. data/examples/save_load_ids/main.rb +322 -0
  73. data/examples/scroll_map/locales/en.yml +4 -0
  74. data/examples/scroll_map/main.rb +140 -0
  75. data/examples/signals/locales/en.yml +6 -0
  76. data/examples/signals/main.rb +278 -0
  77. data/examples/skill_bar/locales/en.yml +14 -0
  78. data/examples/skill_bar/main.rb +159 -0
  79. data/examples/sound/locales/en.yml +6 -0
  80. data/examples/sound/main.rb +122 -0
  81. data/examples/split_screen/locales/en.yml +9 -0
  82. data/examples/split_screen/main.rb +304 -0
  83. data/examples/sprite/locales/en.yml +8 -0
  84. data/examples/sprite/main.rb +180 -0
  85. data/examples/timer/locales/en.yml +12 -0
  86. data/examples/timer/main.rb +273 -0
  87. data/examples/velocity/locales/en.yml +6 -0
  88. data/examples/velocity/main.rb +196 -0
  89. data/examples/walk/locales/en.yml +4 -0
  90. data/examples/walk/main.rb +99 -0
  91. data/exe/rgame +9 -0
  92. data/lib/rgame/boot.rb +3 -0
  93. data/lib/rgame/cli/new_project.rb +139 -0
  94. data/lib/rgame/cli/templates/Gemfile.tt +23 -0
  95. data/lib/rgame/cli/templates/README.md.tt +93 -0
  96. data/lib/rgame/cli/templates/Rakefile.tt +9 -0
  97. data/lib/rgame/cli/templates/assets/locales/en.yml.tt +10 -0
  98. data/lib/rgame/cli/templates/game.rb.tt +23 -0
  99. data/lib/rgame/cli/templates/gitignore.tt +12 -0
  100. data/lib/rgame/cli/templates/main.rb.tt +11 -0
  101. data/lib/rgame/cli/templates/nodes/root.rb.tt +24 -0
  102. data/lib/rgame/cli/templates/rspec.tt +2 -0
  103. data/lib/rgame/cli/templates/rubocop.yml.tt +75 -0
  104. data/lib/rgame/cli/templates/ruby-version.tt +1 -0
  105. data/lib/rgame/cli/templates/spec/locales_spec.rb.tt +18 -0
  106. data/lib/rgame/cli/templates/spec/nodes/root_spec.rb.tt +18 -0
  107. data/lib/rgame/cli/templates/spec/spec_helper.rb.tt +37 -0
  108. data/lib/rgame/cli.rb +66 -0
  109. data/lib/rgame/core/app.rb +46 -0
  110. data/lib/rgame/core/asset_manager.rb +206 -0
  111. data/lib/rgame/core/audio.rb +145 -0
  112. data/lib/rgame/core/font.rb +46 -0
  113. data/lib/rgame/core/gamepad.rb +55 -0
  114. data/lib/rgame/core/image.rb +55 -0
  115. data/lib/rgame/core/input.rb +71 -0
  116. data/lib/rgame/core/locale.rb +22 -0
  117. data/lib/rgame/core/nine_slice.rb +142 -0
  118. data/lib/rgame/core/recording.rb +54 -0
  119. data/lib/rgame/core/renderer.rb +354 -0
  120. data/lib/rgame/core/sprite_sheet.rb +105 -0
  121. data/lib/rgame/core/tile_map_renderer.rb +172 -0
  122. data/lib/rgame/core/ui_atlas.rb +101 -0
  123. data/lib/rgame/core/virtual_gamepad.rb +26 -0
  124. data/lib/rgame/core.rb +18 -0
  125. data/lib/rgame/core_ext.so +0 -0
  126. data/lib/rgame/engine/actor_blockers.rb +131 -0
  127. data/lib/rgame/engine/animation_set.rb +50 -0
  128. data/lib/rgame/engine/animator.rb +44 -0
  129. data/lib/rgame/engine/audio_bus.rb +24 -0
  130. data/lib/rgame/engine/audio_director.rb +59 -0
  131. data/lib/rgame/engine/bounds_blockers.rb +74 -0
  132. data/lib/rgame/engine/camera.rb +78 -0
  133. data/lib/rgame/engine/circle_collider.rb +34 -0
  134. data/lib/rgame/engine/collision_box.rb +59 -0
  135. data/lib/rgame/engine/collision_system.rb +132 -0
  136. data/lib/rgame/engine/component.rb +64 -0
  137. data/lib/rgame/engine/components/action_trigger.rb +40 -0
  138. data/lib/rgame/engine/components/animated_sprite.rb +71 -0
  139. data/lib/rgame/engine/components/box_collider.rb +99 -0
  140. data/lib/rgame/engine/components/camera_follow.rb +45 -0
  141. data/lib/rgame/engine/components/character_body.rb +50 -0
  142. data/lib/rgame/engine/components/circle_collider.rb +80 -0
  143. data/lib/rgame/engine/components/collision_world.rb +231 -0
  144. data/lib/rgame/engine/components/despawn_offscreen.rb +42 -0
  145. data/lib/rgame/engine/components/feet_collider.rb +61 -0
  146. data/lib/rgame/engine/components/hop.rb +76 -0
  147. data/lib/rgame/engine/components/identity.rb +73 -0
  148. data/lib/rgame/engine/components/mover.rb +285 -0
  149. data/lib/rgame/engine/components/navigator.rb +145 -0
  150. data/lib/rgame/engine/components/path_follow.rb +176 -0
  151. data/lib/rgame/engine/components/player_controller.rb +27 -0
  152. data/lib/rgame/engine/components/pool.rb +53 -0
  153. data/lib/rgame/engine/components/screen_wrap.rb +49 -0
  154. data/lib/rgame/engine/components/sprite.rb +47 -0
  155. data/lib/rgame/engine/components/targeting.rb +52 -0
  156. data/lib/rgame/engine/components/thrust_controller.rb +65 -0
  157. data/lib/rgame/engine/components/tile_world.rb +109 -0
  158. data/lib/rgame/engine/components/timer.rb +75 -0
  159. data/lib/rgame/engine/components/velocity.rb +43 -0
  160. data/lib/rgame/engine/components/wander_controller.rb +64 -0
  161. data/lib/rgame/engine/components/world.rb +133 -0
  162. data/lib/rgame/engine/contact_set.rb +74 -0
  163. data/lib/rgame/engine/culling.rb +45 -0
  164. data/lib/rgame/engine/debug_overlay.rb +111 -0
  165. data/lib/rgame/engine/i18n/plural.rb +45 -0
  166. data/lib/rgame/engine/i18n/plural_rules.rb +82 -0
  167. data/lib/rgame/engine/i18n/template.rb +59 -0
  168. data/lib/rgame/engine/i18n.rb +322 -0
  169. data/lib/rgame/engine/input/action_mapper.rb +100 -0
  170. data/lib/rgame/engine/input/actions.rb +92 -0
  171. data/lib/rgame/engine/input/input_map.rb +196 -0
  172. data/lib/rgame/engine/layout.rb +82 -0
  173. data/lib/rgame/engine/nav_grid.rb +87 -0
  174. data/lib/rgame/engine/node2d.rb +555 -0
  175. data/lib/rgame/engine/path.rb +76 -0
  176. data/lib/rgame/engine/player.rb +69 -0
  177. data/lib/rgame/engine/player_layer.rb +70 -0
  178. data/lib/rgame/engine/players.rb +205 -0
  179. data/lib/rgame/engine/pool.rb +51 -0
  180. data/lib/rgame/engine/presentation.rb +171 -0
  181. data/lib/rgame/engine/scene/scene_stack.rb +87 -0
  182. data/lib/rgame/engine/sealed_privates.rb +54 -0
  183. data/lib/rgame/engine/signal.rb +75 -0
  184. data/lib/rgame/engine/spatial_hash.rb +116 -0
  185. data/lib/rgame/engine/text.rb +194 -0
  186. data/lib/rgame/engine/tile_blockers.rb +63 -0
  187. data/lib/rgame/engine/tile_map.rb +147 -0
  188. data/lib/rgame/engine/tile_map_layer.rb +82 -0
  189. data/lib/rgame/engine/tileset.rb +99 -0
  190. data/lib/rgame/engine/timer.rb +51 -0
  191. data/lib/rgame/engine/ui/button.rb +248 -0
  192. data/lib/rgame/engine/ui/column.rb +20 -0
  193. data/lib/rgame/engine/ui/icon_button.rb +93 -0
  194. data/lib/rgame/engine/ui/menu.rb +290 -0
  195. data/lib/rgame/engine/ui/navigation.rb +57 -0
  196. data/lib/rgame/engine/ui/nine_slice_style.rb +50 -0
  197. data/lib/rgame/engine/ui/option_button.rb +163 -0
  198. data/lib/rgame/engine/ui/panel_button.rb +32 -0
  199. data/lib/rgame/engine/ui/panel_menu.rb +36 -0
  200. data/lib/rgame/engine/ui/pointing.rb +146 -0
  201. data/lib/rgame/engine/ui/radial_menu.rb +85 -0
  202. data/lib/rgame/engine/ui/ring.rb +55 -0
  203. data/lib/rgame/engine/ui/row.rb +21 -0
  204. data/lib/rgame/engine/ui/shape_style.rb +102 -0
  205. data/lib/rgame/engine/ui/stack.rb +58 -0
  206. data/lib/rgame/engine/ui/stepping.rb +93 -0
  207. data/lib/rgame/engine/ui/text_button.rb +59 -0
  208. data/lib/rgame/engine/view.rb +76 -0
  209. data/lib/rgame/engine/viewports.rb +171 -0
  210. data/lib/rgame/engine/world_view.rb +71 -0
  211. data/lib/rgame/engine.rb +86 -0
  212. data/lib/rgame/fonts/LiberationSans-Regular.ttf +0 -0
  213. data/lib/rgame/fonts/OFL.txt +102 -0
  214. data/lib/rgame/game.rb +271 -0
  215. data/lib/rgame/rubocop/cop/game/draw_in_local_space.rb +103 -0
  216. data/lib/rgame/rubocop/cop/game/hot_path.rb +36 -0
  217. data/lib/rgame/rubocop/cop/game/layer_boundary.rb +43 -0
  218. data/lib/rgame/rubocop/cop/game/no_core_in_engine_layer.rb +100 -0
  219. data/lib/rgame/rubocop/cop/game/no_engine_in_core_layer.rb +84 -0
  220. data/lib/rgame/rubocop/cop/game/no_interpolation_in_hot_path.rb +50 -0
  221. data/lib/rgame/rubocop/cop/game/no_literal_text.rb +41 -0
  222. data/lib/rgame/rubocop/cop/game/no_needless_allocation.rb +112 -0
  223. data/lib/rgame/rubocop/default.yml +39 -0
  224. data/lib/rgame/rubocop/plugin.rb +45 -0
  225. data/lib/rgame/rubocop.rb +11 -0
  226. data/lib/rgame/util/color.rb +23 -0
  227. data/lib/rgame/util/controls.rb +169 -0
  228. data/lib/rgame/util/route_search.rb +27 -0
  229. data/lib/rgame/util/save_file.rb +107 -0
  230. data/lib/rgame/util/solid_grid.rb +37 -0
  231. data/lib/rgame/util/tensor.rb +3 -0
  232. data/lib/rgame/util/tile_sweep.rb +36 -0
  233. data/lib/rgame/util/z.rb +123 -0
  234. data/lib/rgame/util.rb +10 -0
  235. data/lib/rgame/util_ext.so +0 -0
  236. data/lib/rgame/version.rb +12 -0
  237. data/lib/rgame.rb +5 -0
  238. data/licenses/SDL2/LICENSE.txt +18 -0
  239. metadata +309 -0
@@ -0,0 +1,139 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'erb'
4
+ require 'fileutils'
5
+ require 'rubygems/version'
6
+
7
+ require_relative '../version'
8
+
9
+ module RGame
10
+ module CLI
11
+ # Writes a new game project: `rgame new tictactoe`.
12
+ #
13
+ # The layout it produces is the project's own architecture in miniature, and
14
+ # the generated comments say so at each file:
15
+ #
16
+ # - `game.rb` is the only file that requires `rgame/game`, so it is the
17
+ # only one that loads SDL and OpenGL. It is the game's glue class, the
18
+ # local counterpart of RGame::Game.
19
+ # - `nodes/` requires `rgame` and names only RGame::Engine, so every node
20
+ # runs with no window.
21
+ # - `spec/` therefore loads `rgame` too, and the whole suite runs headless.
22
+ #
23
+ # Getting a newcomer to that shape by default is most of the point: it is
24
+ # the arrangement that keeps game logic spec-able, and it is not one anybody
25
+ # would arrive at by guessing.
26
+ #
27
+ # @api private
28
+ class NewProject
29
+ # Anything the caller did wrong: a bad name, a directory in the way.
30
+ # RGame::CLI turns it into a message and a non-zero status.
31
+ class Error < StandardError; end
32
+
33
+ TEMPLATE_ROOT = File.expand_path('templates', __dir__)
34
+
35
+ DOTFILES = {
36
+ 'gitignore' => '.gitignore',
37
+ 'rspec' => '.rspec',
38
+ 'ruby-version' => '.ruby-version',
39
+ 'rubocop.yml' => '.rubocop.yml'
40
+ }.freeze
41
+
42
+ NAME_PATTERN = /\A[a-z][a-z0-9_-]*\z/i
43
+
44
+ def initialize(name, out: $stdout, root: Dir.pwd)
45
+ @name = name
46
+ @out = out
47
+ @target = File.expand_path(name, root)
48
+
49
+ validate_name!
50
+ end
51
+
52
+ def generate
53
+ check_target!
54
+
55
+ templates.each { |source, destination| write(destination, render(source)) }
56
+
57
+ report_next_steps
58
+ end
59
+
60
+ # `tic_tac_toe` and `tic-tac-toe` both give `TicTacToe`. Used for the
61
+ # window caption and, with `Game` appended, for the game class.
62
+ def caption = @name.split(/[_-]+/).map(&:capitalize).join
63
+
64
+ def game_class = "#{caption}Game"
65
+
66
+ def app_name = @name
67
+
68
+ # "~> 0.2" for a 0.2.0 generator, so a project tracks whatever version
69
+ # created it rather than a number frozen into a template.
70
+ def rgame_requirement = Gem::Version.new(RGame::VERSION).approximate_recommendation
71
+
72
+ # The Ruby running `rgame new`, written to .ruby-version and pointed at
73
+ # from the Gemfile. Whichever Ruby generated the project is the one it was
74
+ # known to work on, which is the only version this can honestly claim.
75
+ #
76
+ # The bare `4.0.5` form rather than mise's `ruby 4.0.5`: every version
77
+ # manager reads it, and so does Bundler's `ruby file:`.
78
+ def ruby_version = RUBY_VERSION
79
+
80
+ private
81
+
82
+ def templates
83
+ Dir.glob('**/*.tt', base: TEMPLATE_ROOT).sort.map do |source|
84
+ [source, destination_for(source)]
85
+ end
86
+ end
87
+
88
+ def destination_for(source)
89
+ dir = File.dirname(source)
90
+ base = File.basename(source, '.tt')
91
+ base = DOTFILES.fetch(base, base)
92
+
93
+ dir == '.' ? base : File.join(dir, base)
94
+ end
95
+
96
+ def render(source)
97
+ erb = ERB.new(File.read(File.join(TEMPLATE_ROOT, source)), trim_mode: '-')
98
+ erb.filename = source
99
+ erb.result(binding)
100
+ end
101
+
102
+ def write(destination, content)
103
+ path = File.join(@target, destination)
104
+
105
+ FileUtils.mkdir_p(File.dirname(path))
106
+ File.write(path, content)
107
+
108
+ @out.puts(" create #{File.join(@name, destination)}")
109
+ end
110
+
111
+ def validate_name!
112
+ return if @name.is_a?(String) && @name.match?(NAME_PATTERN)
113
+
114
+ raise Error, "#{@name.inspect} is not a valid project name — use letters, digits, " \
115
+ 'underscores and dashes, starting with a letter'
116
+ end
117
+
118
+ def check_target!
119
+ return unless File.exist?(@target)
120
+ raise Error, "#{@name} exists and is not a directory" unless File.directory?(@target)
121
+ return if (Dir.children(@target) - ['.', '..']).empty?
122
+
123
+ raise Error, "#{@name} already exists and is not empty"
124
+ end
125
+
126
+ def report_next_steps
127
+ @out.puts(<<~NEXT)
128
+
129
+ Created #{@name}. Next:
130
+
131
+ cd #{@name}
132
+ bundle install
133
+ bundle exec rspec # the game logic, headless — no window needed
134
+ ruby main.rb # the game itself
135
+ NEXT
136
+ end
137
+ end
138
+ end
139
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ source 'https://rubygems.org'
4
+
5
+ # The Ruby this project was generated with, kept in one place so bundler and
6
+ # your version manager cannot disagree about it. Bundler reads .ruby-version as
7
+ # an exact requirement and refuses to install on anything else; for a range,
8
+ # replace this line with something like `ruby '~> <%= ruby_version.split('.').first(2).join('.') %>'`.
9
+ ruby file: '.ruby-version'
10
+
11
+ # The engine. Its one gem dependency is rexml, which Bundler installs with it.
12
+ # SDL2 and OpenGL are system libraries rather than gems, and installing rgame
13
+ # compiles two C extensions, so a C compiler, `make`, `pkg-config` and the
14
+ # SDL2/OpenGL development headers have to be present. See the rgame README.
15
+ gem 'rgame', '<%= rgame_requirement %>'
16
+
17
+ group :development, :test do
18
+ gem 'rake', require: false
19
+ gem 'rspec'
20
+ gem 'rubocop', require: false
21
+ gem 'rubocop-performance', require: false
22
+ gem 'rubocop-rspec', require: false
23
+ end
@@ -0,0 +1,93 @@
1
+ # <%= caption %>
2
+
3
+ A game built with [rgame](https://github.com/psuessenb/rgame).
4
+
5
+ ## Running it
6
+
7
+ ```
8
+ bundle install
9
+ ruby main.rb
10
+ ```
11
+
12
+ `F1` toggles the debug overlay, `F2` closes the window.
13
+
14
+ This project was generated on Ruby <%= ruby_version %>, which is what
15
+ `.ruby-version` holds and what the `Gemfile` points at. Bundler treats that as
16
+ an exact requirement; see the note in the `Gemfile` to loosen it.
17
+
18
+ ## Testing it
19
+
20
+ ```
21
+ bundle exec rspec
22
+ ```
23
+
24
+ The suite runs **headless** — no window, no GPU, no clock. That is not an
25
+ accident of this project being small; it is the arrangement the layout below is
26
+ for, and it holds however large the game gets.
27
+
28
+ ```
29
+ bundle exec rubocop
30
+ bundle exec rake # both of the above
31
+ ```
32
+
33
+ ## Layout
34
+
35
+ ```
36
+ main.rb boots the game and nothing else
37
+ game.rb the window, its size, its caption — and the ONLY file that
38
+ loads SDL and OpenGL
39
+ nodes/ the scene tree: game logic, graphics-free
40
+ root.rb the root node, drawn every frame
41
+ assets/ images, sounds, maps — RGame::Game's media_root
42
+ locales/ translation tables, one file per language
43
+ spec/ specs for nodes/, run with no display
44
+ locales_spec.rb every language has every key en.yml has
45
+ ```
46
+
47
+ ### Why `game.rb` and `nodes/` are separate
48
+
49
+ rgame comes in two halves, and they are worth keeping apart.
50
+
51
+ `RGame::Core` owns the window, the GPU textures and the sound device — real
52
+ operating-system handles. `RGame::Engine` is the scene graph: nodes, components,
53
+ signals, sprites, tile maps. It holds no handles at all, and it never names
54
+ `RGame::Core`. A node is *handed* a renderer at draw time and calls methods on
55
+ it by name.
56
+
57
+ So `require 'rgame'` loads the engine and the value types with no graphics
58
+ library in the process, while `require 'rgame/game'` loads everything. `game.rb`
59
+ does the latter, `nodes/*.rb` and `spec/*` do the former — and that one
60
+ difference is what lets the entire game logic be tested in milliseconds, with a
61
+ spy standing in for the renderer, on a machine with no display.
62
+
63
+ Keep new game logic under `nodes/`. When something there needs to draw, give it
64
+ an `on_draw(renderer, view)` and call the renderer by name.
65
+
66
+ ## Text on screen
67
+
68
+ Text the game draws lives in `assets/locales/en.yml`, not in the code. A node
69
+ builds an `RGame::Engine::Text` from a key once and draws it every frame:
70
+
71
+ ```ruby
72
+ @greeting = RGame::Engine::Text.new('root.greeting') # in initialize
73
+ renderer.text(@greeting, 20, 20) # in on_draw
74
+ ```
75
+
76
+ - **To add text,** add a key to `en.yml`.
77
+ - **To add a language,** add a file beside it, such as `de.yml` starting with
78
+ `de:`, holding the same keys. The game loads every file in `assets/locales/`
79
+ and picks the player's language from their operating system.
80
+ - **`bundle exec rspec` fails** when a language lacks a key `en.yml` has, and
81
+ when a spec draws a key no table has. In the game, a missing key shows the
82
+ English text instead.
83
+
84
+ `spec/spec_helper.rb` loads the tables from `assets/locales/` itself, since specs
85
+ cannot load `game.rb`. Move that directory and update both.
86
+
87
+ The [localization guide](https://github.com/psuessenb/rgame/blob/main/docs/api/localization.md)
88
+ covers plurals, variables and the fallback between languages.
89
+
90
+ ## Where to look next
91
+
92
+ The [rgame API guide](https://github.com/psuessenb/rgame/blob/main/docs/api/README.md)
93
+ covers the scene graph, components, input, drawing, audio and the asset layer.
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rspec/core/rake_task'
4
+ require 'rubocop/rake_task'
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+ RuboCop::RakeTask.new
8
+
9
+ task default: %i[spec rubocop]
@@ -0,0 +1,10 @@
1
+ # The English table: every piece of text the game draws, by key. Rails' format,
2
+ # so the top-level key is the locale and everything below it nests.
3
+ #
4
+ # To add a language, add a file beside this one (de.yml, starting `de:`) with
5
+ # the same keys. RGame::Game loads every .yml in this directory and picks the
6
+ # player's language from their operating system; spec/locales_spec.rb fails
7
+ # while a language lacks a key this file has.
8
+ en:
9
+ root:
10
+ greeting: "Hello from <%= app_name %>!"
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rgame/game'
4
+
5
+ require_relative 'nodes/root'
6
+
7
+ class <%= game_class %> < RGame::Game
8
+ WIDTH = 640
9
+ HEIGHT = 480
10
+
11
+ # The bare `**` forwards every keyword straight through to RGame::Game, so
12
+ # anything it accepts still works here — `players: 2` for split-screen, or
13
+ # `input:` to hand the game a scripted input backend and drive it from a test
14
+ # harness with no hardware attached.
15
+ def initialize(**)
16
+ super(root: Root.new,
17
+ caption: '<%= caption %>',
18
+ width: WIDTH,
19
+ height: HEIGHT,
20
+ media_root: File.join(__dir__, 'assets'),
21
+ **)
22
+ end
23
+ end
@@ -0,0 +1,12 @@
1
+ # Bundler
2
+ /.bundle/
3
+ /vendor/bundle/
4
+
5
+ # Gem packaging, if this project ever becomes one
6
+ /pkg/
7
+ *.gem
8
+
9
+ # Editor / OS
10
+ .DS_Store
11
+ *.swp
12
+ /.ruby-lsp/
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Entry point: `ruby main.rb`.
4
+ #
5
+ # Deliberately nothing but a boot. Everything the game *is* lives in game.rb and
6
+ # nodes/, which is what lets the spec suite load the interesting half without
7
+ # also starting a window.
8
+
9
+ require_relative 'game'
10
+
11
+ <%= game_class %>.new.start
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rgame'
4
+
5
+ # The root of the scene tree. Everything the game draws hangs below it.
6
+ #
7
+ # The three hooks to override are `on_control(actions)`, `on_update(dt)` and
8
+ # `on_draw(renderer, view)`. Override those rather than `control`, `update` or
9
+ # `draw`: the engine does its own bookkeeping in the outer methods and calls
10
+ # these, so there is no `super` to forget.
11
+ #
12
+ # Text on screen is a key into assets/locales/, not a String in the code. A
13
+ # `Text` is built once and read every frame: it looks the key up again only when
14
+ # the language changes, so drawing it costs nothing.
15
+ class Root < RGame::Engine::Node2D
16
+ def initialize
17
+ super
18
+ @greeting = RGame::Engine::Text.new('root.greeting')
19
+ end
20
+
21
+ def on_draw(renderer, _view)
22
+ renderer.text(@greeting, 20, 20)
23
+ end
24
+ end
@@ -0,0 +1,2 @@
1
+ --require spec_helper
2
+ --format documentation
@@ -0,0 +1,75 @@
1
+ # rgame's own cops come with the gem. They catch what a game gets wrong without
2
+ # noticing: a String built every frame, a node drawn at its own position twice,
3
+ # text a translation cannot reach. Each explains itself in its message.
4
+ plugins:
5
+ - rubocop-performance
6
+ - rubocop-rspec
7
+ - rgame/rubocop:
8
+ plugin_class_name: RuboCop::Game::Plugin
9
+
10
+ AllCops:
11
+ NewCops: enable
12
+ TargetRubyVersion: 4.0
13
+
14
+ # This is a game, not a web app: update/draw methods naturally run long, and
15
+ # short coordinate and loop variable names (x, y, dt, id) are idiomatic in the
16
+ # maths they contain. Relax the size and naming cops accordingly.
17
+ Metrics/AbcSize:
18
+ Enabled: false
19
+ Metrics/BlockLength:
20
+ Enabled: false
21
+ Metrics/ClassLength:
22
+ Enabled: false
23
+ Metrics/CyclomaticComplexity:
24
+ Enabled: false
25
+ Metrics/MethodLength:
26
+ Enabled: false
27
+ Metrics/ModuleLength:
28
+ Enabled: false
29
+ Metrics/ParameterLists:
30
+ Enabled: false
31
+ Metrics/PerceivedComplexity:
32
+ Enabled: false
33
+ Naming/MethodParameterName:
34
+ Enabled: false
35
+
36
+ # Maths-heavy code reads fine on standard operator precedence
37
+ # (row * cols + col, x + w / 2); requiring parentheses everywhere is just noise.
38
+ Lint/AmbiguousOperatorPrecedence:
39
+ Enabled: false
40
+
41
+ # nodes/ and spec/ run with no window, and only game.rb loads one. This keeps it
42
+ # so: naming RGame::Core, or requiring rgame/core or rgame/game, fails the lint
43
+ # rather than the spec run on a machine without a display.
44
+ Game/NoCoreInEngineLayer:
45
+ Enabled: true
46
+ Include:
47
+ - 'nodes/**/*.rb'
48
+ - 'spec/**/*.rb'
49
+
50
+ Style/Documentation:
51
+ Enabled: false
52
+
53
+ # Integration-flavoured specs in a game naturally run longer than five lines and
54
+ # assert more than one thing about the frame they drove.
55
+ RSpec/ExampleLength:
56
+ Enabled: false
57
+ RSpec/MultipleExpectations:
58
+ Enabled: false
59
+
60
+ # A node is handed a renderer and calls it by name — it never learns what class
61
+ # answered, which is exactly what lets the spec suite run with no window.
62
+ # Verifying a double against RGame::Core::Renderer would mean loading SDL and
63
+ # OpenGL into the spec process, so these doubles are deliberately unverified.
64
+ RSpec/VerifiedDoubles:
65
+ Enabled: false
66
+
67
+ # Spec paths mirror the source tree, and a class with a trailing dimension maps
68
+ # to node2d_spec.rb rather than the cop's default node2_d_spec.rb.
69
+ RSpec/SpecFilePathFormat:
70
+ CustomTransform:
71
+ Node2D: node2d
72
+ # It checks the tables in assets/locales/, and describes I18n only because
73
+ # that is what it asks, so there is no source file for its path to mirror.
74
+ Exclude:
75
+ - spec/locales_spec.rb
@@ -0,0 +1 @@
1
+ <%= ruby_version %>
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ # The translation tables in assets/locales/, checked against each other. A key
4
+ # added to en.yml and forgotten in another language fails here, rather than
5
+ # showing a player English text in the middle of their own language.
6
+ RSpec.describe RGame::Engine::I18n do
7
+ # Without this, deleting en.yml would leave nothing to compare against, and
8
+ # the example below would pass for every language.
9
+ it 'has a table for the default locale' do
10
+ expect(described_class.available).to include(described_class.default)
11
+ end
12
+
13
+ it 'gives every locale every key the default locale has' do
14
+ missing = described_class.available.to_h { |locale| [locale, described_class.missing_keys(locale)] }
15
+
16
+ expect(missing.reject { |_locale, keys| keys.empty? }).to be_empty
17
+ end
18
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ # What a node spec looks like: build the node, drive one of its hooks, assert.
4
+ # No window is opened, and none is needed — see spec/spec_helper.rb for why.
5
+ RSpec.describe Root do
6
+ describe '#on_draw' do
7
+ it 'draws its greeting from the English table' do
8
+ # A node calls a renderer by name and never asks what class it is, so a
9
+ # plain spy stands in for one. The real RGame::Core::Renderer lives on the
10
+ # other side of a line this suite deliberately does not cross.
11
+ renderer = spy('renderer')
12
+
13
+ described_class.new.on_draw(renderer, nil)
14
+
15
+ expect(renderer).to have_received(:text).with('Hello from <%= app_name %>!', 20, 20)
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ # `rgame` and never `rgame/game`. It loads RGame::Util and RGame::Engine —
4
+ # everything that runs without a window — so no SDL and no OpenGL enter this
5
+ # process, and the whole suite runs with no display: in CI, over SSH, anywhere.
6
+ #
7
+ # It also means `RGame::Core` is an undefined constant here. A spec that reached
8
+ # for it would fail loudly rather than quietly opening a window, which is the
9
+ # point: game logic that can only be tested with a GPU attached is game logic
10
+ # that stops being tested.
11
+ require 'rgame'
12
+
13
+ # The project root, so `require 'nodes/root'` works from anywhere in the suite.
14
+ $LOAD_PATH.unshift File.expand_path('..', __dir__)
15
+
16
+ Dir[File.expand_path('../nodes/**/*.rb', __dir__)].each { |file| require file }
17
+
18
+ # The translation tables, read from disk once. This is the directory RGame::Game
19
+ # loads in the game: `locales` under game.rb's `media_root`. Move one and move
20
+ # the other, or every key a spec draws raises.
21
+ LOCALES = Dir[File.expand_path('../assets/locales/**/*.yml', __dir__)]
22
+ .to_h { |path| [path, File.read(path)] }.freeze
23
+
24
+ RSpec.configure do |config|
25
+ config.expect_with(:rspec) { |c| c.syntax = :expect }
26
+ config.disable_monkey_patching!
27
+
28
+ # RGame::Engine::I18n is global, so every example starts from the game's own
29
+ # tables in the default language, whatever the example before it loaded or
30
+ # switched to. A key no table has raises instead of drawing itself: found by
31
+ # `rake` rather than by a player.
32
+ config.before do
33
+ RGame::Engine::I18n.reset
34
+ LOCALES.each { |path, yaml| RGame::Engine::I18n.load(yaml, source: path) }
35
+ RGame::Engine::I18n.missing = :raise
36
+ end
37
+ end
data/lib/rgame/cli.rb ADDED
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'version'
4
+ require_relative 'cli/new_project'
5
+
6
+ module RGame
7
+ # Argument parsing and dispatch. The work is in RGame::CLI::NewProject.
8
+ module CLI
9
+ USAGE = <<~USAGE
10
+ Usage: rgame COMMAND [ARGS]
11
+
12
+ Commands:
13
+ new NAME Create a new game project in the directory NAME
14
+ version Print the rgame version
15
+ help Print this message
16
+
17
+ Example:
18
+ rgame new tictactoe
19
+ USAGE
20
+
21
+ # Runs one command and returns the process exit status, rather than calling
22
+ # `exit` itself — which is what lets a spec drive it in-process and assert on
23
+ # what it wrote and what it printed.
24
+ #
25
+ # `out` and `err` are injectable for the same reason.
26
+ def self.run(argv, out: $stdout, err: $stderr)
27
+ command, *rest = argv
28
+
29
+ case command
30
+ when 'new' then new_project(rest, out: out, err: err)
31
+ when 'version', '--version', '-v' then version(out: out)
32
+ when 'help', '--help', '-h', nil then help(out: out)
33
+ else
34
+ err.puts("rgame: unknown command #{command.inspect}", '', USAGE)
35
+ 1
36
+ end
37
+ end
38
+
39
+ def self.help(out:)
40
+ out.puts(USAGE)
41
+ 0
42
+ end
43
+
44
+ def self.version(out:)
45
+ out.puts("rgame #{RGame::VERSION}")
46
+ 0
47
+ end
48
+
49
+ def self.new_project(args, out:, err:)
50
+ name = args.first
51
+
52
+ if name.nil? || name.start_with?('-')
53
+ err.puts('rgame new: expected a project name', '', USAGE)
54
+ return 1
55
+ end
56
+
57
+ NewProject.new(name, out: out).generate
58
+ 0
59
+ rescue NewProject::Error => e
60
+ err.puts("rgame new: #{e.message}")
61
+ 1
62
+ end
63
+
64
+ private_class_method :help, :version, :new_project
65
+ end
66
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rgame/core_ext'
4
+
5
+ module RGame
6
+ module Core
7
+ # The two things a game needs one of, owned by the app that needs them and
8
+ # built on first use.
9
+ #
10
+ # Lazily, and that is the point on both counts: an app that draws only
11
+ # primitives builds no asset manager, and one that never plays a sound never
12
+ # opens a device — which is also the right moment to open it, since asking
13
+ # for a sound is the first thing that needs one.
14
+ class App
15
+ # This app's asset manager, rooted at the `media_root:` it was made with.
16
+ #
17
+ # class MyGame < RGame::Core::App
18
+ # def initialize = super(width: 640, height: 480, caption: 'demo', media_root: MEDIA)
19
+ # end
20
+ #
21
+ # app.assets.image('space.png')
22
+ #
23
+ # A game never constructs one. An `Image` belongs to one GL context and
24
+ # has to be told which, so *something* has to hold the app — and since the
25
+ # asset manager is the only thing in the engine that loads from a path,
26
+ # that something is here, once, rather than threaded through every
27
+ # constructor that ends up owning an image.
28
+ def assets = @assets ||= AssetManager.new(root: @media_root, app: self)
29
+
30
+ # This app's sound device. Not tied to the window in any way — audio has
31
+ # no GL context and survives one being recreated — it lives here because
32
+ # a game wants exactly one, the same way it wants one asset manager.
33
+ # It is handed this app's asset manager, so `audio.play_sound('hurt.ogg')`
34
+ # resolves a path the same way `renderer.sprite('hero.json', …)` does.
35
+ # Building the manager does not touch audio — its loaders are lambdas,
36
+ # called at load time — so there is no cycle here despite the manager
37
+ # reaching back through `app.audio` to make a sound.
38
+ def audio = @audio ||= Audio.new(assets: assets)
39
+
40
+ # Where #assets resolves relative paths from. Set once, as a keyword to
41
+ # the constructor; there is deliberately no writer, because changing it
42
+ # after an asset has loaded would leave a cache keyed against two roots.
43
+ attr_reader :media_root
44
+ end
45
+ end
46
+ end