From 5b489f8e17a8b5178e9c4b55eee22f0f72d33073 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9my=20Zurcher?= Date: Tue, 5 Dec 2023 11:28:48 +0100 Subject: add Turn and specs --- lib/colonial_twilight/turn.rb | 239 ++++++++++++++++++++++++++++++++++++++++++ spec/turn_spec.rb | 205 ++++++++++++++++++++++++++++++++++++ 2 files changed, 444 insertions(+) create mode 100644 lib/colonial_twilight/turn.rb create mode 100644 spec/turn_spec.rb 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 -- cgit v1.1-2-g2b99