summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJérémy Zurcher <jeremy@asynk.ch>2023-12-05 11:28:48 +0100
committerJérémy Zurcher <jeremy@asynk.ch>2023-12-05 11:28:48 +0100
commit5b489f8e17a8b5178e9c4b55eee22f0f72d33073 (patch)
treecb49b4dbba8798afb33b2650c45f8fd76354daae
parent87c981c0dd14b15f94936ab63368fff59f436276 (diff)
downloadcolonial-twilight-5b489f8e17a8b5178e9c4b55eee22f0f72d33073.zip
colonial-twilight-5b489f8e17a8b5178e9c4b55eee22f0f72d33073.tar.gz
add Turn and specs
-rw-r--r--lib/colonial_twilight/turn.rb239
-rw-r--r--spec/turn_spec.rb205
2 files changed, 444 insertions, 0 deletions
diff --git a/lib/colonial_twilight/turn.rb b/lib/colonial_twilight/turn.rb
new file mode 100644
index 0000000..34690e7
--- /dev/null
+++ b/lib/colonial_twilight/turn.rb
@@ -0,0 +1,239 @@
+#! /usr/bin/env ruby
+# frozen_string_literal: true
+
+module ColonialTwilight
+ OPERATIONS = {
+ pass: 'Pass',
+ rally: 'Rally',
+ march: 'March',
+ attack: 'Attack',
+ terror: 'Terror'
+ }.freeze
+
+ SPECIAL_ACTIVITIES = {
+ extort: 'Extort',
+ subvert: 'Subvert',
+ ambush: 'Ambush',
+ oas: 'OAS'
+ }.freeze
+
+ class Action
+ attr_reader :type, :space, :cost, :steps, :to_agitate_in
+ attr_accessor :resources
+
+ def initialize(type, space, cost, to_agitate_in: nil)
+ @type = type
+ @space = space
+ @cost = cost
+ @steps = []
+ @resources = 0
+ @to_agitate_in = to_agitate_in
+ end
+
+ def sanitize!
+ map = { src_control: :src, dst_control: :dst }
+ control = _collect_indexes(map)
+ control.each do |k, v|
+ v.pop
+ v.each do |i|
+ step = steps[i]
+ step.delete(step[:src] == k ? :src_control : :dst_control)
+ end
+ end
+ end
+
+ def _collect_indexes(map)
+ hash = {}
+ steps.each_with_index do |step, i|
+ map.each do |k, v|
+ if step.key?(k)
+ hash[step[v]] ||= []
+ hash[step[v]] << i
+ end
+ end
+ end
+ hash
+ end
+
+ def name
+ return 'Agitate' if @type == :agitate
+
+ operation? ? OPERATIONS[@type] : SPECIAL_ACTIVITIES[@type]
+ end
+
+ def operation?
+ OPERATIONS.keys.include? @type
+ end
+
+ def special_activity?
+ SPECIAL_ACTIVITIES.keys.include? @type
+ end
+
+ def transfer_steps(steps)
+ steps.each do |k, v|
+ transfer_from(k, :fln_underground, v)
+ end
+ self
+ end
+
+ def pass
+ @steps << { kind: :pass }
+ self
+ end
+
+ def activate(num = 1)
+ @steps << { kind: :activate, src: @space, num: num } if num.positive?
+ self
+ end
+
+ def transfer_to(dst, what, num = 1, flip: false)
+ @steps << { kind: :transfer, src: @space, dst: dst, what: what, num: num, flip: flip } if num.positive?
+ self
+ end
+
+ def transfer_from(src, what, num = 1, flip: false)
+ @steps << { kind: :transfer, src: src, dst: @space, what: what, num: num, flip: flip } if num.positive?
+ self
+ end
+
+ def shift(num)
+ @steps << { kind: :shift, dst: @space, num: num } unless num.zero?
+ self
+ end
+
+ def extort
+ @steps << { kind: :extort, src: @space, what: :fln_underground, flip: true }
+ self
+ end
+
+ def terror
+ @steps << { kind: :set, src: @space, what: :terror, terror: 1 }
+ @steps << { kind: :set, src: @space, what: :alignment, alignment: :neutral }
+ self
+ end
+
+ def agitate(terror, oppose)
+ @steps << { kind: :agitate, src: @space, terror: terror, shift: oppose }
+ self
+ end
+
+ def inspect
+ "action #{@type} in '#{@space}' cost: #{@cost} #{_to_agitate} : #{_steps}"
+ end
+
+ def _to_agitate
+ @to_agitate_in.nil? ? '' : " - to agitate in #{@to_agitate_in}"
+ end
+
+ def _steps
+ @steps.inject('') { |r, s| r + "\n #{s.inspect}" }
+ end
+ end
+
+ class Turn
+ attr_reader :actions, :operation, :special_activity
+
+ def initialize
+ reset(false)
+ end
+
+ def reset(limited_op_only)
+ @operation = nil
+ @special_activity = nil
+ @actions = []
+ @limited_op_only = limited_op_only
+ end
+
+ def operation_done?
+ !@operation.nil?
+ end
+
+ def special_activity_done?
+ !@special_activity.nil?
+ end
+
+ def may_special_activity?(special_activity)
+ @special_activity.nil? || @special_activity == special_activity
+ end
+
+ def operation_spaces
+ @actions.select(&:operation?).size
+ end
+ alias selected_spaces operation_spaces
+
+ def special_activity_spaces
+ @actions.select(&:special_activity?).size
+ end
+
+ def operation_selected?(space)
+ !@actions.select(&:operation?).find { |a| a.space == space }.nil?
+ end
+
+ def special_activity_selected?(space)
+ !@actions.select(&:special_activity?).find { |a| a.space == space }.nil?
+ end
+
+ def cost
+ @actions.inject(0) { |s, a| s + a.cost }
+ end
+
+ def operation_cost
+ @actions.select(&:operation?).inject(0) { |s, a| s + a.cost }
+ end
+
+ def special_activity_cost
+ @actions.select(&:special_activity?).inject(0) { |s, a| s + a.cost }
+ end
+
+ def pass(cost)
+ operation_in(:pass, nil, -cost).pass
+ end
+
+ def agitate_in(space, terror, oppose)
+ raise "illegal Agitate in #{@operation}" if @operation != :rally
+ raise "not already selected : #{space.name}" unless operation_selected?(space)
+
+ add Action.new(:agitate, space, terror + oppose).agitate(terror, oppose)
+ end
+
+ def operation_in(operation, space, cost, to_agitate_in: nil)
+ raise "unknown operation : #{operation}" unless OPERATIONS.keys.include? operation
+
+ unless @operation.nil?
+ raise "illegal #{operation} in #{@operation}" if @operation != operation
+ raise "illegal #{operation} in limited operation #{@operation}" if @limited_op_only
+ end
+ raise "already selected : #{space.name}" if operation_selected?(space)
+
+ @operation = operation
+ add Action.new(operation, space, cost, to_agitate_in: to_agitate_in)
+ end
+
+ def special_activity_in(special_activity, space, cost, to_agitate_in: nil)
+ raise "unknown special activity : #{special_activity}" unless SPECIAL_ACTIVITIES.keys.include? special_activity
+ raise "illegal #{special_activity} in #{@special_activity}" if !@special_activity.nil? && @special_activity != special_activity
+ raise "illegal #{special_activity} in limited operation #{@operation}" if @limited_op_only
+ raise "already selected : #{space.name}" if special_activity_selected?(space)
+
+ @special_activity = special_activity
+ @operation = :attack if special_activity == :ambush
+ add Action.new(special_activity, space, cost, to_agitate_in: to_agitate_in)
+ end
+
+ def add(action)
+ @actions << action
+ action
+ end
+
+ def inspect
+ "Operation : #{@operation} - in #{selected_spaces} spaces => #{operation_cost} Resources\
+ \nSpecial Activity : #{@special_activity} - in #{special_activity_spaces}\
+ spaces => #{special_activity_cost} Resources\
+ \nactions : #{_actions_to_s}"
+ end
+
+ def _actions_to_s
+ @actions.inject('') { |s, a| s + "\n - #{a.inspect}" }
+ end
+ end
+end
diff --git a/spec/turn_spec.rb b/spec/turn_spec.rb
new file mode 100644
index 0000000..b00c86e
--- /dev/null
+++ b/spec/turn_spec.rb
@@ -0,0 +1,205 @@
+# frozen_string_literal: true
+
+require './lib/colonial_twilight/turn'
+require './spec/mock_board'
+
+describe ColonialTwilight::Turn do
+ before do
+ @turn = ColonialTwilight::Turn.new
+ @a = Sector.new
+ @b = Sector.new
+ end
+
+ describe 'Validation' do
+ it 'validate operation' do
+ expect { @turn.operation_in(:rall, @a, 0) }.to raise_error(Exception)
+ end
+
+ it 'validate special activity' do
+ expect { @turn.special_activity_in(:bush, @a, 0) }.to raise_error(Exception)
+ end
+
+ it '1 operation only' do
+ @turn.operation_in(:march, @a, 0)
+ expect { @turn.operation_in(:rally, @b, 0) }.to raise_error(Exception)
+ end
+
+ it '1 special activity only' do
+ @turn.special_activity_in(:extort, @a, 0)
+ expect { @turn.special_activity_in(:ambush, @b, 0) }.to raise_error(Exception)
+ end
+
+ it 'only 1 operation in limited operation' do
+ @turn.reset(true)
+ @turn.operation_in(:rally, @a, 0)
+ expect { @turn.operation_in(:rally, @a, 0) }.to raise_error(Exception)
+ end
+
+ it 'no special activity in limited operation' do
+ @turn.reset(true)
+ expect { @turn.special_activity_in(:ambush, @a, 0) }.to raise_error(Exception)
+ end
+
+ it 'select a sector once for the operation' do
+ @turn.operation_in(:rally, @a, 0)
+ expect { @turn.operation_in(:rally, @a, 0) }.to raise_error(Exception)
+ end
+
+ it 'select a sector once for the special activity' do
+ @turn.special_activity_in(:ambush, @a, 0)
+ expect { @turn.special_activity_in(:ambush, @a, 0) }.to raise_error(Exception)
+ end
+
+ it 'agitate in an unselected space' do
+ expect { @turn.agitate_n(@a, 0, 0, false) }.to raise_error(Exception)
+ end
+ end
+
+ describe 'interrogations' do
+ it 'operation done' do
+ expect(@turn.operation_done?).to be false
+ @turn.operation_in(:rally, @b, 2)
+ expect(@turn.operation_done?).to be true
+ end
+
+ it 'activity done' do
+ expect(@turn.special_activity_done?).to be false
+ @turn.special_activity_in(:ambush, @a, 3)
+ expect(@turn.special_activity_done?).to be true
+ end
+
+ it 'activity done' do
+ expect(@turn.may_special_activity?(:ambush)).to be true
+ @turn.special_activity_in(:extort, @b, 4).extort
+ expect(@turn.may_special_activity?(:extort)).to be true
+ expect(@turn.may_special_activity?(:ambush)).to be false
+ end
+
+ it 'operation cost' do
+ a = @turn.operation_in(:rally, @a, 1)
+ @turn.operation_in(:rally, @b, 2)
+ b = @turn.special_activity_in(:ambush, @a, 3)
+ @turn.special_activity_in(:ambush, @b, 4)
+ expect(@turn.operation_cost).to be 3
+ expect(a.name == 'Rally').to be true
+ expect(b.name == 'Ambush').to be true
+ end
+
+ it 'activity cost' do
+ @turn.operation_in(:rally, @a, 1)
+ @turn.operation_in(:rally, @b, 2)
+ @turn.special_activity_in(:ambush, @a, 3)
+ @turn.special_activity_in(:ambush, @b, 4)
+ expect(@turn.special_activity_cost).to be 7
+ end
+
+ it 'cost' do
+ @turn.operation_in(:rally, @a, 1)
+ @turn.operation_in(:rally, @b, 2)
+ @turn.special_activity_in(:ambush, @a, 3)
+ @turn.special_activity_in(:ambush, @b, 4)
+ expect(@turn.cost).to be 10
+ end
+
+ it 'selected spaces' do
+ @turn.operation_in(:rally, @a, 1)
+ @turn.operation_in(:rally, @b, 2)
+ @turn.special_activity_in(:ambush, @a, 3)
+ expect(@turn.selected_spaces).to be 2
+ end
+
+ it 'inspect' do
+ @turn.operation_in(:rally, @a, 1)
+ @turn.operation_in(:rally, @b, 2)
+ @turn.special_activity_in(:ambush, @a, 3)
+ expect(@turn.inspect.instance_of?(String)).to be true
+ end
+ end
+
+ describe 'Operations' do
+ it 'Pass' do
+ @turn.pass(1)
+ expect(@turn.cost).to eq(-1)
+ expect(@turn.actions.size).to eq 1
+ expect(@turn.actions[0].steps.size).to eq 1
+ expect(@turn.actions[0].steps[0][:kind]).to be :pass
+ end
+
+ it 'activate' do
+ @turn.special_activity_in(:ambush, @space, 1).activate(1)
+ expect(@turn.cost).to eq(1)
+ expect(@turn.actions[0].steps.size).to eq 1
+ expect(@turn.actions[0].steps[0][:kind]).to be :activate
+ end
+
+ it 'transfer' do
+ h = { @a => 2, @b => 3 }
+ @turn.operation_in(:rally, @space, 1)
+ .transfer_to(:available, :fln_active, 1)
+ .transfer_from(:available, :fln_bases)
+ .transfer_steps(h)
+ expect(@turn.cost).to eq(1)
+ expect(@turn.actions.size).to eq 1
+ expect(@turn.actions[0].steps.size).to eq 4
+ expect(@turn.actions[0].steps[0][:kind]).to be :transfer
+ expect(@turn.actions[0].steps[1][:kind]).to be :transfer
+ expect(@turn.actions[0].steps[2][:kind]).to be :transfer
+ expect(@turn.actions[0].steps[3][:kind]).to be :transfer
+ end
+
+ it 'shift' do
+ @turn.operation_in(:rally, @space, 1)
+ .shift(3)
+ expect(@turn.cost).to eq(1)
+ expect(@turn.actions.size).to eq 1
+ expect(@turn.actions[0].steps.size).to eq 1
+ expect(@turn.actions[0].steps[0][:kind]).to be :shift
+ end
+
+ it 'extort' do
+ @turn.operation_in(:rally, @space, -1)
+ .extort
+ expect(@turn.cost).to eq(-1)
+ expect(@turn.actions.size).to eq 1
+ expect(@turn.actions[0].steps.size).to eq 1
+ expect(@turn.actions[0].steps[0][:kind]).to be :extort
+ end
+
+ it 'terror' do
+ @turn.operation_in(:terror, @space, 1)
+ .terror
+ expect(@turn.cost).to eq(1)
+ expect(@turn.actions.size).to eq 1
+ expect(@turn.actions[0].steps.size).to eq 2
+ expect(@turn.actions[0].steps[0][:kind]).to be :set
+ expect(@turn.actions[0].steps[0][:kind]).to be :set
+ end
+
+ it 'agitate raise if not :rally' do
+ @turn.pass(1)
+ expect { @turn.agitate_in(@a, 1, 0) }.to raise_error(Exception)
+ end
+
+ it 'agitate' do
+ @turn.operation_in(:rally, @space, 1)
+ @turn.agitate_in(@space, 2, 1)
+ expect(@turn.cost).to eq 4
+ expect(@turn.actions.size).to eq 2
+ expect(@turn.actions[1].steps.size).to eq 1
+ expect(@turn.actions[1].steps[0][:kind]).to be :agitate
+ expect(@turn.actions[1].steps[0][:terror]).to eq 2
+ expect(@turn.actions[1].steps[0][:shift]).to eq 1
+ end
+
+ it 'sanitize!' do
+ act = @turn.special_activity_in(:subvert, @a, 0)
+ .transfer_to(:available, :algerian_police, 1)
+ .transfer_from(:available, :fln_underground, 1)
+ act.steps[0].merge!(:src_control=>:uncontrolled)
+ act.steps[1].merge!(:dst_control=>:FLN)
+ act.sanitize!
+ expect(act.steps[0].key?(:src_control)).to be false
+ expect(act.steps[1][:dst_control]).to be :FLN
+ end
+ end
+end