diff options
| author | Jérémy Zurcher <jeremy@asynk.ch> | 2023-10-03 22:44:09 +0200 | 
|---|---|---|
| committer | Jérémy Zurcher <jeremy@asynk.ch> | 2023-10-03 22:44:09 +0200 | 
| commit | dcc1cc51c4ac9eb80431999fef79f0d46728073b (patch) | |
| tree | 4cbe48268310ca65ec294a8687c532da6863b951 | |
| parent | 1e45085b7730bdb9b0b0ed76e6613a9fac5136cd (diff) | |
| download | colonial-twilight-dcc1cc51c4ac9eb80431999fef79f0d46728073b.zip colonial-twilight-dcc1cc51c4ac9eb80431999fef79f0d46728073b.tar.gz  | |
add fln_bot_rules & specs
| -rw-r--r-- | lib/colonial_twilight/fln_bot_rules.rb | 160 | ||||
| -rw-r--r-- | spec/fln_bot_rules_spec.rb | 282 | 
2 files changed, 442 insertions, 0 deletions
diff --git a/lib/colonial_twilight/fln_bot_rules.rb b/lib/colonial_twilight/fln_bot_rules.rb new file mode 100644 index 0000000..e9b9e79 --- /dev/null +++ b/lib/colonial_twilight/fln_bot_rules.rb @@ -0,0 +1,160 @@ +#! /usr/bin/env ruby +# frozen_string_literal: true + +module ColonialTwilight +  module FLNBotRules +    def dbg(msg, ret) +      return if @debug.zero? + +      case @debug +      when 1 then puts "  #{msg} : YES" if ret +      else puts "  #{msg} : #{ret ? 'YES' : 'NO'}" +      end +    end + +    def pass? +      # if resources = 0 && Op Limited as only choice +      r = @board.fln_resources.zero? && limited_op_only? +      dbg 'PASS', r +      r +    end + +    def terror1? +      # unless any 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? +      # 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? +      # rally would place bases (first 2 bullets) +      # 4+ or (3+ FLN and no GOV (unless limited_op_only)) +      r = @board.available_fln_bases.positive? && @board.has do |s| +        may_add_base_in?(s) && (rally_2_in?(s) || rally_1_in?(s)) +      end +      dbg 'RALLY 1', r +      r +    end + +    def rally2? +      # 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.fln_cubes } +      r = a > (b + d6 / 2) +      dbg 'RALLY 2', r +      r +    end + +    # Rally + +    def may_add_base_in?(space) +      r = space.fln_cubes > 2 && (space.fln_bases < (space.country? ? space.max_bases : 1)) +      dbg "  may_add_base : #{space.name}", r +      r +    end + +    def rally_1_in?(space) +      # 3+ FLN and no GOV (unless limited_op_only)) +      r = space.fln_cubes >= 3 && (limited_op_only? ? true : space.gov_cubes.zero?) +      dbg "  rally_1_in : #{space.name}", r +      r +    end + +    def rally_2_in?(space) +      # 4+ FLN +      r = space.fln_cubes >= 4 +      dbg "  rally_2_in : #{space.name}", r +      r +    end + +    def rally_3_in?(space) +      # at FLN bases, with 2- FLN underground or 0 fln_undergroud in country or 0 pop +      r = !space.fln_bases.zero? && +          (space.country? || space.pop.zero? ? space.fln_underground.zero? : space.fln_underground < 2) +      dbg "  rally_3_in : #{space.name}", r +      r +    end + +    def rally_3_priority(spaces) +      # Algeria -> with cubes -> pop 1+ -> least FLN underground +      l0 = (l0 = spaces.reject(&:country?)).empty? ? spaces : l0 +      l1 = (l1 = l0.select { |s| s.gov_cubes.positive? }).empty? ? l0 : l1 +      l2 = (l2 = l1.select { |s| s.pop.positive? }).empty? ? l1 : l2 +      l2.min { |a, b| a.fln_underground <=> b.fln_underground } +    end + +    def rally_5_in?(space) +      # non-city at support with 0 FLN underground +      r = !space.city? && space.support? && space.fln_underground.zero? +      dbg "  rally_5_in : #{space.name}", r +      r +    end + +    def rally_6_in?(space) +      # 2+ pop to agitate +      r = space.pop > 1 && may_agitate_in?(space) +      dbg "  rally_6_in : #{space.name}", r +      r +    end + +    def rally_8_in?(space) +      r = !space.fln_cubes.zero? && space.fln_bases.zero? +      dbg "  rally_8_in : #{space.name}", r +      r +    end + +    # 8.1.2 - Procedure Guidelines +    def fln_to_place? +      @board.available_fln_underground.positive? || !place_from(@board.spaces).nil? +    end + +    # 1) place: outofplay -> available | bases -> cubes 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 + +    def place_in(spaces) +      # 4) support -> with friendly pieces -> random +      l0 = (l0 = spaces.select(&:support?)).empty? ? spaces : l0 +      l1 = (l1 = l0.select { |s| s.fln_cubes.positive? }).empty? ? l0 : l1 +      (l1.empty? ? @board.spaces : l1).sample +    end + +    def place_from(spaces) +      # 5) active only, leave 2 cubes at base or support, most cubes first +      l = spaces.select do |s| +        s.fln_active.positive? && (s.support? || s.fln_bases.positive? ? s.fln_cubes > 2 : true) +      end +      return nil if l.empty? + +      v = l.max { |a, b| a.fln_cubes <=> b.fln_cubes }.fln_cubes +      l.select { |s| s.fln_cubes == v }.sample +    end + +    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 + +    def remove_gov(num = 1) +      # 7) map -> availabe (base -> french -> algerian; troops -> police) +    end + +      # 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/spec/fln_bot_rules_spec.rb b/spec/fln_bot_rules_spec.rb new file mode 100644 index 0000000..2330ac1 --- /dev/null +++ b/spec/fln_bot_rules_spec.rb @@ -0,0 +1,282 @@ +# frozen_string_literal: true + +require './lib/colonial_twilight/fln_bot_rules' +require './lib/colonial_twilight/game' + +class FLNRulesImpl +  include ColonialTwilight::FLNBotRules +  def initialize +    @debug = 0 +    @board = nil +  end +  attr_writer :board + +  def limited_op_only? +    true +  end + +  def d6 +    5 +  end + +  # def first_eligible? +  #   true +  # end +  # +  # def will_be_next_first_eligible? +  #   true +  # end +end + +describe ColonialTwilight::FLNBotRules do + +  rules = FLNRulesImpl.new +  before do +    @board = ColonialTwilight::Board.new +    @board.load :short +    rules.board = @board +  end + +  describe 'Pass' do +    it 'pass? no' do +      expect(rules.pass?).to be false +    end + +    it 'pass? no resources' do +      @board.shift_track :fln_resources, -15 +      expect(rules.pass?).to be true +    end +  end + +  describe 'Terror' do +    it 'terror1? no' do +      expect(rules.terror1?).to be false +    end + +    it 'terror1? all bases are protected' do +      @board.by_name('Souk Ahras').add :fln_underground +      @board.by_name('Tizi Ouzou').add :fln_underground +      @board.by_name('Bougie').add :fln_underground +      @board.by_name('Orleansville').add :fln_underground +      expect(rules.terror1?).to be true +    end + +    # it 'terror2? true' do +    #   expect(rules.terror2?).to be false +    # end +  end + +  describe 'Rally' do + +    it 'rally1? false' do +      expect(rules.rally1?).to be false +    end + +    it 'rally1? may rally_1' do +      space = @board.by_name('Barika') +      space.add :fln_active, 2 +      expect(rules.rally1?).to be true +    end + +    it 'rally1? may rally_2' do +      space = @board.by_name('Barika') +      space.add :fln_active, 3 +      expect(rules.rally1?).to be true +    end + +    it 'rally2? enough fln at bases' do +      expect(rules.rally2?).to be true +    end + +    it 'rally2? false' do +      @board.by_name('Bougie').add :fln_active, 1 +      expect(rules.rally2?).to be false +    end + +    it 'may_add_base_in?' do +      expect(@board.spaces.select(&rules.method(:may_add_base_in?)).size).to eq(0) +    end + +    it 'may_add_base_in?' do +      @board.by_name('Batna').add :fln_active, 3 +      expect(@board.spaces.select(&rules.method(:may_add_base_in?))[0].name).to eq('Batna') +    end + +    it 'may_add_base_in?' do +      space = @board.by_name('Batna') +      space.add :fln_active, 6 +      space.add :fln_base, 1 +      expect(@board.spaces.select(&rules.method(:may_add_base_in?)).size).to eq(0) +    end + +    it 'rally_1_in? not enough fln cubes' do +      space = @board.by_name('Barika') +      expect(rules.rally_1_in?(space)).to be false +    end + +    it 'rally_1_in? enough fln cubes' do +      space = @board.by_name('Barika') +      space.add :fln_active, 2 +      expect(rules.rally_1_in?(space)).to be true +    end + +    it 'rally_2_in? not enough fln cubes' do +      space = @board.by_name('Barika') +      expect(rules.rally_2_in?(space)).to be false +    end + +    it 'rally_2_in? enough fln cubes' do +      space = @board.by_name('Barika') +      space.add :fln_active, 3 +      expect(rules.rally_2_in?(space)).to be true +    end + +    it 'rally_3_in? no base' do +      space = @board.by_name('Batna') +      expect(rules.rally_3_in?(space)).to be false +    end + +    it 'rally_3_in? base and 0 pop and 0 fln underground' do +      space = @board.by_name('Batna') +      space.add :fln_base, 1 +      expect(rules.rally_3_in?(space)).to be true +    end + +    it 'rally_3_in? base and 0 pop but fln underground' do +      space = @board.by_name('Batna') +      space.add :fln_base, 1 +      space.add :fln_underground, 1 +      expect(rules.rally_3_in?(space)).to be false +    end + +    it 'rally_3_in? base and not enough fln underground' do +      space = @board.by_name('Bougie') +      expect(rules.rally_3_in?(space)).to be true +    end + +    it 'rally_3_in? base but not enough fln underground' do +      space = @board.by_name('Bougie') +      space.add :fln_underground, 1 +      expect(rules.rally_3_in?(space)).to be false +    end + +    it 'rally_3_priority Algeria' do +      a = @board.by_name('Ain Oussera') +      b = @board.by_name('Morocco') +      b.add :fln_base, -2 +      b.add :fln_underground, -4 +      expect(rules.rally_3_priority([a, b]).name).to eq('Ain Oussera') +    end + +    it 'rally_3_priority with GOV cubes' do +      a = @board.by_name('Mecheria') +      b = @board.by_name('Saida') +      expect(rules.rally_3_priority([a, b]).name).to eq('Mecheria') +    end + +    it 'rally_3_priority pop 1+' do +      a = @board.by_name('Ain Oussera') +      b = @board.by_name('Sidi Aissa') +      expect(rules.rally_3_priority([a, b]).name).to eq('Ain Oussera') +    end + +    it 'rally_3_priority least fln_underground' do +      a = @board.by_name('Tebessa') +      a.add :fln_underground, 1 +      b = @board.by_name('Barika') +      expect(rules.rally_3_priority([a, b]).name).to eq('Barika') +    end + +    it 'rally_5_in? city' do +      space = @board.by_name('Oran') +      expect(rules.rally_5_in?(space)).to be false +    end + +    it 'rally_5_in? support but fln underground' do +      space = @board.by_name('Barika') +      space.shift :support +      expect(rules.rally_5_in?(space)).to be false +    end + +    it 'rally_5_in? support and fln underground' do +      space = @board.by_name('Barika') +      space.shift :support +      space.add :fln_underground, -1 +      expect(rules.rally_5_in?(space)).to be false +    end + +    it 'rally_6_in? 2+ pop but no fln control' do +      space = @board.by_name('Medea') +      expect(rules.rally_6_in?(space)).to be false +    end + +    it 'rally_6_in? 2+ pop and fln' do +      space = @board.by_name('Medea') +      space.add :fln_active, 3 +      expect(rules.rally_6_in?(space)).to be true +    end + +    it 'rally_8_in? no fln cubes' do +      space = @board.by_name('Tiaret') +      expect(rules.rally_8_in?(space)).to be false +    end + +    it 'rally_8_in? fln cubes but base' do +      space = @board.by_name('Bougie') +      expect(rules.rally_8_in?(space)).to be false +    end + +    it 'rally_8_in? fln cubes and 0 base' do +      space = @board.by_name('Barika') +      expect(rules.rally_8_in?(space)).to be true +    end +  end + +  describe '8.1.2 Procedure Guidelines' do +    it 'place_in' do +      expect(rules.place_in(@board.spaces).city?).to be true +    end + +    it 'place_in support' do +      space = @board.by_name('Mostaganem') +      space.add :fln_active, 3 +      expect(rules.place_in(@board.spaces).city?).to be true +    end + +    it 'place_in support and fln_active' do +      space = @board.by_name('Mostaganem') +      space.shift :support +      space.add :fln_active, 3 +      expect(rules.place_in(@board.spaces).name).to be 'Mostaganem' +    end + +    it 'place_from none' do +      expect(rules.place_from(@board.spaces).nil?).to be true +    end + +    it 'place_from most active' do +      @board.by_name('Oran').add :fln_active, 1 +      @board.by_name('Batna').add :fln_active, 3 +      @board.by_name('Negrine').add :fln_active, 2 +      expect(rules.place_from(@board.spaces).name).to be 'Batna' +    end + +    it 'remove_from all' do +      space = @board.by_name('Bougie') +      space.add :fln_active, 2 +      h = rules.remove_from(space, 6) +      expect(h[:fln_underground]).to be 1 +      expect(h[:fln_active]).to be 2 +      expect(h[:fln_bases]).to be 1 +    end + +    it 'remove_from a few' do +      space = @board.by_name('Bougie') +      space.add :fln_active, 1 +      h = rules.remove_from(space, 2) +      expect(h[:fln_underground]).to be 1 +      expect(h[:fln_active]).to be 1 +      expect(h[:fln_bases]).to be 0 +    end +  end +end  | 
