rgame 0.2.0 → 0.3.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.
Files changed (253) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +135 -1
  3. data/README.md +67 -65
  4. data/docs/api/README.md +86 -82
  5. data/docs/api/app.md +125 -68
  6. data/docs/api/assets.md +203 -165
  7. data/docs/api/audio.md +130 -89
  8. data/docs/api/cli.md +259 -0
  9. data/docs/api/components.md +1032 -265
  10. data/docs/api/drawing.md +160 -160
  11. data/docs/api/examples.md +263 -0
  12. data/docs/api/game.md +149 -50
  13. data/docs/api/images.md +39 -39
  14. data/docs/api/input.md +226 -148
  15. data/docs/api/internals.md +241 -62
  16. data/docs/api/localization.md +285 -0
  17. data/docs/api/scene_graph.md +397 -244
  18. data/docs/api/signals.md +81 -76
  19. data/docs/api/systems.md +186 -73
  20. data/docs/api/text.md +58 -41
  21. data/docs/api/tile_maps.md +237 -0
  22. data/docs/api/toolbox.md +395 -146
  23. data/docs/api/ui.md +883 -54
  24. data/docs/api/values.md +216 -33
  25. data/examples/assets/README.md +322 -0
  26. data/examples/assets/blip.ogg +0 -0
  27. data/examples/assets/glyphs.json +5 -0
  28. data/examples/assets/glyphs.png +0 -0
  29. data/examples/assets/hero.json +12 -0
  30. data/examples/assets/hero.png +0 -0
  31. data/examples/assets/icons.json +13 -0
  32. data/examples/assets/icons.png +0 -0
  33. data/examples/assets/music.ogg +0 -0
  34. data/examples/assets/skills.json +10 -0
  35. data/examples/assets/skills.png +0 -0
  36. data/examples/assets/tileset.png +0 -0
  37. data/examples/assets/tileset.tsx +65 -0
  38. data/examples/assets/town.tmx +26 -0
  39. data/examples/assets/ui.json +11 -0
  40. data/examples/assets/ui.png +0 -0
  41. data/examples/collision/locales/en.yml +8 -0
  42. data/examples/collision/main.rb +316 -0
  43. data/examples/collision_tiles/locales/en.yml +9 -0
  44. data/examples/collision_tiles/main.rb +274 -0
  45. data/examples/fullscreen/locales/en.yml +10 -0
  46. data/examples/fullscreen/main.rb +216 -0
  47. data/examples/game_menu/locales/en.yml +8 -0
  48. data/examples/game_menu/main.rb +170 -0
  49. data/examples/input_glyphs/locales/en.yml +14 -0
  50. data/examples/input_glyphs/main.rb +213 -0
  51. data/examples/jump_topdown/locales/en.yml +9 -0
  52. data/examples/jump_topdown/main.rb +178 -0
  53. data/examples/localization/locales/de.yml +12 -0
  54. data/examples/localization/locales/en.yml +13 -0
  55. data/examples/localization/main.rb +158 -0
  56. data/examples/menu_navigation/locales/en.yml +23 -0
  57. data/examples/menu_navigation/main.rb +365 -0
  58. data/examples/music/locales/en.yml +7 -0
  59. data/examples/music/main.rb +134 -0
  60. data/examples/pathfinding/locales/en.yml +17 -0
  61. data/examples/pathfinding/main.rb +298 -0
  62. data/examples/pooling/locales/en.yml +7 -0
  63. data/examples/pooling/main.rb +259 -0
  64. data/examples/quick_wheel/locales/en.yml +16 -0
  65. data/examples/quick_wheel/main.rb +184 -0
  66. data/examples/radial_menu/locales/en.yml +16 -0
  67. data/examples/radial_menu/main.rb +184 -0
  68. data/examples/save_load/locales/en.yml +11 -0
  69. data/examples/save_load/main.rb +207 -0
  70. data/examples/save_load_ids/locales/en.yml +11 -0
  71. data/examples/save_load_ids/main.rb +322 -0
  72. data/examples/scroll_map/locales/en.yml +4 -0
  73. data/examples/scroll_map/main.rb +140 -0
  74. data/examples/signals/locales/en.yml +6 -0
  75. data/examples/signals/main.rb +278 -0
  76. data/examples/skill_bar/locales/en.yml +14 -0
  77. data/examples/skill_bar/main.rb +159 -0
  78. data/examples/sound/locales/en.yml +6 -0
  79. data/examples/sound/main.rb +122 -0
  80. data/examples/split_screen/locales/en.yml +9 -0
  81. data/examples/split_screen/main.rb +304 -0
  82. data/examples/sprite/locales/en.yml +8 -0
  83. data/examples/sprite/main.rb +180 -0
  84. data/examples/timer/locales/en.yml +12 -0
  85. data/examples/timer/main.rb +273 -0
  86. data/examples/velocity/locales/en.yml +6 -0
  87. data/examples/velocity/main.rb +196 -0
  88. data/examples/walk/locales/en.yml +4 -0
  89. data/examples/walk/main.rb +99 -0
  90. data/exe/rgame +9 -0
  91. data/ext/rgame_core/app/app.c +33 -3
  92. data/ext/rgame_core/app/locale.c +67 -0
  93. data/ext/rgame_core/app/locale.h +28 -0
  94. data/ext/rgame_core/audio/audio.c +39 -2
  95. data/ext/rgame_core/example.rb +0 -49
  96. data/ext/rgame_core/extconf.rb +0 -125
  97. data/ext/rgame_core/include/rgame/core.h +38 -2
  98. data/ext/rgame_core/ruby/audio_ext.c +10 -5
  99. data/ext/rgame_core/ruby/core_ext.c +30 -7
  100. data/ext/rgame_core/ruby/core_ext.h +3 -0
  101. data/ext/rgame_core/ruby/locale_ext.c +44 -0
  102. data/ext/rgame_core/ruby/recording_ext.c +1 -1
  103. data/ext/rgame_core/ruby/renderer_ext.c +20 -20
  104. data/ext/rgame_util/extconf.rb +2 -20
  105. data/ext/rgame_util/route_search.c +305 -0
  106. data/ext/rgame_util/route_search.h +86 -0
  107. data/ext/rgame_util/route_search_ext.c +150 -0
  108. data/ext/rgame_util/solid_grid.c +58 -0
  109. data/ext/rgame_util/solid_grid.h +49 -0
  110. data/ext/rgame_util/solid_grid_ext.c +161 -0
  111. data/ext/rgame_util/tile_sweep.c +164 -0
  112. data/ext/rgame_util/tile_sweep.h +62 -0
  113. data/ext/rgame_util/tile_sweep_ext.c +155 -0
  114. data/ext/rgame_util/util_ext.c +3 -0
  115. data/ext/rgame_util/util_ext.h +15 -0
  116. data/lib/rgame/boot.rb +0 -10
  117. data/lib/rgame/cli/new_project.rb +139 -0
  118. data/lib/rgame/cli/templates/Gemfile.tt +23 -0
  119. data/lib/rgame/cli/templates/README.md.tt +93 -0
  120. data/lib/rgame/cli/templates/Rakefile.tt +9 -0
  121. data/lib/rgame/cli/templates/assets/locales/en.yml.tt +10 -0
  122. data/lib/rgame/cli/templates/game.rb.tt +23 -0
  123. data/lib/rgame/cli/templates/gitignore.tt +12 -0
  124. data/lib/rgame/cli/templates/main.rb.tt +11 -0
  125. data/lib/rgame/cli/templates/nodes/root.rb.tt +24 -0
  126. data/lib/rgame/cli/templates/rspec.tt +2 -0
  127. data/lib/rgame/cli/templates/rubocop.yml.tt +75 -0
  128. data/lib/rgame/cli/templates/ruby-version.tt +1 -0
  129. data/lib/rgame/cli/templates/spec/locales_spec.rb.tt +18 -0
  130. data/lib/rgame/cli/templates/spec/nodes/root_spec.rb.tt +18 -0
  131. data/lib/rgame/cli/templates/spec/spec_helper.rb.tt +37 -0
  132. data/lib/rgame/cli.rb +66 -0
  133. data/lib/rgame/core/app.rb +6 -44
  134. data/lib/rgame/core/asset_manager.rb +13 -31
  135. data/lib/rgame/core/audio.rb +37 -16
  136. data/lib/rgame/core/font.rb +0 -3
  137. data/lib/rgame/core/locale.rb +22 -0
  138. data/lib/rgame/core/nine_slice.rb +0 -21
  139. data/lib/rgame/core/renderer.rb +6 -63
  140. data/lib/rgame/core/sprite_sheet.rb +0 -3
  141. data/lib/rgame/core/tile_map_renderer.rb +2 -19
  142. data/lib/rgame/core/ui_atlas.rb +28 -13
  143. data/lib/rgame/core.rb +1 -8
  144. data/lib/rgame/engine/actor_blockers.rb +131 -0
  145. data/lib/rgame/engine/animation_set.rb +1 -0
  146. data/lib/rgame/engine/audio_director.rb +36 -6
  147. data/lib/rgame/engine/bounds_blockers.rb +74 -0
  148. data/lib/rgame/engine/camera.rb +3 -3
  149. data/lib/rgame/engine/circle_collider.rb +4 -2
  150. data/lib/rgame/engine/collision_box.rb +26 -1
  151. data/lib/rgame/engine/collision_system.rb +110 -22
  152. data/lib/rgame/engine/component.rb +34 -10
  153. data/lib/rgame/engine/components/action_trigger.rb +0 -1
  154. data/lib/rgame/engine/components/animated_sprite.rb +27 -25
  155. data/lib/rgame/engine/components/box_collider.rb +99 -0
  156. data/lib/rgame/engine/components/camera_follow.rb +6 -5
  157. data/lib/rgame/engine/components/character_body.rb +20 -61
  158. data/lib/rgame/engine/components/circle_collider.rb +47 -11
  159. data/lib/rgame/engine/components/collision_world.rb +159 -31
  160. data/lib/rgame/engine/components/despawn_offscreen.rb +24 -8
  161. data/lib/rgame/engine/components/feet_collider.rb +61 -0
  162. data/lib/rgame/engine/components/hop.rb +76 -0
  163. data/lib/rgame/engine/components/identity.rb +73 -0
  164. data/lib/rgame/engine/components/mover.rb +285 -0
  165. data/lib/rgame/engine/components/navigator.rb +145 -0
  166. data/lib/rgame/engine/components/path_follow.rb +123 -31
  167. data/lib/rgame/engine/components/player_controller.rb +5 -2
  168. data/lib/rgame/engine/components/pool.rb +1 -1
  169. data/lib/rgame/engine/components/screen_wrap.rb +33 -11
  170. data/lib/rgame/engine/components/sprite.rb +12 -6
  171. data/lib/rgame/engine/components/targeting.rb +9 -11
  172. data/lib/rgame/engine/components/thrust_controller.rb +1 -1
  173. data/lib/rgame/engine/components/tile_world.rb +41 -13
  174. data/lib/rgame/engine/components/timer.rb +1 -1
  175. data/lib/rgame/engine/components/velocity.rb +23 -7
  176. data/lib/rgame/engine/components/wander_controller.rb +6 -2
  177. data/lib/rgame/engine/components/world.rb +133 -0
  178. data/lib/rgame/engine/contact_set.rb +74 -0
  179. data/lib/rgame/engine/culling.rb +4 -6
  180. data/lib/rgame/engine/debug_overlay.rb +5 -11
  181. data/lib/rgame/engine/i18n/plural.rb +45 -0
  182. data/lib/rgame/engine/i18n/plural_rules.rb +82 -0
  183. data/lib/rgame/engine/i18n/template.rb +59 -0
  184. data/lib/rgame/engine/i18n.rb +276 -51
  185. data/lib/rgame/engine/input/action_mapper.rb +0 -26
  186. data/lib/rgame/engine/input/actions.rb +2 -8
  187. data/lib/rgame/engine/input/input_map.rb +39 -21
  188. data/lib/rgame/engine/nav_grid.rb +87 -0
  189. data/lib/rgame/engine/node2d.rb +248 -133
  190. data/lib/rgame/engine/path.rb +4 -6
  191. data/lib/rgame/engine/players.rb +6 -13
  192. data/lib/rgame/engine/presentation.rb +171 -0
  193. data/lib/rgame/engine/scene/scene_stack.rb +4 -4
  194. data/lib/rgame/engine/sealed_privates.rb +54 -0
  195. data/lib/rgame/engine/spatial_hash.rb +51 -19
  196. data/lib/rgame/engine/text.rb +194 -0
  197. data/lib/rgame/engine/tile_blockers.rb +63 -0
  198. data/lib/rgame/engine/tile_map.rb +2 -3
  199. data/lib/rgame/engine/tile_map_layer.rb +0 -2
  200. data/lib/rgame/engine/tileset.rb +2 -4
  201. data/lib/rgame/engine/timer.rb +2 -2
  202. data/lib/rgame/engine/ui/button.rb +248 -0
  203. data/lib/rgame/engine/ui/column.rb +20 -0
  204. data/lib/rgame/engine/ui/icon_button.rb +93 -0
  205. data/lib/rgame/engine/ui/menu.rb +246 -71
  206. data/lib/rgame/engine/ui/navigation.rb +57 -0
  207. data/lib/rgame/engine/ui/nine_slice_style.rb +50 -0
  208. data/lib/rgame/engine/ui/option_button.rb +163 -0
  209. data/lib/rgame/engine/ui/panel_button.rb +32 -0
  210. data/lib/rgame/engine/ui/panel_menu.rb +36 -0
  211. data/lib/rgame/engine/ui/pointing.rb +146 -0
  212. data/lib/rgame/engine/ui/radial_menu.rb +85 -0
  213. data/lib/rgame/engine/ui/ring.rb +55 -0
  214. data/lib/rgame/engine/ui/row.rb +21 -0
  215. data/lib/rgame/engine/ui/shape_style.rb +102 -0
  216. data/lib/rgame/engine/ui/stack.rb +58 -0
  217. data/lib/rgame/engine/ui/stepping.rb +93 -0
  218. data/lib/rgame/engine/ui/text_button.rb +59 -0
  219. data/lib/rgame/engine/viewports.rb +2 -5
  220. data/lib/rgame/engine/world_view.rb +5 -4
  221. data/lib/rgame/engine.rb +31 -25
  222. data/lib/rgame/game.rb +99 -27
  223. data/lib/rgame/rubocop/cop/game/draw_in_local_space.rb +103 -0
  224. data/lib/rgame/rubocop/cop/game/hot_path.rb +36 -0
  225. data/lib/rgame/rubocop/cop/game/layer_boundary.rb +43 -0
  226. data/lib/rgame/rubocop/cop/game/no_core_in_engine_layer.rb +100 -0
  227. data/lib/rgame/rubocop/cop/game/no_engine_in_core_layer.rb +84 -0
  228. data/lib/rgame/rubocop/cop/game/no_interpolation_in_hot_path.rb +50 -0
  229. data/lib/rgame/rubocop/cop/game/no_literal_text.rb +41 -0
  230. data/lib/rgame/rubocop/cop/game/no_needless_allocation.rb +112 -0
  231. data/lib/rgame/rubocop/default.yml +39 -0
  232. data/lib/rgame/rubocop/plugin.rb +45 -0
  233. data/lib/rgame/rubocop.rb +11 -0
  234. data/lib/rgame/util/color.rb +20 -24
  235. data/lib/rgame/util/controls.rb +14 -28
  236. data/lib/rgame/util/route_search.rb +27 -0
  237. data/lib/rgame/util/save_file.rb +107 -0
  238. data/lib/rgame/util/solid_grid.rb +37 -0
  239. data/lib/rgame/util/tensor.rb +0 -9
  240. data/lib/rgame/util/tile_sweep.rb +36 -0
  241. data/lib/rgame/util/z.rb +4 -14
  242. data/lib/rgame/util.rb +4 -3
  243. data/lib/rgame/version.rb +1 -1
  244. data/lib/rgame.rb +0 -15
  245. metadata +152 -11
  246. data/lib/rgame/engine/actor.rb +0 -53
  247. data/lib/rgame/engine/body.rb +0 -49
  248. data/lib/rgame/engine/cached_label.rb +0 -33
  249. data/lib/rgame/engine/input/player_controller.rb +0 -14
  250. data/lib/rgame/engine/matrix.rb +0 -32
  251. data/lib/rgame/engine/resettable.rb +0 -67
  252. data/lib/rgame/engine/tile_collision.rb +0 -78
  253. data/lib/rgame/engine/ui/menu_item.rb +0 -84
@@ -0,0 +1,44 @@
1
+ /*
2
+ * locale_ext.c — the Ruby binding for RGame::Core.preferred_locales.
3
+ *
4
+ * RGame::Core.preferred_locales # => ["de-AT", "en"]
5
+ *
6
+ * A module function rather than an App method because SDL answers without an
7
+ * app, or even SDL_Init. The reading is locale.c; this file only sizes a buffer
8
+ * and splits the list. Choosing a language from the answer is
9
+ * RGame::Engine::I18n.choose, which the glue calls.
10
+ */
11
+
12
+ #include "ruby/core_ext.h"
13
+
14
+ #include "rgame/core.h"
15
+
16
+ /*
17
+ * The user's preferred locales as Strings, most preferred first, or [] when the
18
+ * OS names none. Never contains nil or an empty String.
19
+ */
20
+ static VALUE core_preferred_locales(VALUE self) {
21
+ (void)self;
22
+
23
+ char stack[256];
24
+ size_t length = rgame_preferred_locales(stack, sizeof stack);
25
+ if (length < sizeof stack) {
26
+ return rb_str_split(rb_utf8_str_new(stack, (long)length), ",");
27
+ }
28
+
29
+ /* Too long for the stack. The environment is read on every call, so the
30
+ * list can grow between two of them: size again until what was written fits. */
31
+ for (;;) {
32
+ VALUE list = rb_utf8_str_new(NULL, (long)length);
33
+ size_t written = rgame_preferred_locales(RSTRING_PTR(list), length + 1);
34
+ if (written <= length) {
35
+ rb_str_set_len(list, (long)written);
36
+ return rb_str_split(list, ",");
37
+ }
38
+ length = written;
39
+ }
40
+ }
41
+
42
+ void rgame_init_locale(VALUE mCore) {
43
+ rb_define_module_function(mCore, "preferred_locales", core_preferred_locales, 0);
44
+ }
@@ -176,7 +176,7 @@ void rgame_init_recording(VALUE mCore) {
176
176
 
177
177
  rb_define_alloc_func(cRecording, recording_alloc);
178
178
  rb_define_singleton_method(cRecording, "new", recording_s_new, -1);
179
- rb_define_method(cRecording, "draw_at", recording_draw_at, 4);
179
+ rb_define_private_method(cRecording, "draw_at", recording_draw_at, 4);
180
180
  rb_define_method(cRecording, "batch_count", recording_batch_count, 0);
181
181
  rb_define_method(cRecording, "vertex_count", recording_vertex_count, 0);
182
182
  rb_define_method(cRecording, "empty?", recording_empty_p, 0);
@@ -374,26 +374,26 @@ void rgame_init_renderer(VALUE mCore) {
374
374
  rb_define_method(cRenderer, "drawing?", renderer_drawing_p, 0);
375
375
  rb_define_method(cRenderer, "app", renderer_app, 0);
376
376
 
377
- rb_define_method(cRenderer, "draw_rect", renderer_draw_rect, 6);
378
- rb_define_method(cRenderer, "draw_quad", renderer_draw_quad, -1);
379
- rb_define_method(cRenderer, "draw_triangle", renderer_draw_triangle, -1);
380
- rb_define_method(cRenderer, "draw_line", renderer_draw_line, -1);
381
- rb_define_method(cRenderer, "draw_circle", renderer_draw_circle, -1);
382
- rb_define_method(cRenderer, "draw_image", renderer_draw_image, 5);
383
- rb_define_method(cRenderer, "draw_image_scaled", renderer_draw_image_scaled, -1);
384
- rb_define_method(cRenderer, "draw_image_rot", renderer_draw_image_rot, -1);
385
- rb_define_method(cRenderer, "draw_text", renderer_draw_text, -1);
386
-
387
- rb_define_method(cRenderer, "push_translate", renderer_push_translate, 2);
388
- rb_define_method(cRenderer, "push_rotate", renderer_push_rotate, 3);
389
- rb_define_method(cRenderer, "push_scale", renderer_push_scale, 2);
390
- rb_define_method(cRenderer, "push_clip", renderer_push_clip, 4);
391
- rb_define_method(cRenderer, "push_layer", renderer_push_layer, 1);
377
+ rb_define_private_method(cRenderer, "draw_rect", renderer_draw_rect, 6);
378
+ rb_define_private_method(cRenderer, "draw_quad", renderer_draw_quad, -1);
379
+ rb_define_private_method(cRenderer, "draw_triangle", renderer_draw_triangle, -1);
380
+ rb_define_private_method(cRenderer, "draw_line", renderer_draw_line, -1);
381
+ rb_define_private_method(cRenderer, "draw_circle", renderer_draw_circle, -1);
382
+ rb_define_private_method(cRenderer, "draw_image", renderer_draw_image, 5);
383
+ rb_define_private_method(cRenderer, "draw_image_scaled", renderer_draw_image_scaled, -1);
384
+ rb_define_private_method(cRenderer, "draw_image_rot", renderer_draw_image_rot, -1);
385
+ rb_define_private_method(cRenderer, "draw_text", renderer_draw_text, -1);
386
+
387
+ rb_define_private_method(cRenderer, "push_translate", renderer_push_translate, 2);
388
+ rb_define_private_method(cRenderer, "push_rotate", renderer_push_rotate, 3);
389
+ rb_define_private_method(cRenderer, "push_scale", renderer_push_scale, 2);
390
+ rb_define_private_method(cRenderer, "push_clip", renderer_push_clip, 4);
391
+ rb_define_private_method(cRenderer, "push_layer", renderer_push_layer, 1);
392
392
  rb_define_method(cRenderer, "layer", renderer_layer, 0);
393
- rb_define_method(cRenderer, "next_layer_slot", renderer_next_layer_slot, 1);
394
- rb_define_method(cRenderer, "pop", renderer_pop, 0);
393
+ rb_define_private_method(cRenderer, "next_layer_slot", renderer_next_layer_slot, 1);
394
+ rb_define_private_method(cRenderer, "pop", renderer_pop, 0);
395
395
 
396
- rb_define_method(cRenderer, "begin_record", renderer_begin_record, 0);
397
- rb_define_method(cRenderer, "end_record", renderer_end_record, 0);
398
- rb_define_method(cRenderer, "cancel_record", renderer_cancel_record, 0);
396
+ rb_define_private_method(cRenderer, "begin_record", renderer_begin_record, 0);
397
+ rb_define_private_method(cRenderer, "end_record", renderer_end_record, 0);
398
+ rb_define_private_method(cRenderer, "cancel_record", renderer_cancel_record, 0);
399
399
  }
@@ -1,27 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # extconf.rb — mkmf script for the `rgame/util_ext` extension.
4
- #
5
- # This is the deliberately minimal half of the project's two extensions. Unlike
6
- # ext/rgame_core/extconf.rb (which links SDL2 + OpenGL for the engine), the
7
- # util extension is pure data: RGame::Util::Tensor and future pure-logic helpers
8
- # that must NOT drag SDL/GL into the process just to be required. So there is no
9
- # pkg_config, no -lGL — only the C standard + warning flags.
10
- #
11
- # That split is the rule for deciding where new code goes: anything depending on
12
- # SDL/OpenGL (or on something that does) belongs under RGame::Core in
13
- # ../rgame_core; everything else belongs here under RGame::Util.
14
-
15
3
  require 'mkmf'
16
4
 
17
- # Match the project's C standard and warning flags. gnu17 (not plain c17) because
18
- # Ruby's headers lean on GNU extensions; gnu17 is a superset so the code still
19
- # compiles as c17. Same choice as ext/rgame_core/extconf.rb.
20
5
  $CFLAGS << ' -std=gnu17 -Wall -Wextra'
21
6
 
22
- # "rgame/util_ext" -> the built object is loaded via `require "rgame/util_ext"`
23
- # and its entry point is Init_util_ext (the basename). Naming it under rgame/
24
- # (rather than plain "util_ext") keeps it namespaced alongside the engine's
25
- # rgame/core_ext.so and off the top of the load path, leaving the bare name
26
- # "rgame" to lib/rgame.rb.
7
+ $CFLAGS << ' -ffp-contract=off'
8
+
27
9
  create_makefile('rgame/util_ext')
@@ -0,0 +1,305 @@
1
+ /*
2
+ * route_search.c — regions and A* over a solid grid. Pure; see route_search.h.
3
+ *
4
+ * Costs are doubles and are never contracted into fused multiply-adds: two
5
+ * routes of equal cost are told apart by comparing sums, and a compiler that
6
+ * fused one of them would pick a different, equally cheap route on one machine
7
+ * than on another.
8
+ */
9
+
10
+ #include "route_search.h"
11
+
12
+ #include <stdlib.h>
13
+ #include <string.h>
14
+
15
+ #ifdef __clang__
16
+ #pragma STDC FP_CONTRACT OFF
17
+ #endif
18
+
19
+ static const double DIAGONAL = 1.4142135623730951;
20
+ static const double DIAGONAL_EXTRA = 1.4142135623730951 - 1.0;
21
+
22
+ bool rgame_route_search_init(rgame_route_search *search, const rgame_solid_grid *grid) {
23
+ memset(search, 0, sizeof(*search));
24
+ search->grid = grid;
25
+ search->cell_count = grid->width * grid->height;
26
+
27
+ size_t count = search->cell_count > 0 ? (size_t)search->cell_count : 1;
28
+ search->regions = malloc(count * sizeof(int32_t));
29
+ search->flood = malloc(count * sizeof(int32_t));
30
+ search->cost = malloc(count * sizeof(double));
31
+ search->parent = malloc(count * sizeof(int32_t));
32
+ search->seen = calloc(count, sizeof(uint32_t));
33
+ search->closed = calloc(count, sizeof(uint32_t));
34
+ search->route = malloc(count * sizeof(int32_t));
35
+
36
+ if (!search->regions || !search->flood || !search->cost || !search->parent || !search->seen ||
37
+ !search->closed || !search->route) {
38
+ rgame_route_search_free(search);
39
+ return false;
40
+ }
41
+ return true;
42
+ }
43
+
44
+ void rgame_route_search_free(rgame_route_search *search) {
45
+ free(search->regions);
46
+ free(search->flood);
47
+ free(search->cost);
48
+ free(search->parent);
49
+ free(search->seen);
50
+ free(search->closed);
51
+ free(search->heap_cell);
52
+ free(search->heap_total);
53
+ free(search->heap_remaining);
54
+ free(search->route);
55
+ memset(search, 0, sizeof(*search));
56
+ }
57
+
58
+ int rgame_route_neighbours(const rgame_solid_grid *grid, int32_t index, int32_t cells[8],
59
+ double steps[8]) {
60
+ int32_t width = grid->width;
61
+ int32_t col = index % width;
62
+ const uint8_t *solid = grid->cells;
63
+ bool west = col > 0 && !solid[index - 1];
64
+ bool east = col < width - 1 && !solid[index + 1];
65
+ bool north = index >= width && !solid[index - width];
66
+ bool south = index < grid->width * grid->height - width && !solid[index + width];
67
+
68
+ int count = 0;
69
+ if (west) { cells[count] = index - 1; steps[count++] = 1.0; }
70
+ if (east) { cells[count] = index + 1; steps[count++] = 1.0; }
71
+ if (north) { cells[count] = index - width; steps[count++] = 1.0; }
72
+ if (south) { cells[count] = index + width; steps[count++] = 1.0; }
73
+ if (north && west && !solid[index - width - 1]) { cells[count] = index - width - 1; steps[count++] = DIAGONAL; }
74
+ if (north && east && !solid[index - width + 1]) { cells[count] = index - width + 1; steps[count++] = DIAGONAL; }
75
+ if (south && west && !solid[index + width - 1]) { cells[count] = index + width - 1; steps[count++] = DIAGONAL; }
76
+ if (south && east && !solid[index + width + 1]) { cells[count] = index + width + 1; steps[count++] = DIAGONAL; }
77
+ return count;
78
+ }
79
+
80
+ /* --- regions --- */
81
+
82
+ static void claim(rgame_route_search *search, int32_t index, int32_t label, int32_t *top) {
83
+ if (search->grid->cells[index] || search->regions[index] != -1) {
84
+ return;
85
+ }
86
+ search->regions[index] = label;
87
+ search->flood[(*top)++] = index;
88
+ }
89
+
90
+ static void label_regions(rgame_route_search *search) {
91
+ const rgame_solid_grid *grid = search->grid;
92
+ int32_t width = grid->width;
93
+ int32_t count = search->cell_count;
94
+ for (int32_t index = 0; index < count; index++) {
95
+ search->regions[index] = -1;
96
+ }
97
+
98
+ int32_t label = 0;
99
+ for (int32_t seed = 0; seed < count; seed++) {
100
+ if (grid->cells[seed] || search->regions[seed] != -1) {
101
+ continue;
102
+ }
103
+ int32_t top = 0;
104
+ claim(search, seed, label, &top);
105
+ while (top > 0) {
106
+ int32_t index = search->flood[--top];
107
+ int32_t col = index % width;
108
+ if (col > 0) { claim(search, index - 1, label, &top); }
109
+ if (col < width - 1) { claim(search, index + 1, label, &top); }
110
+ if (index >= width) { claim(search, index - width, label, &top); }
111
+ if (index < count - width) { claim(search, index + width, label, &top); }
112
+ }
113
+ label++;
114
+ }
115
+
116
+ search->labelled = true;
117
+ search->labelled_at = grid->revision;
118
+ }
119
+
120
+ int32_t rgame_route_region(rgame_route_search *search, int32_t col, int32_t row) {
121
+ if (!rgame_solid_grid_inside(search->grid, col, row)) {
122
+ return -1;
123
+ }
124
+ if (!search->labelled || search->labelled_at != search->grid->revision) {
125
+ label_regions(search);
126
+ }
127
+ return search->regions[row * search->grid->width + col];
128
+ }
129
+
130
+ /* --- the heap: parallel arrays, ordered by total then remaining --- */
131
+
132
+ static bool heap_reserve(rgame_route_search *search) {
133
+ if (search->heap_size < search->heap_capacity) {
134
+ return true;
135
+ }
136
+ size_t capacity = search->heap_capacity ? search->heap_capacity * 2 : 64;
137
+ int32_t *cells = realloc(search->heap_cell, capacity * sizeof(int32_t));
138
+ if (!cells) {
139
+ return false;
140
+ }
141
+ search->heap_cell = cells;
142
+ double *totals = realloc(search->heap_total, capacity * sizeof(double));
143
+ if (!totals) {
144
+ return false;
145
+ }
146
+ search->heap_total = totals;
147
+ double *remainings = realloc(search->heap_remaining, capacity * sizeof(double));
148
+ if (!remainings) {
149
+ return false;
150
+ }
151
+ search->heap_remaining = remainings;
152
+ search->heap_capacity = capacity;
153
+ return true;
154
+ }
155
+
156
+ static bool push(rgame_route_search *search, int32_t index, double total, double remaining) {
157
+ if (!heap_reserve(search)) {
158
+ return false;
159
+ }
160
+ int32_t *cell = search->heap_cell;
161
+ double *totals = search->heap_total;
162
+ double *remainings = search->heap_remaining;
163
+ size_t slot = search->heap_size++;
164
+ while (slot > 0) {
165
+ size_t up = (slot - 1) >> 1;
166
+ if (totals[up] < total || (totals[up] == total && remainings[up] <= remaining)) {
167
+ break;
168
+ }
169
+ cell[slot] = cell[up];
170
+ totals[slot] = totals[up];
171
+ remainings[slot] = remainings[up];
172
+ slot = up;
173
+ }
174
+ cell[slot] = index;
175
+ totals[slot] = total;
176
+ remainings[slot] = remaining;
177
+ return true;
178
+ }
179
+
180
+ static int32_t pop(rgame_route_search *search) {
181
+ int32_t *cell = search->heap_cell;
182
+ double *totals = search->heap_total;
183
+ double *remainings = search->heap_remaining;
184
+ int32_t top = cell[0];
185
+ size_t size = --search->heap_size;
186
+ if (size == 0) {
187
+ return top;
188
+ }
189
+
190
+ int32_t index = cell[size];
191
+ double total = totals[size];
192
+ double remaining = remainings[size];
193
+ size_t slot = 0;
194
+ size_t child;
195
+ while ((child = (slot << 1) + 1) < size) {
196
+ size_t right = child + 1;
197
+ if (right < size && (totals[right] < totals[child] ||
198
+ (totals[right] == totals[child] && remainings[right] < remainings[child]))) {
199
+ child = right;
200
+ }
201
+ if (total < totals[child] || (total == totals[child] && remaining <= remainings[child])) {
202
+ break;
203
+ }
204
+ cell[slot] = cell[child];
205
+ totals[slot] = totals[child];
206
+ remainings[slot] = remainings[child];
207
+ slot = child;
208
+ }
209
+ cell[slot] = index;
210
+ totals[slot] = total;
211
+ remainings[slot] = remaining;
212
+ return top;
213
+ }
214
+
215
+ /* --- A* --- */
216
+
217
+ static void next_generation(rgame_route_search *search) {
218
+ if (++search->generation == 0) {
219
+ size_t count = search->cell_count > 0 ? (size_t)search->cell_count : 1;
220
+ memset(search->seen, 0, count * sizeof(uint32_t));
221
+ memset(search->closed, 0, count * sizeof(uint32_t));
222
+ search->generation = 1;
223
+ }
224
+ }
225
+
226
+ static bool relax(rgame_route_search *search, int32_t from, int32_t to, double cost, int32_t goal_col,
227
+ int32_t goal_row) {
228
+ uint32_t generation = search->generation;
229
+ if (search->closed[to] == generation) {
230
+ return true;
231
+ }
232
+ if (search->seen[to] == generation && search->cost[to] <= cost) {
233
+ return true;
234
+ }
235
+
236
+ search->seen[to] = generation;
237
+ search->cost[to] = cost;
238
+ search->parent[to] = from;
239
+ int32_t width = search->grid->width;
240
+ int32_t dx = abs(to % width - goal_col);
241
+ int32_t dy = abs(to / width - goal_row);
242
+ double remaining = dx < dy ? (double)dy + DIAGONAL_EXTRA * dx : (double)dx + DIAGONAL_EXTRA * dy;
243
+ return push(search, to, cost + remaining, remaining);
244
+ }
245
+
246
+ static rgame_route_result search_route(rgame_route_search *search, int32_t start, int32_t goal,
247
+ int32_t goal_col, int32_t goal_row) {
248
+ next_generation(search);
249
+ search->heap_size = 0;
250
+ if (!relax(search, -1, start, 0.0, goal_col, goal_row)) {
251
+ return RGAME_ROUTE_NO_MEMORY;
252
+ }
253
+
254
+ int32_t cells[8];
255
+ double steps[8];
256
+ while (search->heap_size > 0) {
257
+ int32_t index = pop(search);
258
+ if (search->closed[index] == search->generation) {
259
+ continue;
260
+ }
261
+ if (index == goal) {
262
+ return RGAME_ROUTE_FOUND;
263
+ }
264
+
265
+ search->closed[index] = search->generation;
266
+ int count = rgame_route_neighbours(search->grid, index, cells, steps);
267
+ double cost = search->cost[index];
268
+ for (int i = 0; i < count; i++) {
269
+ if (!relax(search, index, cells[i], cost + steps[i], goal_col, goal_row)) {
270
+ return RGAME_ROUTE_NO_MEMORY;
271
+ }
272
+ }
273
+ }
274
+ /* Unreachable once regions agree the two ends are joined. */
275
+ return RGAME_ROUTE_NONE;
276
+ }
277
+
278
+ static void collect_route(rgame_route_search *search, int32_t goal) {
279
+ int32_t length = 0;
280
+ for (int32_t index = goal; index != -1; index = search->parent[index]) {
281
+ length++;
282
+ }
283
+ int32_t slot = length;
284
+ for (int32_t index = goal; index != -1; index = search->parent[index]) {
285
+ search->route[--slot] = index;
286
+ }
287
+ search->route_length = length;
288
+ }
289
+
290
+ rgame_route_result rgame_route_find(rgame_route_search *search, int32_t from_col, int32_t from_row,
291
+ int32_t to_col, int32_t to_row) {
292
+ search->route_length = 0;
293
+ int32_t from_region = rgame_route_region(search, from_col, from_row);
294
+ if (from_region == -1 || from_region != rgame_route_region(search, to_col, to_row)) {
295
+ return RGAME_ROUTE_NONE;
296
+ }
297
+
298
+ int32_t width = search->grid->width;
299
+ int32_t goal = to_row * width + to_col;
300
+ rgame_route_result result = search_route(search, from_row * width + from_col, goal, to_col, to_row);
301
+ if (result == RGAME_ROUTE_FOUND) {
302
+ collect_route(search, goal);
303
+ }
304
+ return result;
305
+ }
@@ -0,0 +1,86 @@
1
+ #ifndef RGAME_ROUTE_SEARCH_H
2
+ #define RGAME_ROUTE_SEARCH_H
3
+
4
+ #include <stdbool.h>
5
+ #include <stddef.h>
6
+ #include <stdint.h>
7
+
8
+ #include "solid_grid.h"
9
+
10
+ /*
11
+ * Connected regions and A* routes over a solid grid — pure, no Ruby, no SDL.
12
+ *
13
+ * A search reads its grid and never writes it. Everything it keeps between
14
+ * queries lives in the search, so any number of searches may read one grid —
15
+ * which is what lets many walkers plan over the one store a tile world's
16
+ * blockers also read.
17
+ *
18
+ * The grid is 8-connected with octile costs (1 straight, sqrt 2 diagonal), and a
19
+ * diagonal step is allowed only when both orthogonal cells beside it are open,
20
+ * so a route never cuts the corner of a solid cell. That rule is
21
+ * rgame_route_neighbours, and every search over the grid goes through it.
22
+ *
23
+ * Regions are labelled by an orthogonal flood fill the first time one is asked
24
+ * for, and again whenever the grid's revision has moved since — so an
25
+ * unreachable goal, the answer A* is slowest to give, is a lookup, and it stays
26
+ * right when the grid changes.
27
+ *
28
+ * A search allocates its per-cell buffers once, at init, and marks which
29
+ * entries are current with a generation stamp, so a query costs nothing
30
+ * proportional to the grid. The heap grows to the largest query seen and never
31
+ * shrinks: a repeated query allocates nothing.
32
+ */
33
+
34
+ typedef enum {
35
+ RGAME_ROUTE_FOUND,
36
+ RGAME_ROUTE_NONE, /* an end is solid, outside, or in another region */
37
+ RGAME_ROUTE_NO_MEMORY /* the heap could not grow; the search is still usable */
38
+ } rgame_route_result;
39
+
40
+ typedef struct {
41
+ const rgame_solid_grid *grid;
42
+ int32_t cell_count;
43
+
44
+ int32_t *regions; /* per cell; -1 solid */
45
+ int32_t *flood; /* the flood fill's stack */
46
+ bool labelled;
47
+ uint64_t labelled_at; /* the grid revision the labels describe */
48
+
49
+ double *cost;
50
+ int32_t *parent;
51
+ uint32_t *seen;
52
+ uint32_t *closed;
53
+ uint32_t generation; /* stamps; wrapping clears them */
54
+
55
+ int32_t *heap_cell;
56
+ double *heap_total;
57
+ double *heap_remaining;
58
+ size_t heap_size;
59
+ size_t heap_capacity; /* grows, never shrinks */
60
+
61
+ int32_t *route; /* flat indices, start first, after RGAME_ROUTE_FOUND */
62
+ int32_t route_length;
63
+ } rgame_route_search;
64
+
65
+ /* The grid must outlive the search. False, with nothing to free, when
66
+ * allocation fails. */
67
+ bool rgame_route_search_init(rgame_route_search *search, const rgame_solid_grid *grid);
68
+ void rgame_route_search_free(rgame_route_search *search);
69
+
70
+ /* The cells one step from `index` may reach, in the order a search relaxes them
71
+ * (west, east, north, south, then the four diagonals), each with its step cost.
72
+ * Returns how many were written, at most 8. */
73
+ int rgame_route_neighbours(const rgame_solid_grid *grid, int32_t index, int32_t cells[8],
74
+ double steps[8]);
75
+
76
+ /* The region label of a cell — two walkable cells share one exactly when a
77
+ * route joins them — or -1 for a solid cell or one outside the grid. */
78
+ int32_t rgame_route_region(rgame_route_search *search, int32_t col, int32_t row);
79
+
80
+ /* The cheapest route from start to goal, both included, into search->route.
81
+ * Ties between equally cheap routes go to the entry with the smaller remaining
82
+ * estimate, so the same query always returns the same route. */
83
+ rgame_route_result rgame_route_find(rgame_route_search *search, int32_t from_col, int32_t from_row,
84
+ int32_t to_col, int32_t to_row);
85
+
86
+ #endif /* RGAME_ROUTE_SEARCH_H */
@@ -0,0 +1,150 @@
1
+ /*
2
+ * route_search_ext.c — the Ruby binding for RGame::Util::RouteSearch.
3
+ *
4
+ * The search lives in route_search.{c,h}, pure and covered by the Check suite.
5
+ * What this file adds is lifetime: the C search holds a pointer into the
6
+ * SolidGrid it reads, so the Ruby object holds the grid too and marks it, and
7
+ * the grid cannot be collected while a search over it is alive.
8
+ */
9
+
10
+ #include "util_ext.h"
11
+
12
+ #include "route_search.h"
13
+
14
+ typedef struct {
15
+ VALUE grid;
16
+ rgame_route_search search;
17
+ bool ready;
18
+ } route_search_ref;
19
+
20
+ static long live_searches = 0;
21
+
22
+ static void route_search_mark(void *ptr) {
23
+ route_search_ref *ref = ptr;
24
+ rb_gc_mark(ref->grid);
25
+ }
26
+
27
+ static void route_search_free(void *ptr) {
28
+ route_search_ref *ref = ptr;
29
+ if (ref->ready) {
30
+ rgame_route_search_free(&ref->search);
31
+ }
32
+ xfree(ref);
33
+ live_searches--;
34
+ }
35
+
36
+ static size_t route_search_memsize(const void *ptr) {
37
+ const route_search_ref *ref = ptr;
38
+ size_t cells = ref->ready ? (size_t)ref->search.cell_count : 0;
39
+ size_t per_cell = 4 * sizeof(int32_t) + sizeof(double) + 2 * sizeof(uint32_t);
40
+ size_t per_entry = sizeof(int32_t) + 2 * sizeof(double);
41
+ return sizeof(*ref) + cells * per_cell + ref->search.heap_capacity * per_entry;
42
+ }
43
+
44
+ static const rb_data_type_t route_search_data_type = {
45
+ .wrap_struct_name = "rgame_route_search",
46
+ .function = {
47
+ .dmark = route_search_mark,
48
+ .dfree = route_search_free,
49
+ .dsize = route_search_memsize,
50
+ },
51
+ .flags = RUBY_TYPED_FREE_IMMEDIATELY,
52
+ };
53
+
54
+ static rgame_route_search *unwrap(VALUE self) {
55
+ route_search_ref *ref;
56
+ TypedData_Get_Struct(self, route_search_ref, &route_search_data_type, ref);
57
+ return &ref->search;
58
+ }
59
+
60
+ /* RouteSearch.new(grid) — a search over a SolidGrid, which it keeps alive. */
61
+ static VALUE route_search_s_new(VALUE klass, VALUE grid) {
62
+ rgame_solid_grid *cells = rgame_util_solid_grid(grid);
63
+
64
+ route_search_ref *ref;
65
+ VALUE object = TypedData_Make_Struct(klass, route_search_ref, &route_search_data_type, ref);
66
+ live_searches++;
67
+ ref->grid = grid;
68
+ if (!rgame_route_search_init(&ref->search, cells)) {
69
+ rb_memerror();
70
+ }
71
+ ref->ready = true;
72
+ return object;
73
+ }
74
+
75
+ static VALUE route_search_grid(VALUE self) {
76
+ route_search_ref *ref;
77
+ TypedData_Get_Struct(self, route_search_ref, &route_search_data_type, ref);
78
+ return ref->grid;
79
+ }
80
+
81
+ /* region(col, row) — an Integer label two walkable cells share exactly when a
82
+ * route joins them, or nil for a solid cell or one outside the grid. Labels are
83
+ * recomputed on the first ask after the grid changes. */
84
+ static VALUE route_search_region(VALUE self, VALUE col_value, VALUE row_value) {
85
+ int32_t col = 0;
86
+ int32_t row = 0;
87
+ bool col_fits = rgame_util_cell_coordinate(col_value, &col);
88
+ bool row_fits = rgame_util_cell_coordinate(row_value, &row);
89
+ if (!col_fits || !row_fits) {
90
+ return Qnil;
91
+ }
92
+ int32_t region = rgame_route_region(unwrap(self), col, row);
93
+ return region == -1 ? Qnil : INT2NUM(region);
94
+ }
95
+
96
+ /* find(from_col, from_row, to_col, to_row) — the cheapest route, both ends
97
+ * included, as [[col, row], ...]; nil when either end is solid, outside the
98
+ * grid, or in another region. Equally cheap routes are told apart the same way
99
+ * every time. */
100
+ static VALUE route_search_find(VALUE self, VALUE from_col_value, VALUE from_row_value,
101
+ VALUE to_col_value, VALUE to_row_value) {
102
+ int32_t from_col = 0;
103
+ int32_t from_row = 0;
104
+ int32_t to_col = 0;
105
+ int32_t to_row = 0;
106
+ bool fits = rgame_util_cell_coordinate(from_col_value, &from_col);
107
+ fits = rgame_util_cell_coordinate(from_row_value, &from_row) && fits;
108
+ fits = rgame_util_cell_coordinate(to_col_value, &to_col) && fits;
109
+ fits = rgame_util_cell_coordinate(to_row_value, &to_row) && fits;
110
+ if (!fits) {
111
+ return Qnil;
112
+ }
113
+
114
+ rgame_route_search *search = unwrap(self);
115
+ switch (rgame_route_find(search, from_col, from_row, to_col, to_row)) {
116
+ case RGAME_ROUTE_NONE:
117
+ return Qnil;
118
+ case RGAME_ROUTE_NO_MEMORY:
119
+ rb_memerror();
120
+ case RGAME_ROUTE_FOUND:
121
+ break;
122
+ }
123
+
124
+ int32_t width = search->grid->width;
125
+ VALUE route = rb_ary_new_capa(search->route_length);
126
+ for (int32_t i = 0; i < search->route_length; i++) {
127
+ int32_t index = search->route[i];
128
+ rb_ary_push(route, rb_assoc_new(INT2FIX(index % width), INT2FIX(index / width)));
129
+ }
130
+ return route;
131
+ }
132
+
133
+ /* RouteSearch.debug_live_searches — how many searches are holding buffers.
134
+ * Test-only, and named so. */
135
+ static VALUE route_search_s_debug_live_searches(VALUE klass) {
136
+ (void)klass;
137
+ return LONG2NUM(live_searches);
138
+ }
139
+
140
+ void rgame_init_route_search(VALUE mUtil) {
141
+ VALUE cRouteSearch = rb_define_class_under(mUtil, "RouteSearch", rb_cObject);
142
+
143
+ rb_undef_alloc_func(cRouteSearch);
144
+ rb_define_singleton_method(cRouteSearch, "new", route_search_s_new, 1);
145
+ rb_define_singleton_method(cRouteSearch, "debug_live_searches", route_search_s_debug_live_searches, 0);
146
+
147
+ rb_define_method(cRouteSearch, "grid", route_search_grid, 0);
148
+ rb_define_method(cRouteSearch, "region", route_search_region, 2);
149
+ rb_define_method(cRouteSearch, "find", route_search_find, 4);
150
+ }