state_machines-mermaid 0.1.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.
@@ -0,0 +1,261 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'character'
4
+
5
+ # Mage with spell casting and specialization states
6
+ class Mage < Character
7
+ attr_accessor :spell_power, :focus, :school_of_magic, :mana_pool, :spell_book, :affinities, :masteries, :traits,
8
+ :trials_completed, :current_target, :channel_start_time, :cooldown_end_time
9
+
10
+ def initialize
11
+ super
12
+ @spell_power = 50
13
+ @focus = 100
14
+ @mana_pool = 200
15
+ @spell_book = []
16
+ @known_spells = {}
17
+ end
18
+
19
+ # Mental state for concentration
20
+ state_machine :concentration, initial: :focused do
21
+ state :focused do
22
+ def spell_critical_chance
23
+ 0.2
24
+ end
25
+ end
26
+
27
+ state :distracted do
28
+ def spell_failure_chance
29
+ 0.3
30
+ end
31
+ end
32
+
33
+ state :deep_meditation do
34
+ def mana_regeneration_rate
35
+ 5.0
36
+ end
37
+
38
+ def immune_to_interruption?
39
+ true
40
+ end
41
+ end
42
+
43
+ state :interrupted do
44
+ def can_cast?
45
+ false
46
+ end
47
+ end
48
+
49
+ event :meditate do
50
+ transition focused: :deep_meditation, if: -> { mana < 50 && safe_to_meditate? }
51
+ transition distracted: :focused, if: :regain_composure?
52
+ end
53
+
54
+ event :disturb do
55
+ transition %i[focused deep_meditation] => :distracted, unless: :iron_will?
56
+ end
57
+
58
+ event :break_concentration do
59
+ transition any - :interrupted => :interrupted
60
+ end
61
+
62
+ event :refocus do
63
+ transition %i[distracted interrupted] => :focused, if: :concentration_check?
64
+ end
65
+
66
+ after_transition to: :deep_meditation, &:begin_mana_regeneration
67
+ end
68
+
69
+ # Spell school specialization with complex hierarchy
70
+ state_machine :spell_school, initial: :apprentice do
71
+ # Apprentice level
72
+ state :apprentice
73
+
74
+ # Fire magic tree
75
+ state :fire do
76
+ def elemental_affinity
77
+ :fire
78
+ end
79
+ end
80
+ state :ember
81
+ state :flame
82
+ state :inferno
83
+
84
+ # Ice magic tree
85
+ state :ice do
86
+ def elemental_affinity
87
+ :ice
88
+ end
89
+ end
90
+ state :frost
91
+ state :freeze
92
+ state :blizzard
93
+
94
+ # Arcane magic tree
95
+ state :arcane do
96
+ def elemental_affinity
97
+ :arcane
98
+ end
99
+ end
100
+ state :missile
101
+ state :explosion
102
+ state :singularity
103
+
104
+ # Master of multiple schools
105
+ state :archmage do
106
+ def can_dual_cast?
107
+ true
108
+ end
109
+ end
110
+
111
+ # Fire progression
112
+ event :study_fire do
113
+ transition apprentice: :ember, if: :has_fire_affinity?
114
+ transition ember: :flame, if: -> { spell_power > 75 && knows_spell?(:fireball) }
115
+ transition flame: :inferno, if: -> { spell_power > 150 && fire_mastery_complete? }
116
+ end
117
+
118
+ # Ice progression
119
+ event :study_ice do
120
+ transition apprentice: :frost, if: :has_ice_affinity?
121
+ transition frost: :freeze, if: -> { spell_power > 75 && knows_spell?(:ice_lance) }
122
+ transition freeze: :blizzard, if: -> { spell_power > 150 && ice_mastery_complete? }
123
+ end
124
+
125
+ # Arcane progression
126
+ event :study_arcane do
127
+ transition apprentice: :missile, if: :has_arcane_affinity?
128
+ transition missile: :explosion, if: -> { spell_power > 75 && knows_spell?(:arcane_blast) }
129
+ transition explosion: :singularity, if: -> { spell_power > 150 && arcane_mastery_complete? }
130
+ end
131
+
132
+ # Become archmage
133
+ event :transcend do
134
+ transition %i[inferno blizzard singularity] => :archmage, if: :worthy_of_archmage?
135
+ end
136
+
137
+ before_transition any => any do |mage, transition|
138
+ mage.update_spell_list(transition.to)
139
+ end
140
+ end
141
+
142
+ # Spell casting state
143
+ state_machine :casting_state, initial: :ready do
144
+ state :ready
145
+ state :preparing
146
+ state :channeling
147
+ state :releasing
148
+ state :cooldown
149
+
150
+ event :begin_cast do
151
+ transition ready: :preparing, if: :has_target?
152
+ end
153
+
154
+ event :channel do
155
+ transition preparing: :channeling, if: :channel_started?
156
+ end
157
+
158
+ event :release do
159
+ transition channeling: :releasing
160
+ end
161
+
162
+ event :complete do
163
+ transition releasing: :cooldown
164
+ end
165
+
166
+ event :reset do
167
+ transition cooldown: :ready, if: :cooldown_expired?
168
+ end
169
+
170
+ after_transition to: :releasing, &:cast_spell!
171
+ end
172
+
173
+ # Mage-specific methods
174
+ def safe_to_meditate?
175
+ !in_combat? && !enemies_nearby?
176
+ end
177
+
178
+ def regain_composure?
179
+ focus > 50
180
+ end
181
+
182
+ def iron_will?
183
+ has_trait?(:iron_will) || focus > 150
184
+ end
185
+
186
+ def concentration_check?
187
+ rand(100) < focus
188
+ end
189
+
190
+ def begin_mana_regeneration
191
+ @regenerating_mana = true
192
+ end
193
+
194
+ def has_fire_affinity?
195
+ @affinities&.include?(:fire)
196
+ end
197
+
198
+ def has_ice_affinity?
199
+ @affinities&.include?(:ice)
200
+ end
201
+
202
+ def has_arcane_affinity?
203
+ @affinities&.include?(:arcane)
204
+ end
205
+
206
+ def knows_spell?(spell_name)
207
+ @known_spells[spell_name] == true
208
+ end
209
+
210
+ def fire_mastery_complete?
211
+ @masteries && @masteries[:fire] >= 100
212
+ end
213
+
214
+ def ice_mastery_complete?
215
+ @masteries && @masteries[:ice] >= 100
216
+ end
217
+
218
+ def arcane_mastery_complete?
219
+ @masteries && @masteries[:arcane] >= 100
220
+ end
221
+
222
+ def worthy_of_archmage?
223
+ level >= 50 && completed_all_trials?
224
+ end
225
+
226
+ def update_spell_list(school)
227
+ # Update available spells based on school
228
+ end
229
+
230
+ def has_target?
231
+ @current_target != nil
232
+ end
233
+
234
+ def channel_started?
235
+ @channel_start_time != nil
236
+ end
237
+
238
+ def cooldown_expired?
239
+ @cooldown_end_time && Time.now > @cooldown_end_time
240
+ end
241
+
242
+ def cast_spell!
243
+ # Execute the spell
244
+ end
245
+
246
+ def in_combat?
247
+ status == 'combat'
248
+ end
249
+
250
+ def enemies_nearby?
251
+ false
252
+ end
253
+
254
+ def has_trait?(trait)
255
+ @traits&.include?(trait)
256
+ end
257
+
258
+ def completed_all_trials?
259
+ @trials_completed && @trials_completed.size >= 5
260
+ end
261
+ end
@@ -0,0 +1,244 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Regiment with formation and morale states
4
+ class Regiment
5
+ attr_accessor :morale, :soldiers, :commander, :supplies, :casualties, :battle_experience, :threats, :enemy_positions,
6
+ :immediate_threats, :enemy_activity_level, :defending_critical_position, :battle_start_time, :pike_wall_ready
7
+
8
+ def initialize
9
+ @morale = 100
10
+ @soldiers = 100
11
+ @supplies = 100
12
+ @casualties = 0
13
+ @battle_experience = 0
14
+ end
15
+
16
+ state_machine :formation, initial: :column do
17
+ state :column do
18
+ def movement_speed
19
+ 2.0
20
+ end
21
+
22
+ def defense_rating
23
+ 1.0
24
+ end
25
+ end
26
+
27
+ state :line do
28
+ def firepower
29
+ 3.0
30
+ end
31
+
32
+ def defense_rating
33
+ 1.5
34
+ end
35
+ end
36
+
37
+ state :square do
38
+ def cavalry_defense
39
+ 5.0
40
+ end
41
+
42
+ def movement_speed
43
+ 0.5
44
+ end
45
+ end
46
+
47
+ state :wedge do
48
+ def charge_bonus
49
+ 4.0
50
+ end
51
+
52
+ def breakthrough_chance
53
+ 0.7
54
+ end
55
+ end
56
+
57
+ state :scattered do
58
+ def cohesion
59
+ 0.1
60
+ end
61
+
62
+ def rally_difficulty
63
+ 10
64
+ end
65
+ end
66
+
67
+ state :circle do
68
+ def all_around_defense
69
+ 3.0
70
+ end
71
+
72
+ def surrounded_bonus
73
+ 2.0
74
+ end
75
+ end
76
+
77
+ event :deploy do
78
+ transition column: :line, if: :sufficient_space?
79
+ end
80
+
81
+ event :form_square do
82
+ transition %i[column line] => :square, if: :cavalry_threat?
83
+ transition wedge: :square, if: :emergency_defense?
84
+ end
85
+
86
+ event :form_wedge do
87
+ transition %i[column line] => :wedge, if: -> { morale > 70 && commander_present? }
88
+ end
89
+
90
+ event :form_circle do
91
+ transition %i[line square] => :circle, if: :surrounded?
92
+ end
93
+
94
+ event :break_formation do
95
+ transition any - :scattered => :scattered, if: -> { morale < 20 || casualties > 50 }
96
+ end
97
+
98
+ event :rally do
99
+ transition scattered: :column, if: -> { commander_alive? && morale > 40 }
100
+ end
101
+
102
+ event :reform do
103
+ transition %i[square wedge circle] => :line, unless: :under_immediate_threat?
104
+ end
105
+
106
+ before_transition any => :scattered, &:apply_panic_casualties
107
+
108
+ after_transition to: :square, &:prepare_pikes
109
+ end
110
+
111
+ # Supply state
112
+ state_machine :supply_state, initial: :supplied do
113
+ state :supplied do
114
+ def combat_effectiveness
115
+ 1.0
116
+ end
117
+ end
118
+
119
+ state :low_supplies do
120
+ def combat_effectiveness
121
+ 0.8
122
+ end
123
+ end
124
+
125
+ state :foraging do
126
+ def vulnerable_to_ambush?
127
+ true
128
+ end
129
+ end
130
+
131
+ state :starving do
132
+ def combat_effectiveness
133
+ 0.4
134
+ end
135
+
136
+ def desertion_rate
137
+ 0.1
138
+ end
139
+ end
140
+
141
+ event :consume_supplies do
142
+ transition supplied: :low_supplies, if: -> { supplies < 30 }
143
+ transition low_supplies: :starving, if: -> { supplies <= 0 }
144
+ end
145
+
146
+ event :send_foragers do
147
+ transition %i[low_supplies starving] => :foraging, if: :safe_to_forage?
148
+ end
149
+
150
+ event :resupply do
151
+ transition %i[low_supplies foraging starving] => :supplied
152
+ end
153
+
154
+ after_transition on: :resupply, to: :supplied do |regiment|
155
+ regiment.supplies = 100
156
+ end
157
+
158
+ event :foraging_success do
159
+ transition foraging: :low_supplies
160
+ end
161
+
162
+ after_transition on: :foraging_success, to: :low_supplies do |regiment|
163
+ regiment.supplies += 20
164
+ end
165
+ end
166
+
167
+ # Battle readiness
168
+ state_machine :readiness, initial: :fresh do
169
+ state :fresh
170
+ state :engaged
171
+ state :exhausted
172
+ state :victorious
173
+ state :routed
174
+
175
+ event :enter_battle do
176
+ transition fresh: :engaged
177
+ transition exhausted: :engaged, if: :desperate_defense?
178
+ end
179
+
180
+ event :win_engagement do
181
+ transition engaged: :victorious, if: -> { morale > 60 && casualties < 30 }
182
+ end
183
+
184
+ event :lose_engagement do
185
+ transition engaged: :routed, if: -> { morale < 30 || casualties > 60 }
186
+ end
187
+
188
+ event :exhaust do
189
+ transition engaged: :exhausted, if: -> { battle_duration > 480 }
190
+ end
191
+
192
+ event :rest do
193
+ transition %i[exhausted victorious] => :fresh
194
+ end
195
+ end
196
+
197
+ def sufficient_space?
198
+ true # Check battlefield conditions
199
+ end
200
+
201
+ def cavalry_threat?
202
+ @threats&.include?(:cavalry)
203
+ end
204
+
205
+ def emergency_defense?
206
+ morale < 40 && surrounded?
207
+ end
208
+
209
+ def surrounded?
210
+ @enemy_positions && @enemy_positions.count >= 3
211
+ end
212
+
213
+ def commander_present?
214
+ commander&.alive? && commander.with_regiment?
215
+ end
216
+
217
+ def commander_alive?
218
+ commander&.alive?
219
+ end
220
+
221
+ def under_immediate_threat?
222
+ @immediate_threats && !@immediate_threats.empty?
223
+ end
224
+
225
+ def apply_panic_casualties
226
+ @casualties += soldiers * 0.1
227
+ end
228
+
229
+ def prepare_pikes
230
+ @pike_wall_ready = true
231
+ end
232
+
233
+ def safe_to_forage?
234
+ !surrounded? && @enemy_activity_level < 3
235
+ end
236
+
237
+ def desperate_defense?
238
+ @defending_critical_position
239
+ end
240
+
241
+ def battle_duration
242
+ @battle_start_time ? Time.now - @battle_start_time : 0
243
+ end
244
+ end
@@ -0,0 +1,167 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'character'
4
+
5
+ # Troll with regeneration mechanics
6
+ class Troll < Character
7
+ attr_accessor :regeneration_rate, :rage_level, :last_damage_type, :berserker_stacks, :last_fire_damage_time,
8
+ :recovery_started, :health_changes, :enemies_nearby
9
+
10
+ def initialize
11
+ super
12
+ @regeneration_rate = 5
13
+ @rage_level = 0
14
+ @berserker_stacks = 0
15
+ end
16
+
17
+ state_machine :regeneration, initial: :normal do
18
+ state :normal do
19
+ def regen_multiplier
20
+ 1.0
21
+ end
22
+ end
23
+
24
+ state :accelerated do
25
+ def regen_multiplier
26
+ 2.0
27
+ end
28
+ end
29
+
30
+ state :berserk do
31
+ def regen_multiplier
32
+ 3.0
33
+ end
34
+
35
+ def damage_taken_reduction
36
+ 0.5
37
+ end
38
+ end
39
+
40
+ state :suppressed do
41
+ def regen_multiplier
42
+ 0
43
+ end
44
+ end
45
+
46
+ state :dormant do
47
+ def regen_multiplier
48
+ 0.1
49
+ end
50
+
51
+ def appears_dead?
52
+ true
53
+ end
54
+ end
55
+
56
+ event :enrage do
57
+ transition normal: :accelerated, if: -> { health < 75 }
58
+ transition accelerated: :berserk, if: -> { rage_level > 75 && health < 50 }
59
+ end
60
+
61
+ event :take_fire_damage do
62
+ transition %i[normal accelerated berserk] => :suppressed
63
+ end
64
+
65
+ event :take_acid_damage do
66
+ transition %i[normal accelerated] => :suppressed
67
+ transition berserk: :accelerated # Berserk provides some resistance
68
+ end
69
+
70
+ event :cool_down do
71
+ transition suppressed: :normal, if: -> { time_since_fire_damage > 60 }
72
+ transition %i[accelerated berserk] => :normal, unless: -> { health < 50 }
73
+ end
74
+
75
+ event :play_dead do
76
+ transition %i[normal suppressed] => :dormant, if: -> { health < 10 }
77
+ end
78
+
79
+ event :rise_again do
80
+ transition dormant: :normal, if: -> { health > 25 }
81
+ transition dormant: :berserk, if: -> { rage_level > 90 }
82
+ end
83
+
84
+ after_transition any => :berserk do |troll|
85
+ troll.regeneration_rate = 20
86
+ troll.berserker_stacks = 5
87
+ end
88
+
89
+ after_transition any => :suppressed do |troll|
90
+ troll.regeneration_rate = 0
91
+ end
92
+
93
+ after_transition to: :dormant do |troll|
94
+ troll.regeneration_rate = 1
95
+ troll.begin_slow_recovery
96
+ end
97
+
98
+ around_transition any => any do |troll, _transition, block|
99
+ health_before = troll.health
100
+ block.call
101
+ health_after = troll.health
102
+ troll.track_health_change(health_before - health_after)
103
+ end
104
+ end
105
+
106
+ # Combat stance
107
+ state_machine :combat_stance, initial: :defensive do
108
+ state :defensive do
109
+ def armor_bonus
110
+ 5
111
+ end
112
+ end
113
+
114
+ state :aggressive do
115
+ def attack_bonus
116
+ 3
117
+ end
118
+ end
119
+
120
+ state :reckless do
121
+ def attack_bonus
122
+ 6
123
+ end
124
+
125
+ def armor_penalty
126
+ 3
127
+ end
128
+ end
129
+
130
+ event :change_stance do
131
+ transition defensive: :aggressive, if: :confident?
132
+ transition aggressive: :reckless, if: :bloodthirsty?
133
+ transition %i[aggressive reckless] => :defensive, if: :threatened?
134
+ end
135
+ end
136
+
137
+ def time_since_fire_damage
138
+ return Float::INFINITY unless @last_fire_damage_time
139
+
140
+ Time.now - @last_fire_damage_time
141
+ end
142
+
143
+ def begin_slow_recovery
144
+ @recovery_started = Time.now
145
+ end
146
+
147
+ def track_health_change(amount)
148
+ @health_changes ||= []
149
+ @health_changes << { amount: amount, time: Time.now }
150
+ end
151
+
152
+ def confident?
153
+ health > 75 && berserker_stacks.positive?
154
+ end
155
+
156
+ def bloodthirsty?
157
+ rage_level > 80 && health > 50
158
+ end
159
+
160
+ def threatened?
161
+ health < 30 || surrounded?
162
+ end
163
+
164
+ def surrounded?
165
+ @enemies_nearby && @enemies_nearby.count > 3
166
+ end
167
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'minitest/autorun'
4
+ require 'minitest/reporters'
5
+
6
+ require 'state_machines/test_helper'
7
+ require 'state_machines-mermaid'
8
+
9
+ StateMachines::Machine.ignore_method_conflicts = true
10
+
11
+ Dir[File.expand_path('support/models/**/*.rb', __dir__)].sort.each { |file| require file }
12
+
13
+ class Minitest::Test
14
+ include StateMachines::TestHelper
15
+ end
16
+
17
+ Minitest::Reporters.use!(Minitest::Reporters::ProgressReporter.new)