summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--lib/colonial_twilight/fln_bot_rules.rb160
-rw-r--r--spec/fln_bot_rules_spec.rb282
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