diff options
| author | Jérémy Zurcher <jeremy@asynk.ch> | 2020-09-01 15:41:09 +0200 | 
|---|---|---|
| committer | Jérémy Zurcher <jeremy@asynk.ch> | 2020-09-01 15:41:09 +0200 | 
| commit | a537cb64b77793387d2529467d4291198fc7fa70 (patch) | |
| tree | 066221cddae67446900ace94e844c5980a81169a | |
| parent | 996b12185fdece116f73d20cb57eae322ecef2f2 (diff) | |
| download | colonial-twilight-a537cb64b77793387d2529467d4291198fc7fa70.zip colonial-twilight-a537cb64b77793387d2529467d4291198fc7fa70.tar.gz  | |
FLNBot : good, still misses Event and March support
| -rw-r--r-- | lib/colonial_twilight/fln_bot.rb | 826 | 
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  | 
