diff options
author | Jérémy Zurcher <jeremy@asynk.ch> | 2023-12-05 11:29:20 +0100 |
---|---|---|
committer | Jérémy Zurcher <jeremy@asynk.ch> | 2023-12-05 11:29:20 +0100 |
commit | 499cc01cd966e96fe6371a36e0cffcb073806079 (patch) | |
tree | c5ed4dab2351e6e981da62dfb423492e4efbb34f /lib | |
parent | 5b489f8e17a8b5178e9c4b55eee22f0f72d33073 (diff) | |
download | colonial-twilight-499cc01cd966e96fe6371a36e0cffcb073806079.zip colonial-twilight-499cc01cd966e96fe6371a36e0cffcb073806079.tar.gz |
FLNBot : rewrite
Diffstat (limited to 'lib')
-rw-r--r-- | lib/colonial_twilight/fln_bot.rb | 924 | ||||
-rw-r--r-- | lib/colonial_twilight/player.rb | 95 |
2 files changed, 310 insertions, 709 deletions
diff --git a/lib/colonial_twilight/fln_bot.rb b/lib/colonial_twilight/fln_bot.rb index 1b8b6a3..a3f3d20 100644 --- a/lib/colonial_twilight/fln_bot.rb +++ b/lib/colonial_twilight/fln_bot.rb @@ -1,760 +1,400 @@ #! /usr/bin/env ruby -# -*- coding: UTF-8 -*- +# frozen_string_literal: true -module ColonialTwilight - - # Country.independant +require 'colonial_twilight/player' +require 'colonial_twilight/fln_rules' +require 'colonial_twilight/fln_bot_rules' +module ColonialTwilight class FLNBot < Player + include ColonialTwilight::FLNRules + include ColonialTwilight::FLNBotRules - def play possible_actions - @possible_actions = possible_actions - _init - _start + def play_turn(prev_action, possible_actions) + init_turn prev_action, possible_actions + _start_turn 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 + def printd(msg) + return if @debug.zero? - return _march_or_rally + puts msg 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 + def apply_action(action) + @game.apply(:FLN, action) 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? + def available_resources + resources - (@reserved_to_agitate || 0) + end - subvert if may_conduct_special_activity? :subvert - extort if may_conduct_special_activity? :extort + def event_playable? + # FIXME: event is FLN playable + false + end - return conducted_action + def event_more_effective_than_terror? + # FIXME: event would reduce GOV victory margin by as much or more than terror + false 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 + def last_campaign? + # FIXME: the next Propaganda Card will be the last one of the game + true 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 + # PASS ##################################################################### + + def pass + apply_action @turn.pass(1) 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 + # EXTORT ################################################################### - # 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 + 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? - # 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 + apply_action @turn.special_activity_in(:extort, space, -1, to_agitate_in: to_agitate_in).extort 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 + def extortable(except: nil) + @board.search { |s| may_extort_0_in?(s) }.reject { |s| @turn.special_activity_selected?(s) || s == except } 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 + # TERROR ################################################################### + + def terror + # return false if !available_resources.positive? && !extort + return false if event_playable? && event_more_effective_than_terror? - ##### 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?) } + 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) - # 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 + apply_action @turn.operation_in(:terror, space, 1).terror end - # Morocco and Tunisia - spaces.select{|s| s.country? }.each do |selected| - _extort selected - spaces.delete selected + 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 - # if still at 0 resources, everywhere possible - spaces.each do |selected| _extort selected end if @board.fln_resources == 0 + @turn.operation_done? end - def _extort selected - h = get_action :extort, -1, selected - transfer h, 1, :fln_underground, selected, selected, :fln_active - apply_action h - end + # ATTACK ################################################################### - ##### 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 + # return false if !available_resources.positive? && !extort - 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 + 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 - extort if may_conduct_special_activity? :extort + until (space = attack_priority(@board.search { |s| may_attack_2_in?(s) }).sample).nil? + break if !available_resources.positive? && !extort - return conducted_action + _apply_attack(space, ambush_cond.call(space)) + n -= 1 end - return _march_or_rally + @turn.operation_done? 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 + def _apply_attack(space, ambush) + apply_action ambush ? _ambush(space) : _attack(space) 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? } - - 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(' :: ') - spaces.each do |space| - d = _paths(space, {:fln_underground=>1}) {|h| h[:fln_underground] > 0 } - end - - 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 + def _ambush(space) + action = @turn.special_activity_in(:ambush, space, 1).activate(1) + casualties = _casualties(space, action, 1) + _attrition(action, casualties) end - def _paths dst, want - ws = dst.adjacents.map {|s| @board.spaces[s].wilaya }.uniq! # adjacent Wilayas allowed - spaces = @board.search{|s| s != dst and ws.include? s.wilaya } # corresponding spaces - puts "DST : #{dst}\nWilayas : #{ws.inspect}\n" + spaces.collect{|s| s.name }.join(' :: ') if @debug - paths, tree = build_paths dst, ws, 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(' - ')}" } - paths.each{|p| puts p.collect{|s| s.name}.join ' -> '} + 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 - ##### 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 + def _casualties(space, action, casualties) + num = 0 + CASUALTIES_PRIORITY.each do |sym| + next unless (n = space.send(sym)).positive? - 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 } + 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 - 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 + def _attrition(action, casualties) + action.transfer_to(:available, :fln_active, (casualties + 1) / 2) + .transfer_to(:casualties, :fln_active, casualties / 2) + end - if not stop_cond.call - puts ':: only once : shift France track towards F' if @debug - _shift_france_track - end + # SUBVERT ################################################################## - 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 + def subvert + return false if (spaces = subvert_spaces(@board)).empty? - 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 + 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? - 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 } + 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? - 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 } + 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? - 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 } + 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 - _agitate @agitate if not @agitate.nil? and may_continue? + 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 - if @debug - puts "=> Rally done :\n\texpended resources : #{@expended_resources} #{}" - debug_selected_spaces - 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 - subvert if may_conduct_special_activity? :subvert - extort if may_conduct_special_activity? :extort + # RALLY #################################################################### - return conducted_action - # FIXME if NONE => MARCH - end + def rally + # return false if !available_resources.positive? && !extort - 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 + @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 } - 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 + 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 } - 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 + loop do + break unless _place_base_in(_rally(1, stop_cond_base, ->(s) { may_rally_1_in?(s) })) 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 + loop do + break unless _place_base_in(_rally(2, stop_cond_base, ->(s) { may_rally_2_in?(s) })) 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 + loop do + break unless _place_fln_in(_rally(3, stop_cond, ->(s) { may_rally_3_in?(s) }, priority: 3)) + 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 + _shift_france_track unless stop_cond.call - 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 + loop do + break unless _place_fln_in(_rally(5, stop_cond, ->(s) { may_rally_5_in?(s) }, priority: 5)) + 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 + 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 - 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 + + 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 - return apply_action h end - false - end + _agitate_in(agitate_in, max_cost.call) - def _shift_france_track - h = get_action :rally, 1, :france_track, false - return false if not @board.shift_track :france_track, 1 - puts ' => shift_france_track' if @debug - h[:france_track] = @board.france_track - apply_action h + @turn.operation_done? end - ##### ACTIONS ##### + def _rally(num, stop_cond, filter, priority: nil, reselect: false) + return nil if stop_cond.call - def get_action action, cost, selected, t=true - h = { :action => action, - :fln_resources => cost, - :selected => selected, - :controls => {} - } - h[:transfers] = [] if t - h - end + printd(" rally #{num}") + return nil if (space = _rally_one_space(filter, priority: priority, reselect: reselect)).nil? - def transfer h, n, what, from, to, towhat=nil - h[:controls][from] ||= from.control unless from.is_a? Symbol - h[:controls][to] ||= to.control unless to.is_a? Symbol - h[:transfers] << @board.transfer( n, what, from, to, towhat) - # puts h[:transfers][-1] if @debug + printd(" -> #{space.name}") + extort unless available_resources.positive? + + available_resources.positive? ? space : nil 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 - selected = h[:selected] - action = h[:action] - if OPERATIONS.include? action - operation_done action - raise "already selected #{selected.name}" if @selected_spaces.include? selected - @selected_spaces << selected - debug_selected_spaces - elsif SPECIAL_ACTIVITIES.include? action - special_activity_done action - @selected_spaces << 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.shift_track :fln_resources, -cost - @expended_resources += cost unless h.has_key? :already_expended # _reserve_agitate - h[:resources] = {:cost=>cost, :value=>@board.fln_resources} - h[:controls] = h[:controls].inject({}){|ch,(k,v)| ch[k] = [v, k.control] if v != k.control; ch} - @game.action_done self, h - true + 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 - #### HELPERS #### + def _place_priority(spaces, priority) + return spaces if spaces.size < 2 - 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 + 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 has_fln_to_place? - fln_to_place > 0 - end + def _place_base_in(space) + return false if space.nil? - def fln_to_place - (@board.available_fln_underground + @board.count() {|s| s.fln_active }) + 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 may_agitate? s - (s.fln_control? or s.fln_bases_1m?) and not s.oppose? - end + def _place_fln_in(space, to_agitate_in: nil) + return false if space.nil? - 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 + printd " => _place_fln_in : #{space.name}" + return false if (steps = place_guerrillas_in(space)).empty? - 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 ) + apply_action @turn.operation_in(:rally, space, 1, to_agitate_in: to_agitate_in).transfer_steps(steps) end - def build_paths dst, ws, 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 - # filter_out dst, ws, tree - build_shortest_paths tree + 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 filter_out dst, ws, tree - ws.each do |w| - h = tree.select{|s,h| s != dst and s.wilaya == w} - next if h.inject(false) {|b,(s,h)| b||h[:fln][:ok]} - h.each {|s,v| - tree.delete s - tree[dst][:adjs].delete s - } + 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 - end - def build_shortest_paths tree - paths = [] - def dfs s, tree, paths, path - l = tree[s][:adjs].sort{|a,b| tree[a][:d] <=> tree[b][:d]} - d = tree[l[0]][:d] - if d == 0 - paths << (path << l[0]) - else - l.each do |a| - break if tree[a][:d] > d - dfs a, tree, paths, path.clone << a - end - end + if max_cost.positive? && (cost = (terror + oppose)) > max_cost + terror -= (cost - oppose - max_cost) + oppose = 0 end - tree.select{|k,v| v[:fln][:ok]}.each{|k,v| dfs k, tree, paths, [k] } - [paths, tree] - end + return if terror.zero? - 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 + if (cost = terror + oppose) < available_resources + return apply_action @turn.agitate_in(space, terror, oppose) 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 + + max_rcs = available_resources + extortable.size + if cost > max_rcs + terror -= (cost - oppose - max_rcs) + oppose = 0 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 + return if terror.zero? - def last_campaign? - false # FIXME + ((terror + oppose) - available_resources).times { extort(to_agitate_in: space) } + apply_action @turn.agitate_in(space, terror, oppose) 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 _reserve_to_agitate_in?(space, max_cost) + return false if space.nil? - 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 + 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? - ##### FILTERS ##### + 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? - def not_selected s - not @selected_spaces.include? s + ((rally_cost + agitate_cost) - available_resources).times { extort(to_agitate_in: space) } + @reserved_to_agitate = agitate_cost + true end - def try_filter list, &block - filtered = list.select &block - (filtered.empty? ? list : filtered) - end + # MARCH# ################################################################### - 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 + def march + return false if event_playable? && event_more_effective_than_terror? + # FIXME + end end - end diff --git a/lib/colonial_twilight/player.rb b/lib/colonial_twilight/player.rb index 123475b..a4b01ed 100644 --- a/lib/colonial_twilight/player.rb +++ b/lib/colonial_twilight/player.rb @@ -1,98 +1,59 @@ #! /usr/bin/env ruby -# -*- coding: UTF-8 -*- +# frozen_string_literal: true -module ColonialTwilight +require 'colonial_twilight/turn' +module ColonialTwilight class Player + attr_reader :faction, :game, :turn - attr_reader :faction - - def initialize game, faction + def initialize(game, faction) @game = game @board = game.board @faction = faction - @debug = game.options.debug_bot - @possible_actions = nil + @debug = game.options.debug + @turn = Turn.new end - def _init - @card = @game.current_card - @operation = nil - @operation_count = 0 - @special_activity = nil - @special_activity_count = 0 - - @selected_spaces = [] - @expended_resources = 0 + def resources + @faction == :FLN ? @board.fln_resources : @board.gov_resources end - 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 + def score + @faction == :FLN ? @board.opposition_bases : @board.support_commitment 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 + def init_turn(prev_action, possible_actions) + @prev_action = prev_action + @possible_actions = possible_actions + @card = @game.current_card + @turn.reset(limited_op_only?) end - def limited_ope_only? - (@possible_actions.size == 2 and @possible_actions.include? :ope_limited) + def d6 + @game.d6 end - def limited_ope_done? - limited_ope_only? and @operation_count == 1 + def first_eligible? + @game.first == self end - def may_conduct_special_activity? sp - r = @possible_actions.include? :ope_special - r &= (sp == @special_activity) if not @special_activity.nil? - r + def will_be_next_first_eligible? + Game.swap_actions.include? @prev_action 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 + def may_play_event? + @possible_actions.include?(:event) 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 + def limited_op_only? + @possible_actions.size == 2 && @possible_actions.include?(:op_limited) end - def debug_selected_spaces - puts "\tselected spaces :: " + @selected_spaces.collect(){|s| s.is_a?(Symbol) ? s.to_s : s.name}.join(' :: ') if @debug + def limited_op_done? + limited_op_only? && !@turn.operation_done? end - end - end |