kumi 0.0.4 → 0.0.5

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 (49) hide show
  1. checksums.yaml +4 -4
  2. data/CLAUDE.md +109 -2
  3. data/README.md +169 -213
  4. data/documents/DSL.md +3 -3
  5. data/documents/SYNTAX.md +17 -26
  6. data/examples/federal_tax_calculator_2024.rb +36 -38
  7. data/examples/game_of_life.rb +97 -0
  8. data/examples/simple_rpg_game.rb +1000 -0
  9. data/examples/static_analysis_errors.rb +178 -0
  10. data/examples/wide_schema_compilation_and_evaluation_benchmark.rb +1 -1
  11. data/lib/kumi/analyzer/analysis_state.rb +37 -0
  12. data/lib/kumi/analyzer/constant_evaluator.rb +22 -16
  13. data/lib/kumi/analyzer/passes/definition_validator.rb +4 -3
  14. data/lib/kumi/analyzer/passes/dependency_resolver.rb +50 -10
  15. data/lib/kumi/analyzer/passes/input_collector.rb +28 -7
  16. data/lib/kumi/analyzer/passes/name_indexer.rb +2 -2
  17. data/lib/kumi/analyzer/passes/pass_base.rb +10 -27
  18. data/lib/kumi/analyzer/passes/semantic_constraint_validator.rb +110 -0
  19. data/lib/kumi/analyzer/passes/toposorter.rb +3 -3
  20. data/lib/kumi/analyzer/passes/type_checker.rb +2 -1
  21. data/lib/kumi/analyzer/passes/type_consistency_checker.rb +2 -1
  22. data/lib/kumi/analyzer/passes/type_inferencer.rb +2 -4
  23. data/lib/kumi/analyzer/passes/unsat_detector.rb +233 -14
  24. data/lib/kumi/analyzer/passes/visitor_pass.rb +2 -1
  25. data/lib/kumi/analyzer.rb +42 -24
  26. data/lib/kumi/atom_unsat_solver.rb +45 -0
  27. data/lib/kumi/cli.rb +449 -0
  28. data/lib/kumi/constraint_relationship_solver.rb +638 -0
  29. data/lib/kumi/error_reporter.rb +6 -6
  30. data/lib/kumi/evaluation_wrapper.rb +20 -4
  31. data/lib/kumi/explain.rb +8 -8
  32. data/lib/kumi/function_registry/collection_functions.rb +103 -0
  33. data/lib/kumi/parser/dsl_cascade_builder.rb +17 -6
  34. data/lib/kumi/parser/expression_converter.rb +80 -12
  35. data/lib/kumi/parser/parser.rb +2 -0
  36. data/lib/kumi/parser/sugar.rb +117 -16
  37. data/lib/kumi/schema.rb +3 -1
  38. data/lib/kumi/schema_instance.rb +69 -3
  39. data/lib/kumi/syntax/declarations.rb +3 -0
  40. data/lib/kumi/syntax/expressions.rb +4 -0
  41. data/lib/kumi/syntax/root.rb +1 -0
  42. data/lib/kumi/syntax/terminal_expressions.rb +3 -0
  43. data/lib/kumi/types/compatibility.rb +8 -0
  44. data/lib/kumi/types/validator.rb +1 -1
  45. data/lib/kumi/version.rb +1 -1
  46. data/scripts/generate_function_docs.rb +22 -10
  47. metadata +10 -6
  48. data/CHANGELOG.md +0 -25
  49. data/test_impossible_cascade.rb +0 -51
@@ -0,0 +1,1000 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require_relative "../lib/kumi"
5
+
6
+ module DamageCalculation
7
+ extend Kumi::Schema
8
+
9
+ schema do
10
+ input do
11
+ # Base combat stats
12
+ integer :base_attack, domain: 0..500
13
+ integer :strength, domain: 0..100
14
+ integer :level, domain: 1..100
15
+
16
+ # Equipment bonuses
17
+ integer :weapon_damage, domain: 0..100
18
+ integer :weapon_bonus, domain: 0..50
19
+ string :weapon_type, domain: %w[sword dagger staff bow fists]
20
+
21
+ # Combat modifiers
22
+ boolean :critical_hit
23
+ string :attack_type, domain: %w[normal magic special]
24
+ array :abilities, elem: { type: :string }
25
+ array :status_effects, elem: { type: :string }
26
+
27
+ # Status effect turn counters
28
+ integer :rage_turns, domain: 0..10
29
+ integer :poison_turns, domain: 0..10
30
+ integer :blessing_turns, domain: 0..10
31
+ end
32
+
33
+ # Combat traits
34
+ trait :is_critical, (input.critical_hit == true)
35
+ trait :is_magic_attack, (input.attack_type == "magic")
36
+ trait :is_special_attack, (input.attack_type == "special")
37
+ trait :has_rage, fn(:include?, input.status_effects, "rage")
38
+ trait :has_blessing, fn(:include?, input.status_effects, "blessing")
39
+ trait :is_poisoned, fn(:include?, input.status_effects, "poison")
40
+ trait :has_precision, fn(:include?, input.abilities, "precision")
41
+ trait :critical_precision, is_critical & has_precision
42
+
43
+ # Weapon type traits
44
+ trait :melee_weapon, fn(:include?, ["sword", "dagger"], input.weapon_type)
45
+ trait :ranged_weapon, (input.weapon_type == "bow")
46
+ trait :magic_weapon, (input.weapon_type == "staff")
47
+
48
+ # Strength traits
49
+ trait :high_strength, (input.strength >= 15)
50
+
51
+ # Base damage calculation
52
+ value :strength_bonus do
53
+ on high_strength, input.strength * 2 # NEW: bare identifier syntax
54
+ base input.strength
55
+ end
56
+
57
+ value :weapon_total_damage, (input.weapon_damage + input.weapon_bonus)
58
+
59
+ value :base_damage_value, (input.base_attack + strength_bonus + weapon_total_damage)
60
+
61
+ # Status effect modifiers
62
+ value :status_attack_multiplier do
63
+ on has_rage, 1.5 # NEW: bare identifier syntax
64
+ on is_poisoned, 0.8 # NEW: bare identifier syntax
65
+ on has_blessing, 1.1 # NEW: bare identifier syntax
66
+ base 1.0
67
+ end
68
+
69
+ # Critical and special attack modifiers
70
+ value :attack_type_multiplier do
71
+ on critical_precision, 3.0 # NEW: bare identifier syntax
72
+ on is_critical, 2.0 # NEW: bare identifier syntax
73
+ on is_special_attack, 1.3 # NEW: bare identifier syntax
74
+ on is_magic_attack, 1.2 # NEW: bare identifier syntax
75
+ base 1.0
76
+ end
77
+
78
+ # Level scaling
79
+ value :level_bonus, (input.level / 3)
80
+
81
+ # Final damage calculation
82
+ value :total_damage, fn(:round, (base_damage_value + level_bonus) * status_attack_multiplier * attack_type_multiplier)
83
+
84
+ # Damage description for UI
85
+ value :damage_description do
86
+ on critical_precision, "šŸ’„šŸŽÆ PRECISION CRITICAL!" # NEW: bare identifier syntax
87
+ on is_critical, "šŸ’„ CRITICAL HIT!" # NEW: bare identifier syntax
88
+ on is_special_attack, "✨ Special Attack!" # NEW: bare identifier syntax
89
+ on is_magic_attack, "šŸ”® Magic Attack!" # NEW: bare identifier syntax
90
+ base "āš”ļø Attack!"
91
+ end
92
+ end
93
+ end
94
+
95
+ module DamageReduction
96
+ extend Kumi::Schema
97
+
98
+ schema do
99
+ input do
100
+ # Base defensive stats
101
+ integer :base_defense, domain: 0..300
102
+ integer :defense_stat, domain: 0..100
103
+ integer :agility, domain: 0..100
104
+ integer :level, domain: 1..100
105
+
106
+ # Equipment bonuses
107
+ integer :armor_defense, domain: 0..50
108
+ integer :armor_bonus, domain: 0..30
109
+ string :armor_type, domain: %w[leather chainmail plate robe none]
110
+
111
+ # Incoming damage info
112
+ integer :incoming_damage, domain: 0..1000
113
+ string :damage_type, domain: %w[normal magic special]
114
+
115
+ # Status effects
116
+ array :status_effects, elem: { type: :string }
117
+ integer :shield_turns, domain: 0..10
118
+ integer :blessing_turns, domain: 0..10
119
+ integer :poison_turns, domain: 0..10
120
+ end
121
+
122
+ # Defensive traits
123
+ trait :has_shield, fn(:include?, input.status_effects, "shield")
124
+ trait :has_blessing, fn(:include?, input.status_effects, "blessing")
125
+ trait :is_poisoned, fn(:include?, input.status_effects, "poison")
126
+ trait :taking_magic_damage, (input.damage_type == "magic")
127
+ trait :taking_special_damage, (input.damage_type == "special")
128
+ trait :high_agility, (input.agility >= 15)
129
+ trait :heavy_armor, fn(:include?, ["chainmail", "plate"], input.armor_type)
130
+ trait :light_armor, fn(:include?, ["leather", "robe"], input.armor_type)
131
+
132
+ # Defense traits
133
+ trait :high_defense, (input.defense_stat >= 15)
134
+
135
+ # Composite armor/damage type traits
136
+ trait :heavy_vs_magic, heavy_armor & taking_magic_damage
137
+ trait :light_vs_magic, light_armor & taking_magic_damage
138
+
139
+ # Base defense calculation
140
+ value :defense_bonus do
141
+ on high_defense, (input.defense_stat * 1.5) # NEW: bare identifier syntax
142
+ base input.defense_stat
143
+ end
144
+
145
+ value :equipment_defense, (input.armor_defense + input.armor_bonus)
146
+
147
+ value :level_defense_bonus, (input.level / 2)
148
+
149
+ value :base_defense_value, (input.base_defense + defense_bonus + equipment_defense + level_defense_bonus)
150
+
151
+ # Status effect defensive modifiers
152
+ value :status_defense_multiplier do
153
+ on has_shield, 1.4 # NEW: bare identifier syntax
154
+ on has_blessing, 1.2 # NEW: bare identifier syntax
155
+ on is_poisoned, 0.9 # NEW: bare identifier syntax
156
+ base 1.0
157
+ end
158
+
159
+ # Armor type resistances
160
+ value :armor_resistance do
161
+ on heavy_vs_magic, 0.8 # Heavy armor weak to magic
162
+ on light_vs_magic, 1.1 # Light armor resists magic
163
+ on heavy_armor, 1.2 # Heavy armor good vs physical
164
+ on light_armor, 0.9 # Light armor weak vs physical
165
+ base 1.0
166
+ end
167
+
168
+ # Calculate total defense
169
+ value :total_defense, fn(:round, base_defense_value * status_defense_multiplier * armor_resistance)
170
+
171
+ # Dodge calculation
172
+ value :dodge_chance do
173
+ on high_agility, fn(:min, [(input.agility * 0.05), 0.3])
174
+ base fn(:min, [(input.agility * 0.02), 0.15])
175
+ end
176
+
177
+ # Final damage after reduction
178
+ value :damage_after_defense, fn(:max, [(input.incoming_damage - total_defense), 1])
179
+
180
+ # Defense description for UI
181
+ value :defense_description do
182
+ on has_shield, "šŸ›”ļø Shield Active!"
183
+ on heavy_armor, "āš”ļø Heavy Armor Protection"
184
+ on light_armor, "šŸƒ Light Armor Agility"
185
+ base "šŸ›”ļø Defending"
186
+ end
187
+ end
188
+ end
189
+
190
+ module Equipment
191
+ extend Kumi::Schema
192
+
193
+ schema do
194
+ input do
195
+ string :weapon_name
196
+ string :weapon_type, domain: %w[sword dagger staff bow fists]
197
+ integer :weapon_damage, domain: 0..50
198
+ string :armor_name
199
+ string :armor_type, domain: %w[leather chainmail plate robe none]
200
+ integer :armor_defense, domain: 0..30
201
+ string :accessory_name
202
+ string :accessory_type, domain: %w[ring amulet boots gloves none]
203
+ integer :accessory_bonus, domain: 0..20
204
+ end
205
+
206
+ trait :has_weapon, (input.weapon_type != "fists")
207
+ trait :has_armor, (input.armor_type != "none")
208
+ trait :has_accessory, (input.accessory_type != "none")
209
+ trait :melee_weapon, fn(:include?, ["sword", "dagger"], input.weapon_type)
210
+ trait :ranged_weapon, (input.weapon_type == "bow")
211
+ trait :magic_weapon, (input.weapon_type == "staff")
212
+ trait :heavy_armor, fn(:include?, ["chainmail", "plate"], input.armor_type)
213
+ trait :light_armor, fn(:include?, ["leather", "robe"], input.armor_type)
214
+
215
+ value :total_weapon_damage do
216
+ on ranged_weapon, (input.weapon_damage + 4)
217
+ on magic_weapon, (input.weapon_damage + 3)
218
+ on has_weapon, (input.weapon_damage + 2)
219
+ base 2
220
+ end
221
+
222
+ value :total_armor_defense do
223
+ on has_armor, (input.armor_defense + 1)
224
+ base 0
225
+ end
226
+
227
+ value :equipment_agility_modifier do
228
+ on heavy_armor, -2
229
+ on light_armor, 1
230
+ base 0
231
+ end
232
+
233
+ value :equipment_strength_modifier do
234
+ on melee_weapon, 2
235
+ base 0
236
+ end
237
+
238
+ value :equipment_description do
239
+ on has_weapon,has_armor,has_accessory, fn(:concat, [input.weapon_name, ", ", input.armor_name, ", ", input.accessory_name])
240
+ on has_weapon,has_armor, fn(:concat, [input.weapon_name, ", ", input.armor_name])
241
+ on has_weapon, fn(:concat, [input.weapon_name, " (Unarmored)"])
242
+ base "Basic gear"
243
+ end
244
+ end
245
+ end
246
+
247
+ module PlayerEntity
248
+ extend Kumi::Schema
249
+
250
+ schema do
251
+ input do
252
+ string :name
253
+ integer :level, domain: 1..100
254
+ integer :health, domain: 0..1000
255
+ integer :max_health, domain: 1..1000
256
+ integer :mana, domain: 0..500
257
+ integer :max_mana, domain: 0..500
258
+ integer :strength, domain: 1..100
259
+ integer :defense, domain: 1..100
260
+ integer :agility, domain: 1..100
261
+ integer :experience, domain: 0..Float::INFINITY
262
+ string :weapon, domain: %w[sword dagger staff bow fists]
263
+ array :inventory, elem: { type: :string }
264
+ hash :stats, key: { type: :string }, val: { type: :integer }
265
+ hash :equipment, key: { type: :string }, val: { type: :any }
266
+ array :status_effects, elem: { type: :string }
267
+ integer :poison_turns, domain: 0..10
268
+ integer :blessing_turns, domain: 0..10
269
+ integer :rage_turns, domain: 0..5
270
+ integer :shield_turns, domain: 0..5
271
+ end
272
+
273
+ trait :alive, (input.health > 0)
274
+ trait :dead, (input.health <= 0)
275
+ trait :low_health, (input.health <= input.max_health * 0.3) & alive
276
+ trait :full_health, (input.health == input.max_health)
277
+ trait :has_mana, (input.mana > 0)
278
+ trait :low_mana, (input.mana <= input.max_mana * 0.2)
279
+ trait :strong, (input.strength >= 15)
280
+ trait :agile, (input.agility >= 15)
281
+ trait :tanky, (input.defense >= 15)
282
+ trait :experienced, (input.level >= 5)
283
+ trait :has_sword, (input.weapon == "sword")
284
+ trait :has_dagger, (input.weapon == "dagger")
285
+ trait :has_staff, (input.weapon == "staff")
286
+ trait :has_bow, (input.weapon == "bow")
287
+ trait :well_equipped, fn(:include?, input.inventory, "upgrade_token")
288
+ trait :has_potions, fn(:include?, input.inventory, "potion")
289
+ trait :has_status_effects, (fn(:size, input.status_effects) > 0)
290
+
291
+ value :health_percentage, ((input.health * 100) / input.max_health)
292
+ value :mana_percentage, ((input.mana * 100) / input.max_mana)
293
+
294
+ # Basic combat stats (for UI display)
295
+ value :base_weapon_bonus do
296
+ on has_sword, 8
297
+ on has_dagger, 5
298
+ on has_staff, 3
299
+ on has_bow, 6
300
+ base 2
301
+ end
302
+
303
+ value :equipment_defense_bonus do
304
+ on well_equipped, 2
305
+ base 0
306
+ end
307
+
308
+ # Simplified values for UI - actual combat will use the damage schemas
309
+ value :total_attack, (15 + input.strength + fn(:fetch, input.equipment, "weapon_damage", 12) + base_weapon_bonus)
310
+ value :defense_rating, (10 + input.defense + fn(:fetch, input.equipment, "armor_defense", 5) + (input.level / 2))
311
+ value :dodge_chance do
312
+ on agile, fn(:min, [(input.agility * 0.05), 0.3])
313
+ base fn(:min, [(input.agility * 0.02), 0.15])
314
+ end
315
+
316
+ value :health_status_description do
317
+ on dead, "šŸ’€ Dead"
318
+ on low_health, "āš ļø Critically wounded"
319
+ on full_health, "šŸ’š Perfect condition"
320
+ base "🩹 Injured"
321
+ end
322
+
323
+ value :status_description, health_status_description
324
+
325
+ value :can_level_up, (input.experience >= (input.level * 100))
326
+ value :next_level_exp, (input.level * 100)
327
+ end
328
+ end
329
+
330
+ module Enemy
331
+ extend Kumi::Schema
332
+
333
+ schema do
334
+ input do
335
+ string :name
336
+ string :type, domain: %w[goblin orc troll dragon skeleton mage]
337
+ integer :level, domain: 1..50
338
+ integer :health, domain: 0..2000
339
+ integer :max_health, domain: 1..2000
340
+ integer :attack, domain: 1..200
341
+ integer :defense, domain: 1..100
342
+ float :dodge_chance, domain: 0.0..0.5
343
+ array :abilities, elem: { type: :string }
344
+ hash :loot_table, key: { type: :string }, val: { type: :integer }
345
+ end
346
+
347
+ trait :alive, (input.health > 0)
348
+ trait :dead, (input.health <= 0)
349
+ trait :boss, (input.type == "dragon")
350
+ trait :weak, (input.health <= input.max_health * 0.25)
351
+ trait :dangerous, (input.attack >= 50)
352
+ trait :agile_enemy, (input.dodge_chance >= 0.2)
353
+
354
+ value :threat_level do
355
+ on boss, "šŸ’€ BOSS"
356
+ on dangerous, "⚔ Dangerous"
357
+ on weak, "🩹 Weakened"
358
+ base "āš”ļø Normal"
359
+ end
360
+
361
+ value :experience_reward, ((input.level * 25) + (fn(:size, input.abilities) * 10))
362
+
363
+ value :gold_reward do
364
+ on boss, ((input.level * 50) + 200)
365
+ on dangerous, ((input.level * 30) + 50)
366
+ base ((input.level * 20) + 10)
367
+ end
368
+
369
+ # Basic combat stats for UI - actual combat will use damage schemas
370
+ value :attack_damage, (input.attack + (input.level / 2))
371
+ value :defense_rating, (input.defense + (input.level / 3))
372
+ end
373
+ end
374
+
375
+ module CombatCalculation
376
+ extend Kumi::Schema
377
+
378
+ schema do
379
+ input do
380
+ integer :attacker_attack, domain: 0..500
381
+ integer :defender_defense, domain: 0..300
382
+ float :defender_dodge, domain: 0.0..1.0
383
+ boolean :critical_hit
384
+ string :attack_type, domain: %w[normal magic special]
385
+ array :attacker_abilities, elem: { type: :string }
386
+ integer :attacker_level, domain: 1..100
387
+ end
388
+
389
+ trait :hit_connects, (0.5 > input.defender_dodge)
390
+ trait :is_critical, (input.critical_hit == true)
391
+ trait :is_magic, (input.attack_type == "magic")
392
+ trait :is_special, (input.attack_type == "special")
393
+ trait :has_rage, fn(:include?, input.attacker_abilities, "rage")
394
+ trait :has_precision, fn(:include?, input.attacker_abilities, "precision")
395
+ trait :critical_precision, is_critical & has_precision
396
+
397
+ value :base_damage, fn(:max, [(input.attacker_attack - input.defender_defense), 1])
398
+
399
+ value :damage_multiplier do
400
+ on critical_precision, 3.0
401
+ on is_critical, 2.0
402
+ on has_rage, 1.5
403
+ on is_special, 1.3
404
+ base 1.0
405
+ end
406
+
407
+ value :final_damage do
408
+ on hit_connects, fn(:round, (base_damage * damage_multiplier))
409
+ base 0
410
+ end
411
+
412
+ trait :critical_hit_connects, hit_connects & is_critical
413
+ trait :special_hit_connects, hit_connects & is_special
414
+
415
+ value :attack_result do
416
+ on critical_hit_connects, "šŸ’„ CRITICAL HIT!"
417
+ on special_hit_connects, "✨ Special Attack!"
418
+ on hit_connects, "āš”ļø Hit!"
419
+ base "šŸ’Ø Miss!"
420
+ end
421
+ end
422
+ end
423
+
424
+ module GameState
425
+ extend Kumi::Schema
426
+
427
+ schema do
428
+ input do
429
+ integer :turn, domain: 1..Float::INFINITY
430
+ string :phase, domain: %w[exploration combat victory defeat menu]
431
+ boolean :player_alive
432
+ boolean :enemy_alive
433
+ array :combat_log, elem: { type: :string }
434
+ hash :flags, key: { type: :string }, val: { type: :boolean }
435
+ integer :enemies_defeated, domain: 0..Float::INFINITY
436
+ integer :gold, domain: 0..Float::INFINITY
437
+ end
438
+
439
+ trait :in_combat, (input.phase == "combat")
440
+ trait :exploring, (input.phase == "exploration")
441
+ trait :game_over, (input.phase == "defeat")
442
+ trait :victorious, (input.phase == "victory")
443
+ trait :both_alive, input.player_alive & input.enemy_alive
444
+ trait :combat_ongoing, in_combat & both_alive
445
+
446
+ value :turn_description do
447
+ on game_over, "šŸ’€ GAME OVER"
448
+ on victorious, "šŸŽ‰ VICTORY!"
449
+ on combat_ongoing, "āš”ļø Combat"
450
+ on exploring, "šŸ—ŗļø Exploring..."
451
+ base "šŸ“‹ Main Menu"
452
+ end
453
+
454
+ value :can_flee, in_combat & input.player_alive
455
+ value :can_attack, combat_ongoing
456
+ value :can_explore, exploring & input.player_alive
457
+
458
+ value :progress_score, ((input.enemies_defeated * 100) + input.gold)
459
+ end
460
+ end
461
+
462
+ class SimpleGame
463
+ def initialize
464
+ @player_data = {
465
+ name: "Hero",
466
+ level: 1,
467
+ health: 100,
468
+ max_health: 100,
469
+ mana: 50,
470
+ max_mana: 50,
471
+ strength: 12,
472
+ defense: 10,
473
+ agility: 8,
474
+ experience: 0,
475
+ weapon: "sword",
476
+ inventory: ["potion", "bread"],
477
+ stats: { "kills" => 0, "damage_dealt" => 0 },
478
+ equipment: {
479
+ "weapon_name" => "Iron Sword",
480
+ "weapon_type" => "sword",
481
+ "weapon_damage" => 12,
482
+ "armor_name" => "Leather Vest",
483
+ "armor_type" => "leather",
484
+ "armor_defense" => 5,
485
+ "accessory_name" => "none",
486
+ "accessory_type" => "none",
487
+ "accessory_bonus" => 0
488
+ },
489
+ status_effects: [],
490
+ poison_turns: 0,
491
+ blessing_turns: 0,
492
+ rage_turns: 0,
493
+ shield_turns: 0
494
+ }
495
+
496
+ @game_state = {
497
+ turn: 1,
498
+ phase: "menu",
499
+ player_alive: true,
500
+ enemy_alive: false,
501
+ combat_log: [],
502
+ flags: { "first_combat" => true },
503
+ enemies_defeated: 0,
504
+ gold: 50
505
+ }
506
+
507
+ @current_enemy = nil
508
+ end
509
+
510
+ def start
511
+ puts "šŸŽ® Welcome to Kumi RPG!"
512
+ puts "=" * 40
513
+
514
+ loop do
515
+ display_status
516
+ handle_phase
517
+ break if @game_state[:phase] == "defeat"
518
+ end
519
+ end
520
+
521
+ private
522
+
523
+ def display_status
524
+ puts "==" * 20
525
+
526
+ player = PlayerEntity.from(@player_data)
527
+ game = GameState.from(@game_state)
528
+
529
+ puts "\n#{game[:turn_description]}"
530
+ puts "Player: #{@player_data[:name]} (Lvl #{@player_data[:level]}) #{player[:status_description]}"
531
+ puts "Health: #{@player_data[:health]}/#{@player_data[:max_health]} (#{player[:health_percentage].round(1)}%)"
532
+ puts "Attack: #{player[:total_attack]} | Defense: #{player[:defense_rating].round(1)}"
533
+ puts "Equipment: #{get_equipment_display}"
534
+ puts "Gold: #{@game_state[:gold]} | Score: #{game[:progress_score]}"
535
+
536
+ if @current_enemy
537
+ enemy = Enemy.from(@current_enemy)
538
+ puts "\nCurrent Enemy:"
539
+ puts "Enemy: #{@current_enemy[:name]} (Lvl #{@current_enemy[:level]}) #{enemy[:threat_level]}"
540
+ puts "Enemy Health: #{@current_enemy[:health]}/#{@current_enemy[:max_health]}"
541
+ end
542
+ end
543
+
544
+ def handle_phase
545
+ case @game_state[:phase]
546
+ when "menu"
547
+ handle_menu
548
+ when "exploration"
549
+ handle_exploration
550
+ when "combat"
551
+ handle_combat
552
+ when "victory"
553
+ handle_victory
554
+ when "defeat"
555
+ puts "šŸ’€ Game Over! Final Score: #{GameState.from(@game_state)[:progress_score]}"
556
+ end
557
+ end
558
+
559
+ def handle_menu
560
+ puts "\nChoose action:"
561
+ puts "1. Start exploring"
562
+ puts "2. View character"
563
+ puts "3. Manage equipment"
564
+ puts "4. Quit"
565
+
566
+ action = simulate_user_input(["1", "2", "3", "4"])
567
+
568
+ case action
569
+ when "1"
570
+ @game_state[:phase] = "exploration"
571
+ when "2"
572
+ show_character_details
573
+ when "3"
574
+ manage_equipment
575
+ when "4"
576
+ @game_state[:phase] = "defeat"
577
+ end
578
+ end
579
+
580
+ def handle_exploration
581
+ puts "\nExploring the dungeon..."
582
+ puts "You venture deeper into the dark corridors..."
583
+
584
+ if rand < 0.7
585
+ encounter_enemy
586
+ # Don't return to menu - stay in combat!
587
+ else
588
+ find_treasure
589
+ puts "\n[ACTION COMPLETE - Returning to menu...]"
590
+ sleep(1)
591
+ @game_state[:phase] = "menu"
592
+ end
593
+ end
594
+
595
+ def encounter_enemy
596
+ enemy_types = [
597
+ { name: "Goblin Scout", type: "goblin", level: rand(1..3), health: 30, max_health: 30, attack: 8, defense: 3, dodge_chance: 0.1, abilities: [], loot_table: { "gold" => 15 } },
598
+ { name: "Orc Warrior", type: "orc", level: rand(2..5), health: 60, max_health: 60, attack: 15, defense: 8, dodge_chance: 0.05, abilities: ["rage"], loot_table: { "gold" => 30 } },
599
+ { name: "Skeleton Mage", type: "skeleton", level: rand(3..6), health: 40, max_health: 40, attack: 20, defense: 5, dodge_chance: 0.15, abilities: ["magic_missile"], loot_table: { "gold" => 25 } }
600
+ ]
601
+
602
+ @current_enemy = enemy_types.sample
603
+ @current_enemy[:health] = @current_enemy[:max_health]
604
+ @game_state[:phase] = "combat"
605
+ @game_state[:enemy_alive] = true
606
+
607
+ puts "šŸ’€ You encounter a #{@current_enemy[:name]}!"
608
+ puts "\n[Press Enter to enter combat]"
609
+ simulate_user_input([""])
610
+ end
611
+
612
+ def find_treasure
613
+ if rand < 0.3 && !@player_data[:inventory].include?("upgrade_token")
614
+ # Found equipment upgrade
615
+ @player_data[:inventory] << "upgrade_token"
616
+ puts "✨ You found an upgrade token! Visit equipment menu to enhance your gear."
617
+ else
618
+ # Found gold
619
+ treasure = rand(10..30)
620
+ @game_state[:gold] += treasure
621
+ puts "šŸ’° You found #{treasure} gold!"
622
+ end
623
+
624
+ puts "\n[TREASURE FOUND - Press Enter to continue exploring or return to menu]"
625
+ simulate_user_input([""])
626
+ end
627
+
628
+ def handle_combat
629
+ game = GameState.from(@game_state)
630
+
631
+ return unless game[:combat_ongoing]
632
+
633
+ puts "\nCombat options:"
634
+ puts "1. Attack"
635
+ puts "2. Flee"
636
+ puts "3. Use Potion (if available)" if @player_data[:inventory].include?("potion")
637
+
638
+ options = ["1", "2"]
639
+ options << "3" if @player_data[:inventory].include?("potion")
640
+
641
+ action = simulate_user_input(options)
642
+
643
+ case action
644
+ when "1"
645
+ player_attack
646
+ when "2"
647
+ attempt_flee
648
+ when "3"
649
+ use_potion if @player_data[:inventory].include?("potion")
650
+ end
651
+
652
+ enemy_attack if @game_state[:enemy_alive] && @game_state[:player_alive]
653
+ check_combat_end
654
+ @game_state[:turn] += 1
655
+ end
656
+
657
+ def player_attack
658
+ player = PlayerEntity.from(@player_data)
659
+
660
+ # Calculate player's attack damage using DamageCalculation schema
661
+ damage_calc_data = {
662
+ base_attack: 10,
663
+ strength: @player_data[:strength],
664
+ level: @player_data[:level],
665
+ weapon_damage: @player_data[:equipment]["weapon_damage"],
666
+ weapon_bonus: player[:base_weapon_bonus],
667
+ weapon_type: @player_data[:weapon],
668
+ critical_hit: rand < 0.1,
669
+ attack_type: "normal",
670
+ abilities: [],
671
+ status_effects: @player_data[:status_effects],
672
+ rage_turns: @player_data[:rage_turns],
673
+ poison_turns: @player_data[:poison_turns],
674
+ blessing_turns: @player_data[:blessing_turns]
675
+ }
676
+
677
+ damage_calc = DamageCalculation.from(damage_calc_data)
678
+
679
+ # Calculate enemy's damage reduction using DamageReduction schema
680
+ damage_reduction_data = {
681
+ base_defense: 5,
682
+ defense_stat: @current_enemy[:defense],
683
+ agility: 8, # Default enemy agility
684
+ level: @current_enemy[:level],
685
+ armor_defense: 0,
686
+ armor_bonus: 0,
687
+ armor_type: "none",
688
+ incoming_damage: damage_calc[:total_damage],
689
+ damage_type: "normal",
690
+ status_effects: [],
691
+ shield_turns: 0,
692
+ blessing_turns: 0,
693
+ poison_turns: 0
694
+ }
695
+
696
+ damage_reduction = DamageReduction.from(damage_reduction_data)
697
+
698
+ # Apply dodge chance
699
+ if rand > @current_enemy[:dodge_chance]
700
+ final_damage = damage_reduction[:damage_after_defense]
701
+ @current_enemy[:health] = [@current_enemy[:health] - final_damage, 0].max
702
+ @player_data[:stats]["damage_dealt"] += final_damage
703
+
704
+ puts "#{damage_calc[:damage_description]} You deal #{final_damage} damage! #{damage_reduction[:defense_description]}"
705
+ else
706
+ puts "šŸ’Ø Miss! The enemy dodged your attack!"
707
+ end
708
+
709
+ @game_state[:enemy_alive] = @current_enemy[:health] > 0
710
+ end
711
+
712
+ def enemy_attack
713
+ player = PlayerEntity.from(@player_data)
714
+
715
+ # Calculate enemy's attack damage using DamageCalculation schema
716
+ damage_calc_data = {
717
+ base_attack: @current_enemy[:attack],
718
+ strength: 12, # Default enemy strength
719
+ level: @current_enemy[:level],
720
+ weapon_damage: 0,
721
+ weapon_bonus: 5,
722
+ weapon_type: "fists",
723
+ critical_hit: rand < 0.05,
724
+ attack_type: @current_enemy[:abilities].include?("magic_missile") ? "magic" : "normal",
725
+ abilities: @current_enemy[:abilities],
726
+ status_effects: [],
727
+ rage_turns: 0,
728
+ poison_turns: 0,
729
+ blessing_turns: 0
730
+ }
731
+
732
+ damage_calc = DamageCalculation.from(damage_calc_data)
733
+
734
+ # Calculate player's damage reduction using DamageReduction schema
735
+ damage_reduction_data = {
736
+ base_defense: 5,
737
+ defense_stat: @player_data[:defense],
738
+ agility: @player_data[:agility],
739
+ level: @player_data[:level],
740
+ armor_defense: @player_data[:equipment]["armor_defense"],
741
+ armor_bonus: player[:equipment_defense_bonus],
742
+ armor_type: @player_data[:equipment]["armor_type"],
743
+ incoming_damage: damage_calc[:total_damage],
744
+ damage_type: damage_calc_data[:attack_type],
745
+ status_effects: @player_data[:status_effects],
746
+ shield_turns: @player_data[:shield_turns],
747
+ blessing_turns: @player_data[:blessing_turns],
748
+ poison_turns: @player_data[:poison_turns]
749
+ }
750
+
751
+ damage_reduction = DamageReduction.from(damage_reduction_data)
752
+
753
+ # Apply dodge chance
754
+ if rand > damage_reduction[:dodge_chance]
755
+ final_damage = damage_reduction[:damage_after_defense]
756
+ @player_data[:health] = [@player_data[:health] - final_damage, 0].max
757
+
758
+ puts "#{@current_enemy[:name]} attacks! #{damage_calc[:damage_description]} You take #{final_damage} damage! #{damage_reduction[:defense_description]}"
759
+ else
760
+ puts "#{@current_enemy[:name]} attacks! šŸ’Ø You dodged the attack!"
761
+ end
762
+
763
+ @game_state[:player_alive] = @player_data[:health] > 0
764
+ end
765
+
766
+ def attempt_flee
767
+ if rand < 0.7
768
+ puts "šŸ’Ø You successfully flee from combat!"
769
+ puts "\n[FLED FROM COMBAT - Press Enter to return to menu]"
770
+ simulate_user_input([""])
771
+ @game_state[:phase] = "menu"
772
+ @current_enemy = nil
773
+ @game_state[:enemy_alive] = false
774
+ else
775
+ puts "āŒ Failed to flee! The enemy blocks your escape!"
776
+ puts "[CONTINUE FIGHTING]"
777
+ end
778
+ end
779
+
780
+ def use_potion
781
+ @player_data[:inventory].delete("potion")
782
+ heal_amount = 50
783
+ @player_data[:health] = [@player_data[:health] + heal_amount, @player_data[:max_health]].min
784
+ puts "🧪 You drink a potion and recover #{heal_amount} health!"
785
+ end
786
+
787
+ def check_combat_end
788
+ if !@game_state[:enemy_alive] && @current_enemy && @current_enemy[:health] == 0
789
+ # Enemy was defeated (not fled from)
790
+ enemy = Enemy.from(@current_enemy)
791
+ exp_gain = enemy[:experience_reward]
792
+ gold_gain = enemy[:gold_reward]
793
+
794
+ @player_data[:experience] += exp_gain
795
+ @game_state[:gold] += gold_gain
796
+ @game_state[:enemies_defeated] += 1
797
+ @player_data[:stats]["kills"] += 1
798
+
799
+ puts "šŸŽ‰ Victory! Gained #{exp_gain} XP and #{gold_gain} gold!"
800
+
801
+ check_level_up
802
+
803
+ puts "\n[COMBAT VICTORY - Press Enter to continue exploring]"
804
+ simulate_user_input([""])
805
+ @game_state[:phase] = "exploration"
806
+ @current_enemy = nil
807
+ elsif !@game_state[:player_alive]
808
+ @game_state[:phase] = "defeat"
809
+ end
810
+ end
811
+
812
+ def check_level_up
813
+ player = PlayerEntity.from(@player_data)
814
+
815
+ if player[:can_level_up]
816
+ @player_data[:level] += 1
817
+ @player_data[:max_health] += 20
818
+ @player_data[:health] = @player_data[:max_health]
819
+ @player_data[:strength] += 2
820
+ @player_data[:defense] += 1
821
+ @player_data[:agility] += 1
822
+ @player_data[:experience] = 0
823
+
824
+ puts "⭐ LEVEL UP! You are now level #{@player_data[:level]}!"
825
+ end
826
+ end
827
+
828
+ def handle_victory
829
+ puts "šŸŽ‰ Congratulations! You've mastered the dungeon!"
830
+ @game_state[:phase] = "defeat"
831
+ end
832
+
833
+ def show_character_details
834
+ player = PlayerEntity.from(@player_data)
835
+ puts "\nšŸ“Š Character Details:"
836
+ puts "Name: #{@player_data[:name]}"
837
+ puts "Level: #{@player_data[:level]} (#{@player_data[:experience]}/#{player[:next_level_exp]} XP)"
838
+ puts "Health: #{@player_data[:health]}/#{@player_data[:max_health]}"
839
+ puts "Mana: #{@player_data[:mana]}/#{@player_data[:max_mana]}"
840
+ puts "Stats - STR: #{@player_data[:strength]}, DEF: #{@player_data[:defense]}, AGI: #{@player_data[:agility]}"
841
+ puts "Combat - Attack: #{player[:total_attack]}, Defense: #{player[:defense_rating].round(1)}, Dodge: #{(player[:dodge_chance] * 100).round(1)}%"
842
+ puts "Weapon: #{@player_data[:weapon]} (+#{player[:base_weapon_bonus]} base damage)"
843
+ puts "Equipment: #{get_equipment_display}"
844
+ puts "Inventory: #{@player_data[:inventory].join(', ')}"
845
+ puts "Kills: #{@player_data[:stats]['kills']}, Damage Dealt: #{@player_data[:stats]['damage_dealt']}"
846
+
847
+ puts "\nPress Enter to continue..."
848
+ simulate_user_input([""])
849
+ end
850
+
851
+ def get_equipment_display
852
+ equipment = Equipment.from({
853
+ weapon_name: @player_data[:equipment]["weapon_name"],
854
+ weapon_type: @player_data[:equipment]["weapon_type"],
855
+ weapon_damage: @player_data[:equipment]["weapon_damage"],
856
+ armor_name: @player_data[:equipment]["armor_name"],
857
+ armor_type: @player_data[:equipment]["armor_type"],
858
+ armor_defense: @player_data[:equipment]["armor_defense"],
859
+ accessory_name: @player_data[:equipment]["accessory_name"] || "none",
860
+ accessory_type: @player_data[:equipment]["accessory_type"] || "none",
861
+ accessory_bonus: @player_data[:equipment]["accessory_bonus"] || 0
862
+ })
863
+ equipment[:equipment_description]
864
+ end
865
+
866
+ def get_equipment_damage(weapon_type)
867
+ case weapon_type
868
+ when "sword" then 12
869
+ when "dagger" then 8
870
+ when "staff" then 6
871
+ when "bow" then 10
872
+ else 3
873
+ end
874
+ end
875
+
876
+ def get_equipment_defense(armor_type)
877
+ case armor_type
878
+ when "leather" then 5
879
+ when "chainmail" then 10
880
+ when "plate" then 15
881
+ when "robe" then 3
882
+ else 0
883
+ end
884
+ end
885
+
886
+ def manage_equipment
887
+ puts "\nšŸ›”ļø Equipment Management"
888
+ equipment = Equipment.from({
889
+ weapon_name: @player_data[:equipment]["weapon_name"],
890
+ weapon_type: @player_data[:equipment]["weapon_type"],
891
+ weapon_damage: @player_data[:equipment]["weapon_damage"],
892
+ armor_name: @player_data[:equipment]["armor_name"],
893
+ armor_type: @player_data[:equipment]["armor_type"],
894
+ armor_defense: @player_data[:equipment]["armor_defense"],
895
+ accessory_name: @player_data[:equipment]["accessory_name"] || "none",
896
+ accessory_type: @player_data[:equipment]["accessory_type"] || "none",
897
+ accessory_bonus: @player_data[:equipment]["accessory_bonus"] || 0
898
+ })
899
+
900
+ puts "Current Equipment: #{equipment[:equipment_description]}"
901
+ puts "Total Weapon Damage: #{equipment[:total_weapon_damage]}"
902
+ puts "Total Armor Defense: #{equipment[:total_armor_defense]}"
903
+ puts "Agility Modifier: #{equipment[:equipment_agility_modifier]}"
904
+ puts "Strength Modifier: #{equipment[:equipment_strength_modifier]}"
905
+
906
+ if @player_data[:inventory].include?("upgrade_token")
907
+ puts "\nYou have an upgrade token! Choose equipment to upgrade:"
908
+ puts "1. Upgrade weapon"
909
+ puts "2. Upgrade armor"
910
+ puts "3. Go back"
911
+
912
+ action = simulate_user_input(["1", "2", "3"])
913
+
914
+ case action
915
+ when "1"
916
+ upgrade_weapon
917
+ when "2"
918
+ upgrade_armor
919
+ when "3"
920
+ puts "\n[EQUIPMENT MENU CLOSED - Returning to main menu]"
921
+ end
922
+ else
923
+ puts "\nNo upgrade tokens available. Find them while exploring!"
924
+ puts "\n[EQUIPMENT MENU - Press Enter to return to main menu]"
925
+ simulate_user_input([""])
926
+ end
927
+ end
928
+
929
+ def upgrade_weapon
930
+ @player_data[:inventory].delete("upgrade_token")
931
+
932
+ case @player_data[:equipment]["weapon_type"]
933
+ when "sword"
934
+ @player_data[:equipment]["weapon_name"] = "Enchanted Sword"
935
+ @player_data[:equipment]["weapon_damage"] += 5
936
+ puts "āš”ļø Your Iron Sword has been upgraded to an Enchanted Sword! (+5 damage)"
937
+ when "dagger"
938
+ @player_data[:equipment]["weapon_name"] = "Poisoned Dagger"
939
+ @player_data[:equipment]["weapon_damage"] += 4
940
+ puts "šŸ—”ļø Your Dagger has been upgraded to a Poisoned Dagger! (+4 damage)"
941
+ when "staff"
942
+ @player_data[:equipment]["weapon_name"] = "Crystal Staff"
943
+ @player_data[:equipment]["weapon_damage"] += 6
944
+ puts "šŸŖ„ Your Staff has been upgraded to a Crystal Staff! (+6 damage)"
945
+ when "bow"
946
+ @player_data[:equipment]["weapon_name"] = "Elven Bow"
947
+ @player_data[:equipment]["weapon_damage"] += 4
948
+ puts "šŸ¹ Your Bow has been upgraded to an Elven Bow! (+4 damage)"
949
+ end
950
+
951
+ puts "\n[WEAPON UPGRADED - Press Enter to continue...]"
952
+ simulate_user_input([""])
953
+ end
954
+
955
+ def upgrade_armor
956
+ @player_data[:inventory].delete("upgrade_token")
957
+
958
+ case @player_data[:equipment]["armor_type"]
959
+ when "leather"
960
+ @player_data[:equipment]["armor_name"] = "Studded Leather"
961
+ @player_data[:equipment]["armor_type"] = "chainmail"
962
+ @player_data[:equipment]["armor_defense"] += 3
963
+ puts "šŸ›”ļø Your Leather Vest has been upgraded to Studded Leather! (+3 defense)"
964
+ when "chainmail"
965
+ @player_data[:equipment]["armor_name"] = "Plate Mail"
966
+ @player_data[:equipment]["armor_type"] = "plate"
967
+ @player_data[:equipment]["armor_defense"] += 5
968
+ puts "āš”ļø Your Chainmail has been upgraded to Plate Mail! (+5 defense)"
969
+ when "none"
970
+ @player_data[:equipment]["armor_name"] = "Leather Vest"
971
+ @player_data[:equipment]["armor_type"] = "leather"
972
+ @player_data[:equipment]["armor_defense"] = 5
973
+ puts "šŸ›”ļø You now have a Leather Vest! (+5 defense)"
974
+ else
975
+ puts "Your armor is already at maximum level!"
976
+ end
977
+
978
+ puts "\n[ARMOR UPGRADED - Press Enter to continue...]"
979
+ simulate_user_input([""])
980
+ end
981
+
982
+ def simulate_user_input(valid_options)
983
+ puts "\n> (Simulating user input from: #{valid_options.join(', ')})"
984
+ sleep(0.3)
985
+
986
+ # Real user input
987
+ choice = gets&.chomp&.strip || valid_options.first
988
+
989
+
990
+ # # Show equipment manageme3
991
+
992
+ puts "> #{choice}"
993
+ choice
994
+ end
995
+ end
996
+
997
+ if __FILE__ == $0
998
+ game = SimpleGame.new
999
+ game.start
1000
+ end