summaryrefslogtreecommitdiffstats
path: root/lib/colonial_twilight/fln_bot
diff options
context:
space:
mode:
Diffstat (limited to 'lib/colonial_twilight/fln_bot')
-rw-r--r--lib/colonial_twilight/fln_bot/fln_attack.rb67
-rw-r--r--lib/colonial_twilight/fln_bot/fln_bot_rules.rb363
-rw-r--r--lib/colonial_twilight/fln_bot/fln_extort.rb17
-rw-r--r--lib/colonial_twilight/fln_bot/fln_march.rb11
-rw-r--r--lib/colonial_twilight/fln_bot/fln_pass.rb9
-rw-r--r--lib/colonial_twilight/fln_bot/fln_rally.rb190
-rw-r--r--lib/colonial_twilight/fln_bot/fln_rules.rb94
-rw-r--r--lib/colonial_twilight/fln_bot/fln_subvert.rb50
-rw-r--r--lib/colonial_twilight/fln_bot/fln_terror.rb28
9 files changed, 829 insertions, 0 deletions
diff --git a/lib/colonial_twilight/fln_bot/fln_attack.rb b/lib/colonial_twilight/fln_bot/fln_attack.rb
new file mode 100644
index 0000000..bf461e7
--- /dev/null
+++ b/lib/colonial_twilight/fln_bot/fln_attack.rb
@@ -0,0 +1,67 @@
+# frozen_string_literal: true
+
+module ColonialTwilight
+ module FLNBotAttack
+ def attack
+ # return false if !available_resources.positive? && !extort
+
+ n = 2
+ ambush_cond = ->(s) { n.positive? && @turn.may_special_activity?(:ambush) && may_ambush_1_in?(s) }
+ cond = ->(s) { may_attack_1_in?(s) || ambush_cond.call(s) }
+ until (space = attack_priority(@board.search(&cond)).sample).nil?
+ break if !available_resources.positive? && !extort
+
+ _apply_attack(space, ambush_cond.call(space))
+ n -= 1
+ end
+
+ until (space = attack_priority(@board.search { |s| may_attack_2_in?(s) }).sample).nil?
+ break if !available_resources.positive? && !extort
+
+ _apply_attack(space, ambush_cond.call(space))
+ n -= 1
+ end
+
+ @turn.operation_done?
+ end
+
+ def _apply_attack(space, ambush)
+ apply_action ambush ? _ambush(space) : _attack(space)
+ end
+
+ def _ambush(space)
+ action = @turn.special_activity_in(:ambush, space, 1).activate(1)
+ casualties = _casualties(space, action, 1)
+ _attrition(action, casualties)
+ end
+
+ def _attack(space)
+ action = @turn.operation_in(:attack, space, 1).activate(space.fln_underground)
+ return action if (d = d6) > space.guerrillas
+
+ casualties = _casualties(space, action, 2)
+ _attrition(action, casualties)
+ action.transfer_from(place_from, :fln_underground) if d == 1
+ action
+ end
+
+ def _casualties(space, action, casualties)
+ num = 0
+ FLNAttackRules::CASUALTIES_PRIORITY.each do |sym|
+ next unless (n = space.send(sym)).positive?
+
+ casualties -= (n = (n > casualties ? casualties : n))
+ num += n
+ action.transfer_to(:casualties, sym, n)
+ action.shift(:commitment, -1) if sym == :gov_bases
+ break if casualties.zero?
+ end
+ num
+ end
+
+ def _attrition(action, casualties)
+ action.transfer_to(:available, :fln_active, (casualties + 1) / 2)
+ .transfer_to(:casualties, :fln_active, casualties / 2)
+ end
+ end
+end
diff --git a/lib/colonial_twilight/fln_bot/fln_bot_rules.rb b/lib/colonial_twilight/fln_bot/fln_bot_rules.rb
new file mode 100644
index 0000000..44148ee
--- /dev/null
+++ b/lib/colonial_twilight/fln_bot/fln_bot_rules.rb
@@ -0,0 +1,363 @@
+# frozen_string_literal: true
+
+module ColonialTwilight
+ module FLNBotRules
+ def dbg(msg, ret)
+ return if @debug.zero?
+
+ case @debug
+ when 1 then puts " #{msg}" if ret
+ else puts " #{msg} : #{ret ? 'YES' : 'NO'}"
+ end
+ end
+
+ def pass?(board = @board)
+ # if resources = 0 && Op Limited as only choice
+ r = board.fln_resources.zero? && limited_op_only?
+ dbg 'PASS', r
+ r
+ end
+
+ def terror1?(board = @board)
+ # if no FLN base is (pop 0 && 0 FLN underground or pop 1+ && 1- FLN underground)
+ r = !board.has do |s|
+ s.fln_bases.positive? && ((s.pop.zero? && s.fln_underground.zero?) || (!s.pop.zero? && s.fln_underground < 2))
+ end
+ dbg 'TERROR 1', r
+ r
+ end
+
+ def terror2?(_board = nil)
+ # if GOV is first eligible && will be second eligible
+ r = !first_eligible? && will_be_next_first_eligible?
+ dbg 'TERROR 2', r
+ r
+ end
+
+ def rally1?(board = @board)
+ # rally would place a base (rally 1 or 2)
+ r = board.available_fln_bases.positive? && board.has { |s| may_rally_1_in?(s) || may_rally_2_in?(s) }
+ dbg 'RALLY 1', r
+ r
+ end
+
+ def rally2?(board = @board)
+ # if #FLN bases * 2 > #FLN at FLN bases + 1d6/2
+ a = board.count(&:fln_bases) * 2
+ b = board.count { |s| s.fln_bases.zero? ? 0 : s.guerrillas }
+ r = a > (b + d6 / 2)
+ dbg 'RALLY 2', r
+ r
+ end
+ end
+
+ module FLNRallyRules
+ def may_rally_1_in?(space)
+ # 3+ FLN and no GOV (unless limited_op_only))
+ r = may_rally_in?(space) && may_add_base_in?(space) && space.guerrillas >= 3 &&
+ (limited_op_only? || space.gov_cubes.zero?)
+ dbg " may_rally_1_in : #{space.name}", r
+ r
+ end
+
+ def may_rally_2_in?(space)
+ # 4+ FLN
+ r = may_rally_in?(space) && may_add_base_in?(space) && space.guerrillas >= 4
+ dbg " may_rally_2_in : #{space.name}", r
+ r
+ end
+
+ def may_rally_3_in?(space)
+ # at FLN bases, with 2- FLN underground or 0 fln_underground in country or 0 pop
+ r = may_rally_in?(space) && !space.fln_bases.zero? &&
+ (space.country? || space.pop.zero? ? space.fln_underground.zero? : space.fln_underground < 2)
+ dbg " may_rally_3_in : #{space.name}", r
+ r
+ end
+
+ def rally_3_priority(spaces)
+ # Algeria -> with cubes -> pop 1+ -> least FLN underground
+ f = _filter(spaces) { |s| !s.country? }
+ f = _filter(f) { |s| s.gov_cubes.positive? }
+ f = _filter(f) { |s| s.pop.positive? }
+ _min(f, :fln_underground)
+ end
+
+ def may_rally_5_in?(space)
+ # non-city at support with 0 FLN underground
+ r = may_rally_in?(space) && !space.city? && space.support? && space.fln_underground.zero?
+ dbg " may_rally_5_in : #{space.name}", r
+ r
+ end
+
+ def rally_5_priority(spaces)
+ # highest population
+ _max(spaces, :pop)
+ end
+
+ def may_rally_6_in?(space, already_rallied)
+ # 2+ pop to agitate after rally
+ r = (already_rallied || may_rally_in?(space)) && space.pop > 1 && (space.terror.positive? || !space.oppose?)
+ if r
+ # may agitate if : FLN base or control after rally
+ n = already_rallied ? 0 : place_guerrillas_in(space).values.sum
+ r &= space.fln_bases.positive? || (space.gov < (space.fln + n))
+ end
+ dbg " may_rally_6_in : #{space.name}", r
+ r
+ end
+
+ def rally_6_priority(spaces)
+ # max pop, min terror, support : reference ?
+ f = _max(spaces, :pop)
+ f = _min(f, :terror)
+ _filter(f, &:support?)
+ # FIXME: maybe already selected first, or not
+ end
+
+ def may_rally_7_in?(space)
+ r = may_rally_in?(space)
+ dbg " may_rally_7_in : #{space.name}", r
+ r
+ end
+
+ def rally_7_priority(spaces)
+ # highest population -> gain FLN control -> remove Gov control -> city -> least terror
+ f = _max(spaces, :pop)
+ f = _filter(f) { |s| s.gov >= s.fln && s.gov < s.fln + place_guerrillas_in(s).values.sum }
+ f = _filter(f) { |s| s.gov >= s.fln && s.gov == s.fln + place_guerrillas_in(s).values.sum }
+ f = _filter(f, &:city?)
+ _min(f, :terror)
+ end
+
+ def may_rally_8_in?(space)
+ r = may_rally_in?(space) && !space.guerrillas.zero? && space.fln_bases.zero?
+ dbg " may_rally_8_in : #{space.name}", r
+ r
+ end
+
+ def rally_8_priority(spaces)
+ # Algeria -> most Guerrillas -> no gov cubes
+ f = _filter(spaces) { |s| !s.country? }
+ f = _max(f, :guerrillas)
+ _filter(f) { |s| s.gov_cubes.zero? }
+ end
+
+ def may_rally_9_in?(space)
+ r = may_agitate_in?(space)
+ dbg " may_rally_9_in : #{space.name}", r
+ r
+ end
+
+ def rally_9_priority(spaces, resources, &is_rallied)
+ has_resources = ->(s) { resources.zero? || (resources - (is_rallied.call(s) ? 0 : 1)) > s.terror }
+ f = _filter(spaces) { |s| s.support? && has_resources.call(s) }
+ _filter(f) { |s| s.neutral? && has_resources.call(s) }
+ end
+ end
+
+ module FLNExtortRules
+ def may_extort_0_in?(space)
+ r = may_extort_in?(space) && space.fln_underground > (space.fln_bases.zero? ? 0 : 1)
+ dbg " may_extort_0_in : #{space.name}", r
+ r
+ end
+
+ def extort_priority(spaces)
+ # 2+ guerrillas, 3+ if gov cubes and fln base -> Country -> anywhere if still at 0
+ f = _filter(spaces) { |s| s.guerrillas > (s.gov_cubes.positive? && s.fln_bases.positive? ? 2 : 1) }
+ _filter(f, &:country?)
+ end
+ end
+
+ module FLNSubvertRules
+ def may_subvert_1_in?(space, num)
+ # to remove last cubes
+ r = may_subvert_in?(space) && space.french_cubes.zero? && space.algerian_cubes <= num
+ dbg " may_subvert_1_in : #{space.name}", r
+ r
+ end
+
+ def subvert_1_priority(spaces)
+ # Police -> Troop
+ _max(spaces, :algerian_police)
+ end
+
+ def may_subvert_2_in?(space)
+ # to replace 1 Algerian Police
+ r = may_subvert_in?(space) && space.algerian_police.positive?
+ dbg " may_subvert_2_in : #{space.name}", r
+ r
+ end
+ end
+
+ module FLNTerrorRules
+ def may_terror_1_in?(space)
+ # to remove support, do not active last underground at bases
+ r = may_terror_in?(space) && space.support? && space.fln_underground > (space.fln_bases.positive? ? 1 : 0)
+ dbg " may_terror_1_in : #{space.name}", r
+ r
+ end
+
+ def terror_1_priority(spaces)
+ # highest population
+ _max(spaces, :pop)
+ end
+
+ def may_terror_2_in?(space, de_gaule: false)
+ # neutral and no terror and pacifiable, do not active last underground at bases
+ r = may_terror_in?(space) && space.neutral? && !space.terror? && _pacifiable(space, de_gaule) &&
+ space.fln_underground > (space.fln_bases.positive? ? 1 : 0)
+ dbg " may_terror_2_in : #{space.name}", r
+ r
+ end
+
+ def _pacifiable(space, de_gaule)
+ # in a city or sector with gov base OR
+ # if Recall de Gaulle in a sector with troops and police and gov control
+ (!space.country? && space.gov_bases.positive?) ||
+ (de_gaule && space.sector? && space.troops.positive? && space.police.positive? && space.gov_control?)
+ end
+ end
+
+ module FLNAttackRules
+ CASUALTIES_PRIORITY = %i[french_police algerian_police french_troops algerian_troops gov_bases].freeze
+
+ def may_attack_1_in?(space)
+ # attack will remove 1+ GOV piece, do not expose a base
+ r = may_attack_in?(space) && space.guerrillas > 5 && space.fln_bases.zero?
+ dbg " may_attack_1_in : #{space.name}", r
+ r
+ end
+
+ def may_ambush_1_in?(space)
+ # do not expose a base
+ r = may_ambush_in?(space) && (space.fln_bases.zero? || space.guerrillas > 1)
+ dbg " may_ambush_1_in : #{space.name}", r
+ r
+ end
+
+ def may_attack_2_in?(space)
+ # 4+ guerrillas, do not expose a base
+ r = may_attack_in?(space) && space.guerrillas > 3 && space.fln_bases.zero?
+ dbg " may_attack_2_in : #{space.name}", r
+ r
+ end
+
+ def attack_priority(spaces)
+ # remove priority GOV bases -> French Troops -> French Police -> most pieces
+ f = _filter(spaces) { |s| s.gov_bases.positive? && s.troops.zero? && s.police.zero? }
+ f = _filter(f) { |s| s.french_troops.positive? && s.police.zero? }
+ f = _filter(f) { |s| s.french_police.positive? }
+ _max(f, :gov)
+ end
+ end
+
+ # 8.1.2 - Procedure Guidelines
+ module FLNGuidelines
+ def _filter(spaces, &block)
+ return spaces if spaces.empty?
+
+ (f = spaces.select(&block)).empty? ? spaces : f
+ end
+
+ def _max(spaces, sym)
+ return spaces if spaces.empty?
+
+ v = spaces.max { |a, b| a.send(sym) <=> b.send(sym) }.send(sym)
+ spaces.select { |s| s.send(sym) == v }
+ end
+
+ def _min(spaces, sym)
+ return spaces if spaces.empty?
+
+ v = spaces.min { |a, b| a.send(sym) <=> b.send(sym) }.send(sym)
+ spaces.select { |s| s.send(sym) == v }
+ end
+
+ def available_fln_bases?(board = @board)
+ board.available_fln_bases.positive?
+ end
+
+ def may_add_base_in?(space)
+ space.guerrillas > 2 && (space.fln_bases < (space.country? ? space.max_bases : 1))
+ end
+
+ def placeable_guerrillas?(board = @board)
+ return true if board.available_fln_underground.positive?
+
+ board.spaces.map(&method(:_removable_guerrillas)).inject(0, :+).positive?
+ end
+
+ def placeable_guerrillas(board = @board)
+ board.available_fln_underground + board.spaces.map(&method(:_removable_guerrillas)).inject(0, :+)
+ end
+
+ def max_placable_guerrillas_in?(space)
+ max_placable_guerrillas(space).clamp(0, space.fln_bases.positive? ? (space.pop + 1 - space.guerrillas) : 666)
+ end
+
+ def place_guerrillas_in(space, board = @board)
+ n = max_placable_guerrillas_in?(space)
+ h = { space: 0 } # do not select space
+ n -= h[:available] = (a = board.available_fln_underground) >= n ? n : a
+ while n.positive? && !(spaces = _remove_guerrillas_priority(board.spaces, h)).empty?
+ s = spaces.sample
+ n -= h[s] = (g = _removable_guerrillas(s)) >= n ? n : g
+ end
+ h.reject { |_k, v| v.zero? } # FIXME: in empty? maybe hide active guerrillas ?
+ end
+
+ def pick_guerrillas_from(board = @board)
+ return :available if board.available_fln_underground.positive?
+
+ _remove_guerrillas_priority(board.spaces).sample
+ end
+
+ # 1) place: outofplay -> available | bases -> guerrillas if choice
+ # 2) place: underground first unless from map then place active first flipped as underground
+ # 3) march: underground -> active, unless march would activate then move active first
+
+ # applied as last filter in FLNBot#_priority
+ def place_guerrillas_priority(spaces)
+ # 4) support -> with friendly pieces -> random
+ f = _filter(spaces, &:support?)
+ _filter(f) { |s| s.guerrillas.positive? }
+ end
+
+ # place_guerrillas_in
+ def _removable_guerrillas(space)
+ # 5) active only, leave 2 guerrillas at base or support
+ a = (a = space.fln_underground) > 2 ? 2 : a
+ n = space.fln_active - (space.support? || space.fln_bases.positive? ? (2 - a) : 0)
+ n.positive? ? n : 0
+ end
+
+ def _not_selected(spaces, selected)
+ spaces.reject { |s| selected.key?(s) }
+ end
+
+ # place_guerrillas_in
+ def _remove_guerrillas_priority(spaces, selected = {})
+ # 5) #removable_guerrillas then most guerrillas first
+ return [] if (l = _not_selected(spaces, selected).select { |s| _removable_guerrillas(s).positive? }).empty?
+
+ _max(l, :guerrillas)
+ end
+
+ # not used yet
+ def remove_from(space, num = 1)
+ # 6) remove active -> underground -> base
+ h = {}
+ num -= h[:fln_active] = (s = space.fln_active) >= num ? num : s
+ num -= h[:fln_underground] = (s = space.fln_underground) >= num ? num : s
+ h[:fln_bases] = (s = space.fln_bases) >= num ? num : s
+ h
+ end
+
+ # 7) remove gov : map -> availabe (base -> french -> algerian; troops -> police)
+ # 8) reduce : commitment -> support -> france track -> gov resource
+ # 9) shift : support -> oppose | best combined; remove terror only if also shift
+ # 10) random
+ end
+end
diff --git a/lib/colonial_twilight/fln_bot/fln_extort.rb b/lib/colonial_twilight/fln_bot/fln_extort.rb
new file mode 100644
index 0000000..5889e24
--- /dev/null
+++ b/lib/colonial_twilight/fln_bot/fln_extort.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+module ColonialTwilight
+ module FLNBotExtort
+ def extort(except: nil, to_agitate_in: nil)
+ return false if available_resources > 4
+ return false unless @turn.may_special_activity?(:extort)
+ return false if (space = extort_priority(extortable(except: except)).sample).nil?
+
+ apply_action @turn.special_activity_in(:extort, space, -1, to_agitate_in: to_agitate_in).extort
+ end
+
+ def extortable(except: nil)
+ @board.search { |s| may_extort_0_in?(s) }.reject { |s| @turn.special_activity_selected?(s) || s == except }
+ end
+ end
+end
diff --git a/lib/colonial_twilight/fln_bot/fln_march.rb b/lib/colonial_twilight/fln_bot/fln_march.rb
new file mode 100644
index 0000000..f1631ad
--- /dev/null
+++ b/lib/colonial_twilight/fln_bot/fln_march.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+module ColonialTwilight
+ module FLNBotMarch
+ def march
+ return false if event_playable? && event_more_effective_than_terror?
+
+ # FIXME
+ end
+ end
+end
diff --git a/lib/colonial_twilight/fln_bot/fln_pass.rb b/lib/colonial_twilight/fln_bot/fln_pass.rb
new file mode 100644
index 0000000..ffd8f2f
--- /dev/null
+++ b/lib/colonial_twilight/fln_bot/fln_pass.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+module ColonialTwilight
+ module FLNBotPass
+ def pass
+ apply_action @turn.pass(1)
+ end
+ end
+end
diff --git a/lib/colonial_twilight/fln_bot/fln_rally.rb b/lib/colonial_twilight/fln_bot/fln_rally.rb
new file mode 100644
index 0000000..0e91534
--- /dev/null
+++ b/lib/colonial_twilight/fln_bot/fln_rally.rb
@@ -0,0 +1,190 @@
+# frozen_string_literal: true
+
+module ColonialTwilight
+ module FLNBotRally
+ def rally
+ # return false if !available_resources.positive? && !extort
+
+ @reserved_to_agitate = 0
+ # max 6 spaces
+ max_selected = (limited_op_only? ? 1 : 6)
+ # max 2/3 resources unless starts with < 9 resources
+ max_resources = (@board.fln_resources < 9 ? 0 : @board.fln_resources * 2 / 3)
+ max_cost = -> { max_resources.zero? ? 0 : max_resources - @turn.cost }
+
+ stop_cond = if max_resources.zero?
+ -> { @turn.selected_spaces >= max_selected }
+ else
+ -> { @turn.selected_spaces >= max_selected || (@turn.cost + @reserved_to_agitate) >= max_resources }
+ end
+ stop_cond_base = -> { !available_fln_bases? || stop_cond.call }
+
+ loop do
+ break unless _place_base_in(_rally(1, stop_cond_base, ->(s) { may_rally_1_in?(s) }))
+ end
+
+ loop do
+ break unless _place_base_in(_rally(2, stop_cond_base, ->(s) { may_rally_2_in?(s) }))
+ end
+
+ loop do
+ break unless _place_fln_in(_rally(3, stop_cond, ->(s) { may_rally_3_in?(s) }, priority: 3))
+ end
+
+ _shift_france_track unless stop_cond.call
+
+ loop do
+ break unless _place_fln_in(_rally(5, stop_cond, ->(s) { may_rally_5_in?(s) }, priority: 5))
+ end
+
+ unless stop_cond.call
+ printd(' rally 6')
+ filter = ->(s) { may_rally_6_in?(s, @turn.operation_selected?(s)) }
+ space = _rally_one_space(filter, priority: 6, reselect: true)
+ if _reserve_to_agitate_in?(space, max_cost.call)
+ agitate_in = space
+ _place_fln_in(space, to_agitate_in: space) unless @turn.operation_selected?(space)
+ end
+ end
+
+ 2.times do
+ break unless _place_fln_in(_rally(7, stop_cond, ->(s) { may_rally_7_in?(s) }, priority: 7))
+ end
+
+ 2.times do
+ break unless _place_fln_in(_rally(8, stop_cond, ->(s) { may_rally_8_in?(s) }, priority: 8))
+ end
+
+ if agitate_in.nil?
+ printd ' rally 9'
+ filter = ->(s) { may_rally_9_in?(s) && (@turn.operation_selected?(s) || @turn.selected_spaces < max_selected) }
+ spaces = rally_9_priority(@board.search(&filter), max_cost.call) { |s| @turn.operation_selected?(s) }.shuffle
+ while (space = spaces.pop)
+ if @turn.operation_selected?(space)
+ agitate_in = space
+ elsif _reserve_to_agitate_in?(space, max_cost.call) && _place_fln_in(space, to_agitate_in: space)
+ agitate_in = space
+ end
+ break unless agitate_in.nil?
+ end
+ end
+ _agitate_in(agitate_in, max_cost.call)
+
+ @turn.operation_done?
+ end
+
+ def _rally(num, stop_cond, filter, priority: nil, reselect: false)
+ return nil if stop_cond.call
+
+ printd(" rally #{num}")
+ return nil if (space = _rally_one_space(filter, priority: priority, reselect: reselect)).nil?
+
+ printd(" -> #{space.name}")
+ extort unless available_resources.positive?
+
+ available_resources.positive? ? space : nil
+ end
+
+ def _rally_one_space(filter, priority: nil, reselect: false)
+ spaces = @board.search(&filter)
+ spaces = spaces.reject(&@turn.method('operation_selected?')) unless reselect
+ spaces = _place_priority(spaces, priority) unless priority.nil?
+ spaces.sample
+ end
+
+ def _place_priority(spaces, priority)
+ return spaces if spaces.size < 2
+
+ spaces = case priority
+ when 3 then rally_3_priority(spaces)
+ when 5 then rally_5_priority(spaces)
+ when 6 then rally_6_priority(spaces)
+ when 7 then rally_7_priority(spaces)
+ else spaces
+ end
+ place_guerrillas_priority(spaces)
+ end
+
+ def _place_base_in(space)
+ return false if space.nil?
+
+ printd " => _place_base_in : #{space.name}"
+ a, u = (n = space.fln_active) >= 2 ? [2, 0] : [n, 2 - n]
+ apply_action @turn.operation_in(:rally, space, 1)
+ .transfer_to(:available, :fln_active, a)
+ .transfer_to(:available, :fln_underground, u)
+ .transfer_from(:available, :fln_base)
+ end
+
+ def _place_fln_in(space, to_agitate_in: nil)
+ return false if space.nil?
+
+ printd " => _place_fln_in : #{space.name}"
+ return false if (steps = place_guerrillas_in(space)).empty?
+
+ apply_action @turn.operation_in(:rally, space, 1, to_agitate_in: to_agitate_in).transfer_steps(steps)
+ end
+
+ def _shift_france_track
+ printd(' rally 4')
+ return false if @board.france_track.zero?
+
+ extort unless available_resources.positive?
+ apply_action @turn.operation_in(:rally, :france_track, 1).shift(1)
+ end
+
+ def _agitate_in(space, max_cost)
+ return if space.nil?
+
+ printd " => _agitate_in : #{space.name}"
+ terror = space.terror
+ oppose = space.oppose? ? 0 : 1
+ if @reserved_to_agitate.positive?
+ terror = terror > @reserved_to_agitate ? @reserved_to_agitate : terror
+ oppose = 0 if terror == @reserved_to_agitate
+ return apply_action @turn.agitate_in(space, terror, oppose)
+ end
+
+ if max_cost.positive? && (cost = (terror + oppose)) > max_cost
+ terror -= (cost - oppose - max_cost)
+ oppose = 0
+ end
+ return if terror.zero?
+
+ if (cost = terror + oppose) < available_resources
+ return apply_action @turn.agitate_in(space, terror, oppose)
+ end
+
+ max_rcs = available_resources + extortable.size
+ if cost > max_rcs
+ terror -= (cost - oppose - max_rcs)
+ oppose = 0
+ end
+ return if terror.zero?
+
+ ((terror + oppose) - available_resources).times { extort(to_agitate_in: space) }
+ apply_action @turn.agitate_in(space, terror, oppose)
+ end
+
+ def _reserve_to_agitate_in?(space, max_cost)
+ return false if space.nil?
+
+ printd " => _reserve_to_agitate_in : #{space.name}"
+ cost = (rally_cost = (@turn.operation_selected?(space) ? 0 : 1)) + (agitate_cost = max_agitate_cost(space))
+ agitate_cost -= (cost - max_cost) if max_cost.positive? && cost > max_cost
+ return false unless agitate_cost.positive?
+
+ if (cost = (rally_cost + agitate_cost)) < available_resources
+ @reserved_to_agitate = agitate_cost
+ return true
+ end
+ max_rcs = available_resources + extortable.size
+ agitate_cost -= (cost - max_rcs) if cost > max_rcs
+ return false unless agitate_cost.positive?
+
+ ((rally_cost + agitate_cost) - available_resources).times { extort(to_agitate_in: space) }
+ @reserved_to_agitate = agitate_cost
+ true
+ end
+ end
+end
diff --git a/lib/colonial_twilight/fln_bot/fln_rules.rb b/lib/colonial_twilight/fln_bot/fln_rules.rb
new file mode 100644
index 0000000..39acd3d
--- /dev/null
+++ b/lib/colonial_twilight/fln_bot/fln_rules.rb
@@ -0,0 +1,94 @@
+# frozen_string_literal: true
+
+module ColonialTwilight
+ module FLNRules
+ # Rally 3.3.1 + France Track
+ def may_rally_in?(space)
+ space.sector? || (space.city? && !space.support?) || (space.country? && space.independent?)
+ end
+
+ def rally_spaces(board)
+ board.search(&method(:may_rally_in?))
+ end
+
+ def may_agitate_in?(space)
+ !space.country? && (space.fln_control? || space.fln_bases.positive?) && (space.terror.positive? || !space.oppose?)
+ end
+
+ def agitate_spaces(board)
+ board.search(&method(:may_agitate_in?))
+ end
+
+ def max_placable_guerrillas(space)
+ space.fln_bases.positive? ? space.fln_bases + space.pop : 1
+ end
+
+ def max_agitate_cost(space)
+ space.terror + (space.oppose? ? 0 : 1)
+ end
+
+ # March 3.3.2
+ def must_stop?(space_from, space_to)
+ space_from.wilaya != space_to.wilaya || space_from.country? || space_to.country?
+ end
+
+ def must_activate?(board, space_from, space_to, num = 1)
+ (space_from.country? || space_to.support?) &&
+ (num + space_to.gov_cubes + (space_from.country? ? board.border_zone_track : 0)) > 3
+ end
+
+ # Attack 3.3.3
+ def may_attack_in?(space)
+ space.guerrillas.positive? && space.gov.positive?
+ end
+
+ def attack_spaces(board)
+ board.search(&method(:may_attack_in?))
+ end
+
+ # Terror 3.3.4
+ def may_terror_in?(space)
+ !space.country? && !space.pop.zero? && space.fln_underground.positive?
+ end
+
+ def terror_spaces(board)
+ board.search(&method(:may_terror_in?))
+ end
+
+ # Extort 4.3.1
+ def may_extort_in?(space)
+ space.fln_underground.positive? && (space.country? ? space.independent? : !space.pop.zero? && space.fln_control?)
+ end
+
+ def extort_spaces(board)
+ board.search(&method(:may_extort_in?))
+ end
+
+ # Subvert 4.3.2
+ def may_subvert_in?(space)
+ space.fln_underground.positive? && space.algerian_cubes.positive?
+ end
+
+ def subvert_spaces(board)
+ board.search(&method(:may_subvert_in?))
+ end
+
+ # Ambush 4.3.3
+ def may_ambush_in?(space)
+ may_attack_in?(space) && space.fln_underground.positive?
+ end
+
+ def ambush_spaces(board)
+ board.search(&method(:may_ambush_in?))
+ end
+
+ # OAS 5.3.1
+ def may_oas_in?(space)
+ !space.country? && !space.pop.zero? && !space.terror.positive?
+ end
+
+ def oas_spaces(board)
+ board.search(&method(:may_oas_in?))
+ end
+ end
+end
diff --git a/lib/colonial_twilight/fln_bot/fln_subvert.rb b/lib/colonial_twilight/fln_bot/fln_subvert.rb
new file mode 100644
index 0000000..773b1a7
--- /dev/null
+++ b/lib/colonial_twilight/fln_bot/fln_subvert.rb
@@ -0,0 +1,50 @@
+# frozen_string_literal: true
+
+module ColonialTwilight
+ module FLNBotSubvert
+ def subvert
+ return false if (spaces = subvert_spaces(@board)).empty?
+
+ n = 2
+ while n.positive?
+ printd(' subvert 1')
+ break if (space = subvert_1_priority(spaces.select { |s| may_subvert_1_in?(s, n) }).sample).nil?
+
+ n -= space.algerian_cubes
+ apply_action _subvert_remove(space, space.algerian_police, space.algerian_troops)
+ spaces.delete(space)
+ end
+ return true if n.zero? || spaces.empty?
+
+ if n == 2 && placeable_guerrillas?
+ printd(' subvert 2')
+ unless (space = spaces.select { |s| may_subvert_2_in?(s) }.sample).nil?
+ apply_action _subvert_replace(space, pick_guerrillas_from)
+ return true
+ end
+ end
+ return false if n == 2 && !@turn.operation_done?
+
+ spaces.shuffle!
+ while n.positive? && !(space = spaces.pop).nil?
+ printd(' subvert 3')
+ n -= (p = (p = space.algerian_police) > n ? n : p)
+ n -= (t = (t = space.algerian_troops) > n ? n : t)
+ apply_action _subvert_remove(space, p, t)
+ end
+ n != 2
+ end
+
+ def _subvert_remove(space, police, troops)
+ @turn.special_activity_in(:subvert, space, 0)
+ .transfer_to(:available, :algerian_police, police)
+ .transfer_to(:available, :algerian_troops, troops)
+ end
+
+ def _subvert_replace(space, place_from)
+ @turn.special_activity_in(:subvert, space, 0)
+ .transfer_to(:available, :algerian_police, 1)
+ .transfer_from(place_from, :fln_underground, 1)
+ end
+ end
+end
diff --git a/lib/colonial_twilight/fln_bot/fln_terror.rb b/lib/colonial_twilight/fln_bot/fln_terror.rb
new file mode 100644
index 0000000..a9fd9e2
--- /dev/null
+++ b/lib/colonial_twilight/fln_bot/fln_terror.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+module ColonialTwilight
+ module FLNBotTerror
+ def terror
+ # return false if !available_resources.positive? && !extort
+ return false if event_playable? && event_more_effective_than_terror?
+
+ until (space = terror_1_priority(@board.search { |s| may_terror_1_in?(s) }).sample).nil?
+ exc = space.fln_underground == 1 ? space : nil
+ break if !available_resources.positive? && !extort(except: exc)
+
+ apply_action @turn.operation_in(:terror, space, 1).terror
+ end
+
+ if last_campaign?
+ until (space = @board.search { |s| may_terror_2_in?(s) }.sample).nil?
+ exc = space.fln_underground == 1 ? space : nil
+ break if !available_resources.positive? && !extort(except: exc)
+
+ apply_action @turn.operation_in(:terror, space, 1).terror
+ end
+ end
+
+ @turn.operation_done?
+ end
+ end
+end