summaryrefslogtreecommitdiffstats
path: root/lib
diff options
context:
space:
mode:
authorJérémy Zurcher <jeremy@asynk.ch>2020-09-01 15:41:09 +0200
committerJérémy Zurcher <jeremy@asynk.ch>2020-09-01 15:41:09 +0200
commita537cb64b77793387d2529467d4291198fc7fa70 (patch)
tree066221cddae67446900ace94e844c5980a81169a /lib
parent996b12185fdece116f73d20cb57eae322ecef2f2 (diff)
downloadcolonial-twilight-a537cb64b77793387d2529467d4291198fc7fa70.zip
colonial-twilight-a537cb64b77793387d2529467d4291198fc7fa70.tar.gz
FLNBot : good, still misses Event and March support
Diffstat (limited to 'lib')
-rw-r--r--lib/colonial_twilight/fln_bot.rb826
1 files changed, 823 insertions, 3 deletions
diff --git a/lib/colonial_twilight/fln_bot.rb b/lib/colonial_twilight/fln_bot.rb
index 9944507..42d3883 100644
--- a/lib/colonial_twilight/fln_bot.rb
+++ b/lib/colonial_twilight/fln_bot.rb
@@ -3,17 +3,837 @@
module ColonialTwilight
- class FLNBot
+ class Player
attr_reader :faction
def initialize game, faction
@game = game
+ @board = game.board
+ @ui = game.ui
@faction = faction
+ @debug = game.options.debug_bot
+ @possible_actions = nil
end
- def play possible_actions
- puts 'FLNBot.play' #FIXME
+ def conducted_action
+ a = nil
+ if not @card.nil?
+ raise "Operation #{@operation} conducted with event" if not @operation.nil?
+ raise "Special Activity #{@special_activity} conducted with event" if not @special_activity.nil?
+ a = :event
+ elsif not @special_activity.nil?
+ a = :ope_special
+ else
+ if @operation_count == 0
+ a = :pass
+ elsif @operation_count == 1
+ a = :ope_limited
+ else
+ a = :ope_only
+ end
+ end
+ raise "#{a} has been conducted but is not allowed" if not @possible_actions.include? a
+ puts "Conducted action is : #{a}" if @debug
+ a
+ end
+
+ private
+
+ def first_eligible?
+ @game.actions.size == 0
+ # @possible_actions.length == 5
+ end
+
+ def may_play_event?
+ not @card.nil? and @possible_actions.include? :event
+ end
+
+ def limited_ope_only?
+ (@possible_actions.size == 2 and @possible_actions.include? :ope_limited)
+ end
+
+ def limited_ope_done?
+ limited_ope_only? and @operation_count == 1
+ end
+
+ def may_conduct_special_activity? sp
+ r = @possible_actions.include? :ope_special
+ r &= (sp == @special_activity) if not @special_activity.nil?
+ r
+ end
+
+ def operation_done ope
+ raise "try to conduct ope #{ope} over #{@operation}" if not (@operation.nil? or @operation == ope)
+ raise "cannot conduct another" if @operation_count > 0 and limited_ope_only?
+ @card = nil
+ @operation = ope
+ @operation_count += 1
+ end
+
+ def special_activity_done sp
+ raise "try to conduct special activity #{sp} over #{@special_activity}" if not (@special_activity.nil? or @special_activity == sp)
+ raise "cannot conduct a special activity" if not may_conduct_special_activity? sp
+ @card = nil
+ @special_activity = sp
+ @special_activity_count += 1
+ end
+
+ end
+
+ # Country.independant
+
+ class FLNBot < Player
+
+ def play card, possible_actions
+ @card = card
+ @possible_actions = possible_actions
+
+ @operation = nil
+ @operation_count = 0
+ @special_activity = nil
+ @special_activity_count = 0
+
+ @selected_spaces = []
+ @expended_resources = 0
+
+ _start
+ end
+
+ private
+
+ def _start
+ # resources = 0 && Ope Limited as only choice
+ if @board.fln_resources == 0 and limited_ope_only?
+ puts ' => PASS' if @debug
+ h = get_action :pass, -1, :pass, false
+ apply_action h
+ return conducted_action
+ end
+
+ # GOV is first eligible && will be second eligible
+ if not first_eligible? and @game.eligibility_swap?
+ puts ' => TERROR 1' if @debug
+ return terror
+ end
+
+ # exists no FLN base with (POP 1+ && 1- FLN undeground OR POP 0 && 0 FLN underground)
+ if not @board.has(:sectors) {|s| s.fln_bases_1m? and ((not s.pop0? and s.fln_u_1l?) or (s.pop0? and s.fln_u_0?)) }
+ puts ' => TERROR 2' if @debug
+ return terror
+ end
+
+ return _march_or_rally
+ end
+
+ def _march_or_rally
+ # rally would place base : see rally first 2 bullets
+ if @board.available_fln_bases > 0 and @board.has {|s| may_add_fln_base?(s) and ((s.guerrillas >= 3 and (limited_ope_only? ? true : s.gov_cubes_0?)) or s.guerrillas >= 4) }
+ puts ' => RALLY 1' if @debug
+ return rally
+ end
+
+ # #FLN bases * 2 > #FLN at FLN bases + 1d6/2
+ if (@board.count() {|s| s.fln_bases } * 2) > (@board.count() {|s| s.fln_bases_0? ? 0 : s.guerrillas } + rand(7)/2)
+ puts ' => RALLY 2' if @debug
+ return rally
+ end
+
+ puts ' => MARCH 1' if @debug
+ return march
+ end
+
+ ##### TERROR OPERATION #####
+ def terror
+ # not POP 0 && 1+ FLN underground && (no FLN bases || 2+ FLN underground)
+ spaces = @board.search(:sectors) {|s| not s.pop0? and s.fln_u_1m? and (s.fln_bases_0? or s.fln_u_2m?) }
+
+ # play event if more profitable
+ vpts = 0
+ (@board.fln_resources + possible_extort).clamp(0, spaces.length).times {|n| vpts += spaces[n].pop }
+ return event if spaces.empty? or (may_play_event? and vpts <= @card.fln_effectiveness)
+
+ # to remove support, highest POP first
+ sort_filter(spaces.select{|s| s.support? }, :pop).each do |selected|
+ break if not may_continue?
+ _terror selected
+ spaces.delete selected
+ end
+
+ # if last campaign, neutral with no terror and pacifiable, highest POP first
+ sort_filter(spaces.select{|s| s.neutral? and not s.has_terror? and pacifiable?(s) and not_selected s }, :pop).each do |selected|
+ break if not may_continue?
+ _terror selected
+ spaces.delete selected
+ end if last_campaign?
+
+ subvert if may_conduct_special_activity? :subvert
+ extort if may_conduct_special_activity? :extort
+
+ return conducted_action
+ end
+
+ def _terror selected
+ h = get_action :terror, 1, selected
+ transfer h, 1, :fln_underground, selected, selected, :fln_active
+ mks = []
+ mks << [:terror, 1, nil, 0, 1] if not selected.terror?
+ mks << [:alignment, 1, :support, :oppose, :neutral] if selected.oppose?
+ mks << [:alignment, 1, :oppose, :support, :neutral] if selected.support?
+ h[:markers] = mks
+ apply_action h
+ end
+
+ ##### EVENT #####
+ def event
+ if may_play_event? and @card.fln_effective? and ((@card.fln_marked? or @card.capability?) or (rand(7) < 4 and @card.fln_playable?))
+ raise "FIXME event not implemented yet"
+ return conducted_action
+ end
+ return attack
+ end
+
+ ##### SUBVERT SPECIAL ACTIVITY #####
+ def subvert
+ puts ' => SUBVERT' if @debug
+ puts ':: 1+ FLN underground && 1+ algerian cubes' if @debug
+ spaces = @board.search(:sectors) {|s| s.fln_u_1m? and s.algerian_cubes >= 1 }
+
+ # in up to 2 spaces, to remove last cube
+ tmp = spaces.select{|s| s.french_cubes == 0 and s.algerian_cubes < 3 }
+ tmp.shuffle!
+ tmp.sort! {|a,b|
+ r = b.algerian_police <=> a.algerian_police # police first
+ r = b.algerian_troops <=> a.algerian_troops if r == 0 # troops then
+ r
+ }
+
+ # up to 2 spaces and 2 cubes
+ n = 0
+ 2.times do
+ selected = tmp.shift
+ break if selected.nil? or n >= 2
+ n += _remove selected
+ spaces.delete selected
+ end
+
+ # only place FLN from available
+ if n == 0 and @board.available_fln_underground > 0 and not spaces.empty?
+ n += _replace spaces[rand(spaces.length)]
+ end
+
+ # randomly if one piece was removed or GOV is first eligible
+ if n == 1 or not first_eligible? and not spaces.empty?
+ n += _remove spaces[rand(spaces.length)]
+ end
+ end
+
+ def _replace selected
+ h = get_action :subvert, 0, selected
+ transfer h, 1, :algerian_police, selected, :available
+ transfer h, 1, :fln_underground, :available, selected
+ apply_action h
+ 2
+ end
+
+ def _remove selected
+ h = get_action :subvert, 0, selected
+ ap = selected.algerian_police.clamp(0, 2)
+ transfer h, ap, :algerian_police, selected, :available if ap > 0
+ at = selected.algerian_troops.clamp(0, 2 - ap)
+ transfer h, at, :algerian_troops, selected, :available if at > 0
+ apply_action h
+ ap + at
+ end
+
+ ##### EXTORT SPECIAL_ACTIVITY #####
+ def extort
+ # 1+ POP && (FLN control or country) && (1+ FLN underground or 2+ FLN if 1+ FLN bases and not country)
+ spaces = @board.search() {|s| not s.pop0? and (s.fln_control? or s.country?) and s.fln_u_1m? and (s.country? or s.fln_bases_0? or s.fln_u_2m?) }
+
+ # 3+ FLN or 2+ FLN if (0 GOV cubes or 0 FLN base)
+ spaces.select{|s| s.guerrillas > 3 or (s.guerrillas > 2 and (s.gov_cubes_0? or s.fln_bases_0?)) }.each do |selected|
+ _extort selected
+ spaces.delete selected
+ end
+
+ # Morocco and Tunisia
+ spaces.select{|s| s.country? }.each do |selected|
+ _extort selected
+ spaces.delete selected
+ end
+
+ # if still at 0 resources, everywhere possible
+ spaces.each do |selected| _extort selected end if @board.fln_resources == 0
+ end
+
+ def _extort selected
+ h = get_action :extort, -1, selected
+ transfer h, 1, :fln_underground, selected, selected, :fln_active
+ apply_action h
+ end
+
+ ##### ATTACK OPERATION #####
+ PIECES = [:gov_base,:french_troops,:french_police,:algerian_troops,:algerian_police].freeze
+ def attack
+ spaces = nil
+ if not may_conduct_special_activity? :ambush
+ # GOV pieces && no FLN bases && 6+ FLN
+ spaces = @board.search(:sectors) {|s| s.has_gov? and s.fln_bases_0? and s.guerrillas > 5 }
+ else
+ # GOV pieces && ((no FLN bases && (6+ FLN or 1+ FLN underground)) || (FLN bases and 2+ FLN underground))
+ spaces = @board.search(:sectors) {|s| s.has_gov? and ((s.fln_bases_0? and (s.guerrillas > 5 or s.fln_u_1m?)) or (not s.fln_bases_0? and s.fln_u_2m?)) }
+ end
+ casualties = _compute_casualties spaces
+ if casualties.inject(0){|n,c| n + c[:french_police] + c[:french_troops] + c[:gov_base] } > 2
+ casualties.each {|c| puts spaces[c[:i]].name; puts c.inspect } if @debug
+ ambushes = 0
+ casualties.each do |c|
+ break if not may_continue?
+ a = c[:n] < 5
+ next if a and ambushes == 2 # only 2 ambushes
+ selected = spaces[c[:i]]
+ h = get_action :attack, 1, selected
+ if a
+ ambushes += 1
+ h[:action] = :ambush
+ transfer h, 1, :fln_underground, selected, selected, :fln_active
+ else
+ transfer h, selected.fln_underground, :fln_underground, selected, selected, :fln_active if selected.fln_underground > 0
+ end
+ PIECES.each {|k| transfer h, c[k], k, selected, :casualties if c[k] != 0 }
+ if !a
+ fc = c[:french_police] + c[:french_troops] + c[:gov_base]
+ fc.times {|t| transfer h, 1, :fln_active, selected, (t%2 == 0 ? :available : :casualties), :fln_underground }
+ end
+ apply_action h
+ end
+
+ if may_continue?
+ # GOV pieces && 3+ FLN underground
+ spaces = @board.search(:sectors) {|s| s.has_gov? and s.guerrillas > 3 }.select{|s| not_selected s }
+ if not spaces.empty?
+ c = _compute_casualties(spaces)[0]
+ selected = spaces[c[:i]]
+ h = get_action :attack, 1, selected
+ transfer h, selected.fln_underground, :fln_underground, selected, selected, :fln_active if selected.fln_underground > 0
+ if rand(7) <= selected.fln
+ PIECES.each {|k| transfer h, c[k], k, selected, :casualties if c[k] != 0 }
+ fc = c[:french_police] + c[:french_troops] + c[:gov_base]
+ fc.times {|t| transfer h, 1, :fln_active, selected, (t%2 == 0 ? :available : :casualties), :fln_underground }
+ end
+ apply_action h
+ end
+ end
+
+ extort if may_conduct_special_activity? :extort
+
+ return conducted_action
+ end
+
+ return _march_or_rally
+ end
+
+ def _compute_casualties spaces
+ casualties = spaces.inject([]) {|n,s|
+ h = {:i=>n.length, :n=>s.fln, :u=>s.fln_underground}
+ PIECES.each {|k| h[k] = 0 }
+ t = 0
+ if s.guerrillas > 5 # auto succes -> 2 casualties
+ t = h[:french_police] = s.french_police.clamp(0, 2)
+ t += h[:algerian_police] = s.algerian_police.clamp(0, 2 - t) if t < 2
+ t += h[:french_troops] = s.french_troops.clamp(0, 2 - t) if t < 2
+ t += h[:algerian_troops] = s.algerian_troops.clamp(0, 2 - t) if t < 2
+ t += h[:gov_base] = s.gov_bases.clamp(0, 2 - t) if t < 2
+ else # ambush 1 casualty
+ t = h[:french_police] = 1 if s.french_police > 0
+ t = h[:algerian_police] = 1 if s.algerian_police > 0 and t == 0
+ t = h[:french_troops] = 1 if s.french_troops > 0 and t == 0
+ t = h[:algerian_troops] = 1 if s.algerian_troops > 0 and t == 0
+ t = h[:gov_base] = 1 if s.fln_bases > 0 and t == 0
+ end
+ h[:t] = t
+ n << h
+ }
+ casualties.shuffle!
+ casualties.sort!{|a,b|
+ r = b[:gov_base] <=> a[:gov_base] # bases
+ r = b[:french_troops] <=> a[:french_troops] if r == 0 # french troops
+ r = b[:french_police] <=> a[:french_police] if r == 0 # french police
+ r = b[:t] <=> a[:t] if r == 0 # most pieces
+ r
+ }
+ casualties
+ end
+
+ ##### MARCH OPERATION #####
+ def march
+ # up to 2/3 resources expended unless 8 or less resources
+ rcs_max = (@board.fln_resources <= 8 ? @board.fln_resources : @board.fln_resources * 2 / 3)
+ stop_cond = -> { @expended_resources == rcs_max or not may_continue? }
+
+ # FIXME
+ # @board.spaces_h['Tlemcen'].add :fln_underground, -1
+ # @board.spaces_h['Tlemcen'].add :fln_base, 1
+ # @board.spaces_h['Mascara'].add :fln_underground, 2
+ # @board.spaces_h['Mascara'].add :fln_active, 2
+ # @board.spaces_h['Saida'].add :fln_base
+ # @board.spaces_h['Saida'].add :fln_underground, 1
+ # @board.spaces_h['Saida'].add :fln_active, 2
+ # @board.spaces_h['Sidi Bel Abbes'].add :algerian_police, 1
+ @board.spaces_h['Bordj Bou Arreridj'].add :fln_base, 1
+ @board.spaces_h['Oum El Bouaghi'].add :fln_base, 1
+ # @board.spaces_h['Biskra'].add :fln_base, 1
+ @board.spaces_h['Tebessa'].add :fln_active, 1
+ @board.spaces_h['Negrine'].add :fln_active, 1
+ @board.spaces_h['Negrine'].add :fln_underground, 1
+ #
+ spaces = @board.search {|s| not s.fln_bases_0? and s.fln_underground == 0 }
+ puts "spaces :: " + spaces.collect(){|s| s.is_a?(Symbol) ? s.to_s : s.name}.join(' :: ')
+ selected = spaces[0]
+ selected = @board.spaces[11]
+ puts "DEST : #{selected.name}"
+ d = _paths(selected, {:fln_underground=>1}) {|h| h[:fln_underground] > 0 }
+
+ puts "FIXME : march is not implemented yet"
+ exit 1
+
+ # DEAD ZONE => no multiple march
+
+ # march with underground -> unless march will trigger active
+ #
+ # unless limited ope can march again untill cross wilaya or border
+ #
+ # pay per destination
+ #
+
+ ########
+ # march 1 underground FLN to each base that does not have 1
+ # - lowest cost
+ # while not stop_cond.call
+ # spaces = @board.search {|s| not s.fln_bases_0? and s.fln_underground == 0 }
+ # break if spaces.empty?
+ # end
+
+
+ ########
+ # march 1 FLN to each spaces at Support if 0 FLN
+ # march 2 FLN in up to 1 city if Amateur Bombers in effect
+ # - to stay underground : unless last Campaign
+ # - lowest cost
+
+ ########
+ # march to remove GOV control in 1 POP+ not at Oppose
+ # - mountain
+ # - highest POP
+ # - lowest cost
+
+ ########
+ # march 3 FLN to non-resettled POP0 with room for a base
+ # - fewest GOV cubes
+ # - mountain
+ # - lowest cost
+ # - at least 1 FLN stays underground
+
+ # selected = @board.spaces[11]
+ # puts "DEST : #{selected.name}"
+ # d = _paths(selected, {:fln_underground=>1}) {|h| h[:fln_underground] > 0 }
+
+ return conducted_action
+ end
+
+ def _paths dst, want, &cond
+ ws = dst.adjacents.map {|s| @board.spaces[s].wilaya }.uniq! # adjacent Wilayas allowed
+ puts ws.inspect
+ spaces = @board.search{|s| s != dst and ws.include? s.wilaya } # in tree spaces
+ puts spaces.collect{|s| s.name }.join(' :: ')
+ tree = build_tree dst, spaces, want
+ tree.sort{|(x,a),(y,b)| a[:d]<=>b[:d]}.each{|k,v| puts "\t#{v[:d]} #{v[:fln][:max]}:#{v[:fln][:ok]} #{k.name} :: #{v[:adjs].map{|s| s.name}.join(' - ')}" }
+ end
+
+ ##### RALLY OPERATION #####
+ def rally
+ # max 6 spaces unless ope_only => 1
+ selected_max = (limited_ope_only? ? 1 : 6)
+ # up to 2/3 resources expended unless 8 or less resources
+ rcs_max = (@board.fln_resources <= 8 ? @board.fln_resources : @board.fln_resources * 2 / 3)
+ stop_cond = -> { @selected_spaces.length == selected_max or @expended_resources == rcs_max or not may_continue? }
+
+ while not stop_cond.call and @board.available_fln_bases > 0
+ puts ':: may add fln && 3+ FLN && 0 GOV cubes (unless Limited OP)' if @debug
+ break if not _place_base @board.search {|s| may_add_fln_base?(s) and s.guerrillas >= 3 and (limited_ope_only? ? true : s.gov_cubes_0?) and not_selected s }
+ end
+
+ while not stop_cond.call and @board.available_fln_bases > 0
+ puts ':: may add fln && 4+ FLN' if @debug
+ break if not _place_base @board.search {|s| may_add_fln_base?(s) and s.guerrillas >= 4 and not_selected s }
+ end
+
+ while not stop_cond.call or not has_fln_to_place?
+ puts ':: FLN base && ((1+ pop && 1- underground FLN) or ((0 pop || country) && 0 underground FLN))' if @debug
+ spaces = @board.search(:sectors) {|s| s.fln_bases_1m? and ((not s.pop0? and s.fln_u_1l?) or ((s.pop0? or s.country?) and s.fln_u_0?)) and not_selected s }
+ break if not _place_fln_1 spaces
+ end
+
+ if not stop_cond.call
+ puts ':: only once : shift France track towards F' if @debug
+ _shift_france_track
+ end
+
+ while not stop_cond.call and has_fln_to_place?
+ puts ':: non City && Support && 0 underground FLN' if @debug
+ break if not _place_fln_2 @board.search(:sectors) {|s| not (s.city? and s.support?) and s.fln_u_0? and not_selected s }
+ end
+
+ if not @expended_resources == rcs_max and may_continue?
+ puts ':: (FLN control or Base) and 2+ pop && not oppose' if @debug
+ rcs = (rcs_max - @expended_resources)
+ @expended_resources += _reserve_agitate rcs, @board.search(:sectors) {|s| may_agitate? s and s.pop >= 2 and not_selected s }
+ end
+
+ 2.times do
+ break if stop_cond.call or not has_fln_to_place?
+ puts ':: anywhere' if @debug
+ break if not _place_fln_3 @board.search() {|s| not_selected s }
+ end
+
+ 2.times do
+ break if stop_cond.call or not has_fln_to_place?
+ puts ':: no FLN base but 1+ FLN' if @debug
+ break if not _place_fln_4 @board.search() {|s| s.fln_bases_0? and s.guerrillas >= 1 and not_selected s }
+ end
+
+ if @agitate.nil? and may_continue?
+ puts ':: (FLN control or Base) && not oppose' if @debug
+ rcs = (rcs_max - @expended_resources)
+ @expended_resources += _reserve_agitate rcs, @board.search(:sectors) {|s| may_agitate? s }
+ end
+
+ _agitate @agitate if not @agitate.nil? and may_continue?
+
+ if @debug
+ puts "=> Rally done :\n\texpended resources : #{@expended_resources} #{}"
+ puts "\tselected spaces :: " + @selected_spaces.collect(){|s| s.is_a?(Symbol) ? s.to_s : s.name}.join(' :: ')
+ end
+
+ subvert if may_conduct_special_activity? :subvert
+ extort if may_conduct_special_activity? :extort
+
+ return conducted_action
+ # FIXME if NONE => MARCH
+ end
+
+ def _place_base spaces
+ return false if spaces.empty?
+ puts ' => place_base' if @debug
+ selected = spaces[rand(spaces.length)]
+ a = selected.fln_active.clamp(0,2)
+ u = a < 2 ? 2 - a : 0
+ h = get_action :rally, 1, selected
+ transfer h, a, :fln_active, selected, :available, :fln_underground if a != 0
+ transfer h, u, :fln_underground, selected, :available if u != 0
+ transfer h, 1, :fln_base, :available, selected
+ apply_action h
+ end
+
+ def _reserve_agitate max_cost, spaces
+ spaces.select!{|s| not not_selected s } if not has_fln_to_place?
+ spaces.select!{|s| (s.terror + 1 + (not_selected(s) ? 1 : 0)) <= max_cost }
+ return 0 if spaces.empty?
+ spaces.shuffle!
+ spaces.sort! {|a,b|
+ r = b.pop <=> a.pop # most population
+ r = a.terror <=> b.terror if r == 0 # less terror
+ r = (a.support? ? 0 : 1) <=> (b.support? ? 0 : 1) if r == 0 # support
+ r = (not_selected(a) ? 1 : 0) <=> (not_selected(b) ? 1 : 0) if r == 0 # already selected
+ r
+ }
+ @agitate = spaces[0]
+ _place_fln [@agitate] if not_selected @agitate
+ @agitate.terror + 1
+ end
+
+ def _agitate selected
+ puts ' => agitate' if @debug
+ h = get_action :agitate, selected.terror + 1, selected, false
+ h[:already_expended] = true
+ h[:markers] =[ [:alignment, 1, :oppose, selected.alignment, (selected.support? ? :neutral : :oppose)] ]
+ if selected.terror > 0
+ h[:markers].insert(0, [:terror, -selected.terror, nil, selected.terror, 0])
+ @board.terror selected, -selected.terror
+ end
+ @board.shift selected, :oppose
+ apply_action h
+ end
+
+ def _place_fln_1 spaces
+ return false if spaces.empty?
+ puts ' => place_fln_1' if @debug
+ spaces = try_filter(spaces) {|s| not s.country? } # in Algeria
+ spaces = try_filter(spaces) {|s| not s.gov_cubes_0? } # with GOV cubes
+ spaces = try_filter(spaces) {|s| not s.pop0? } # POP 1+
+ spaces.shuffle!
+ spaces.sort! do |a,b|
+ r = a.fln_underground <=> b.fln_underground # least underground FLN
+ r = b.fln_active <=> b.fln_active if r == 0 # most active FLN
+ r
+ end
+ a = spaces[0].fln_underground
+ b = spaces[0].fln_active
+ spaces.select!{|s| s.fln_underground==a and s.fln_active==b }
+ _place_fln spaces
+ end
+
+ def _place_fln_2 spaces
+ return false if spaces.empty?
+ puts ' => place_fln_2' if @debug
+ spaces = sort_filter(spaces, :pop) # highest POP
+ _place_fln spaces
+ end
+
+ def _place_fln_3 spaces
+ return false if spaces.empty?
+ puts ' => place_fln_3' if @debug
+ spaces = try_filter(spaces) {|s| s.uncontrolled? and (s.pop + 1).clamp(0, n) > (s.gov_cubes + s.gov_bases) } # may gain FLN control
+ spaces = try_filter(spaces) {|s| s.gov_control? and (s.pop + 1).clamp(0, n) > (s.gov_cubes + s.gov_bases) } # may remove GOV control
+ spaces = try_filter(spaces) {|s| ['II','IV','V'].include? s.wilaya } # Wilaya with a city
+ spaces = sort_filter(spaces, :terror, :asc) # least terror markers
+ _place_fln spaces
+ end
+
+ def _place_fln_4 spaces
+ return false if spaces.empty?
+ puts ' => place_fln_4' if @debug
+ spaces = try_filter(spaces) {|s| not s.country? } # in Algeria
+ spaces = sort_filter(spaces, :fln) # most FLN
+ spaces = try_filter(spaces) {|s| s.gov_cubes_0? } # no Government cubes
+ _place_fln spaces
+ end
+
+ def _place_fln spaces
+ puts "\t spaces :: " + spaces.collect(){|s| s.is_a?(Symbol) ? s.to_s : s.name}.join(' - ') if @debug
+ spaces = try_filter(spaces) {|s| s.support? } # Support spaces 8.1.2#4
+ spaces = try_filter(spaces) {|s| s.guerrillas > 0 } # friendly 8.1.2#4
+ spaces.shuffle!
+ while not spaces.empty?
+ selected = spaces.shift
+ n = fln_to_place.clamp(0, (selected.pop + 1 - selected.fln)) # at most POP + 1
+ if n == 0 # will only flip underground if #FLN at POP + 1
+ next if selected.fln_active == 0 # will not flip 0 active FLN
+ transfer h, selected.fln_active, :fln_active, selected, selected, :fln_underground
+ return apply_action h
+ end
+ m = n.clamp(0, @board.available_fln_underground)
+ h = get_action :rally, 1, selected
+ transfer h, m, :fln_underground, :available, selected
+ while m < n
+ # leave at least 2 FLN at FLN base or support
+ actives = @board.search(:sectors) {|s| (not s.fln_bases_0? or s.support?) ? s.fln_active > 2 : s.fln_active > 0 }
+ actives = sort_filter(actives, :fln) # the most active
+ space = actives.shift
+ q = space.fln_active
+ q -= 2 if not space.fln_bases_0? or space.support?
+ q = q.clamp(1, n)
+ transfer h, q, :fln_active, space, selected, :fln_underground
+ m += q
+ end
+ return apply_action h
+ end
+ false
+ end
+
+ def _shift_france_track
+ h = get_action :rally, 1, :france_track, false
+ return false if not @board.shift_france_track 1
+ puts ' => shift_france_track' if @debug
+ h[:france_track] = @board.france_track
+ apply_action h
+ end
+
+ ##### ACTIONS #####
+
+ def get_action action, cost, selected, t=true
+ h = { :action => action,
+ :fln_resources => cost,
+ :selected => selected,
+ :controls => {}
+ }
+ h[:transfers] = [] if t
+ h
+ end
+
+ def transfer h, n, what, from, to, towhat=nil
+ towhat = what if towhat.nil?
+ h[:controls][from] ||= from.control unless from.is_a? Symbol
+ h[:controls][to] ||= to.control unless to.is_a? Symbol
+ @board.transfer n, what, from, to, towhat
+ h[:transfers] << { :n => n, :what => what, :from=>from, :to => to, :towhat=> towhat }
+ # puts h[:transfers][-1] if @debug
+ end
+
+ # def revert_transfers h
+ # h[:transfers].each do |tr|
+ # @board.transfer tr[:n], tr[:towhat], tr[:to], tr[:from], tr[:what]
+ # end
+ # end
+
+ OPERATIONS = [:rally, :march, :attack, :terror].freeze
+ SPECIAL_ACTIVITIES = [:extort, :subvert, :ambush, :oas].freeze
+ IGNORE = [:pass, :event, :agitate].freeze
+
+ def apply_action h
+ action = h[:action]
+ if OPERATIONS.include? action
+ operation_done action
+ raise "already selected #{h[:selected].name}" if @selected_spaces.include? h[:selected]
+ @selected_spaces << h[:selected] #unless @selected_spaces.include? h[:selected]
+ puts 'selected spaces :: ' + @selected_spaces.collect(){|s| s.is_a?(Symbol) ? s.to_s : s.name}.join(' :: ') if @debug
+ elsif SPECIAL_ACTIVITIES.include? action
+ special_activity_done action
+ @selected_spaces << h[:selected] if action == :ambush
+ elsif not IGNORE.include? action
+ raise "apply unknown action #{h[:action]}"
+ else
+ # :pass, :event, :agitate
+ end
+ cost = h[:fln_resources]
+ @board.fln_resources -= cost
+ @expended_resources += cost unless h.has_key? :already_expended # _reserve_agitate
+ h[:resources] = {:cost=>cost, :value=>@board.fln_resources}
+ h[:controls].each do |k,v|
+ if v != k.control
+ h[:controls][k] = [v, k.control]
+ else
+ h[:controls].delete k
+ end
+ end
+ @ui.show_player_action self, h
+ true
+ end
+
+ #### HELPERS ####
+
+ def may_continue?
+ return false if limited_ope_done?
+ return true if @board.fln_resources > 0
+ return false if not may_conduct_special_activity? :extort
+ puts "\t=> pause to extort" if @debug
+ extort
+ return @board.fln_resources > 0
+ end
+
+ def has_fln_to_place?
+ fln_to_place > 0
+ end
+
+ def fln_to_place
+ (@board.available_fln_underground + @board.count() {|s| s.fln_active })
+ end
+
+ def may_agitate? s
+ (s.fln_control? or s.fln_bases_1m?) and not s.oppose?
+ end
+
+ def may_add_fln_base? s
+ # return false if (s.support? and s.city?) coltwi ?
+ # max 1 base in Algeria AND at least 1 underground FLN
+ (s.fln_bases < (s.country? ? s.max_bases : 1)) and (s.fln_underground > 0)
+ end
+
+ def activate to, from, flns
+ # goes active if moved into Support or crossed International Border && #FLN + GOV cubes (+border) > 3
+ ( (to.support? or from.country?) and (flns + to.gov_cubes + (from.country? ? @board.border_zone_track : 0)) > 3 )
+ end
+
+ def build_tree dst, spaces, want
+ tree = ([dst] + spaces).inject({}) do |h,s|
+ # filter out adjacents : dst OR adjacent to dst OR same wilaya
+ a = s.adjacents.map{|n| @board.spaces[n]}.select{|a| a == dst or (spaces.include?(a) and (s.wilaya == a.wilaya or s == dst))}
+ h[s] = { :adjs=> a, :fln => can_march_from(s, want), :d => 0}
+ h
+ end
+ q = [dst]
+ while not q.empty?
+ s = q.shift
+ h = tree[s]
+ d = h[:d] + 1
+ h[:adjs].each do |a|
+ next if a == dst
+ p = tree[a][:d]
+ if p == 0 or d < p
+ tree[a][:d] = d
+ q << a
+ end
+ end
+ end
+ tree
+ end
+
+ def can_march_from s, want, with={}
+ u = s.fln_underground + (with[:fln_underground]||0)
+ a = s.fln_active + (with[:fln_active]||0)
+ d = 0
+ if not s.fln_bases_0?
+ # at bases, leave last underground FLN, leave 2 FLN
+ if u > 0
+ u -= 1
+ d = 1
+ else
+ a = (a > 2 ? a - 2 : 0)
+ end
+ end
+ if (s.fln_bases_0? and s.support?) or d == 1
+ # march with underground FLN, that could be swaped with an active FLN
+ d = ((a > 0 and u > 0) ? 1 : 0)
+ if a > 0
+ a -= 1
+ elsif u > 0
+ u -= 1
+ end
+ end
+ # never trigger GOV Control on populated spaces
+ max = [(not s.country? and not s.pop0? and not s.gov_control?) ? (s.fln - s.gov) : 666, u + a].min
+ # does it satisfy want conditions
+ ok = (u >= (want[:fln_underground]||0) and a >= (want[:fln_active]||0) and max >= (want[:fln]||1))
+ { :fln_underground=>u, :fln_active=>a, :delta=>d, :max=>max, :ok=>ok }
+ end
+
+ def last_campaign?
+ false # FIXME
+ end
+
+ def possible_extort
+ # FIXME : if resources at 0 and no spaces
+ # 1+ POP && (FLN control or country) && (1+ FLN underground or 2+ FLN if 1+ FLN bases)
+ spaces = @board.search() {|s| not s.pop0? and (s.fln_control? or s.country?) and s.fln_u_1m? and (s.country? or s.fln_bases_0? or s.fln_u_2m?) }
+ spaces.inject(0) {|n,s| n + s.fln_underground - ((not s.fln_bases_0? and not s.country?) ? 1 : 0) }
+ end
+
+ def pacifiable? s
+ # FIXME if Recall de Gaulle 8.4.2 third bullet point
+ not s.country? and (not s.gov_bases_0? or (s.troop >= 1 and s.police >= 1 and s.gov_control?))
+ end
+
+ ##### FILTERS #####
+
+ def not_selected s
+ not @selected_spaces.include? s
+ end
+
+ def try_filter list, &block
+ filtered = list.select &block
+ (filtered.empty? ? list : filtered)
+ end
+
+ def sort_filter spaces, sym, order=:desc
+ spaces.shuffle!
+ if order == :desc
+ spaces.sort! {|a,b| b.send(sym) <=> a.send(sym) }
+ else
+ spaces.sort! {|a,b| a.send(sym) <=> b.send(sym) }
+ end
+ v = spaces[0].send(sym)
+ spaces.select {|s| s.send(sym) == v }
end
end