From 9458b6413e3609e12f563dcb321d493b5f317017 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9my=20Zurcher?= Date: Wed, 11 Mar 2026 15:16:47 +0100 Subject: update FlnBot infrastructure --- lib/colonial_twilight/fln_bot.rb | 375 ++----------------------- lib/colonial_twilight/fln_bot/fln_attack.rb | 67 +++++ lib/colonial_twilight/fln_bot/fln_bot_rules.rb | 363 ++++++++++++++++++++++++ lib/colonial_twilight/fln_bot/fln_extort.rb | 17 ++ lib/colonial_twilight/fln_bot/fln_march.rb | 11 + lib/colonial_twilight/fln_bot/fln_pass.rb | 9 + lib/colonial_twilight/fln_bot/fln_rally.rb | 190 +++++++++++++ lib/colonial_twilight/fln_bot/fln_rules.rb | 94 +++++++ lib/colonial_twilight/fln_bot/fln_subvert.rb | 50 ++++ lib/colonial_twilight/fln_bot/fln_terror.rb | 28 ++ lib/colonial_twilight/fln_bot_rules.rb | 363 ------------------------ lib/colonial_twilight/fln_rules.rb | 94 ------- spec/fln_bot_rules_spec.rb | 6 +- spec/fln_rules_spec.rb | 2 +- 14 files changed, 853 insertions(+), 816 deletions(-) create mode 100644 lib/colonial_twilight/fln_bot/fln_attack.rb create mode 100644 lib/colonial_twilight/fln_bot/fln_bot_rules.rb create mode 100644 lib/colonial_twilight/fln_bot/fln_extort.rb create mode 100644 lib/colonial_twilight/fln_bot/fln_march.rb create mode 100644 lib/colonial_twilight/fln_bot/fln_pass.rb create mode 100644 lib/colonial_twilight/fln_bot/fln_rally.rb create mode 100644 lib/colonial_twilight/fln_bot/fln_rules.rb create mode 100644 lib/colonial_twilight/fln_bot/fln_subvert.rb create mode 100644 lib/colonial_twilight/fln_bot/fln_terror.rb delete mode 100644 lib/colonial_twilight/fln_bot_rules.rb delete mode 100644 lib/colonial_twilight/fln_rules.rb diff --git a/lib/colonial_twilight/fln_bot.rb b/lib/colonial_twilight/fln_bot.rb index 1db4564..4e4cbf2 100644 --- a/lib/colonial_twilight/fln_bot.rb +++ b/lib/colonial_twilight/fln_bot.rb @@ -1,20 +1,36 @@ # frozen_string_literal: true -require 'colonial_twilight/player' -require 'colonial_twilight/fln_rules' -require 'colonial_twilight/fln_bot_rules' +require_relative 'player' +require_relative 'fln_bot/fln_rules' +require_relative 'fln_bot/fln_bot_rules' + +require_relative 'fln_bot/fln_attack' +require_relative 'fln_bot/fln_extort' +require_relative 'fln_bot/fln_march' +require_relative 'fln_bot/fln_pass' +require_relative 'fln_bot/fln_rally' +require_relative 'fln_bot/fln_subvert' +require_relative 'fln_bot/fln_terror' module ColonialTwilight class FLNBot < Player include ColonialTwilight::FLNRules include ColonialTwilight::FLNBotRules - include ColonialTwilight::FLNRalyRules + include ColonialTwilight::FLNRallyRules include ColonialTwilight::FLNExtortRules include ColonialTwilight::FLNSubvertRules include ColonialTwilight::FLNTerrorRules include ColonialTwilight::FLNAttackRules include ColonialTwilight::FLNGuidelines + include ColonialTwilight::FLNBotAttack + include ColonialTwilight::FLNBotExtort + include ColonialTwilight::FLNBotMarch + include ColonialTwilight::FLNBotPass + include ColonialTwilight::FLNBotRally + include ColonialTwilight::FLNBotSubvert + include ColonialTwilight::FLNBotTerror + def play_turn(prev_action, possible_actions) init_turn prev_action, possible_actions _start_turn @@ -50,356 +66,5 @@ module ColonialTwilight # FIXME: the next Propaganda Card will be the last one of the game true end - - # PASS ##################################################################### - - def pass - apply_action @turn.pass(1) - end - - # EXTORT ################################################################### - - 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 - - # TERROR ################################################################### - - 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 - - # ATTACK ################################################################### - - 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 - 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 - - # SUBVERT ################################################################## - - 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 - - # RALLY #################################################################### - - 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 - - # MARCH# ################################################################### - - 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_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 diff --git a/lib/colonial_twilight/fln_bot_rules.rb b/lib/colonial_twilight/fln_bot_rules.rb deleted file mode 100644 index a773cdc..0000000 --- a/lib/colonial_twilight/fln_bot_rules.rb +++ /dev/null @@ -1,363 +0,0 @@ -# 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 FLNRalyRules - 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_rules.rb b/lib/colonial_twilight/fln_rules.rb deleted file mode 100644 index 39acd3d..0000000 --- a/lib/colonial_twilight/fln_rules.rb +++ /dev/null @@ -1,94 +0,0 @@ -# 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/spec/fln_bot_rules_spec.rb b/spec/fln_bot_rules_spec.rb index d0a2f4c..9d5009e 100644 --- a/spec/fln_bot_rules_spec.rb +++ b/spec/fln_bot_rules_spec.rb @@ -1,13 +1,13 @@ # frozen_string_literal: true -require './lib/colonial_twilight/fln_rules' -require './lib/colonial_twilight/fln_bot_rules' +require './lib/colonial_twilight/fln_bot/fln_rules' +require './lib/colonial_twilight/fln_bot/fln_bot_rules' require './spec/mock_board' class FLNRulesImpl include ColonialTwilight::FLNRules include ColonialTwilight::FLNBotRules - include ColonialTwilight::FLNRalyRules + include ColonialTwilight::FLNRallyRules include ColonialTwilight::FLNExtortRules include ColonialTwilight::FLNSubvertRules include ColonialTwilight::FLNTerrorRules diff --git a/spec/fln_rules_spec.rb b/spec/fln_rules_spec.rb index 7dfc00b..dc19be1 100644 --- a/spec/fln_rules_spec.rb +++ b/spec/fln_rules_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require './lib/colonial_twilight/fln_rules' +require './lib/colonial_twilight/fln_bot/fln_rules' require './lib/colonial_twilight/board' require './spec/mock_board' -- cgit v1.1-2-g2b99