summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJérémy Zurcher <jeremy@asynk.ch>2026-03-15 10:57:18 +0100
committerJérémy Zurcher <jeremy@asynk.ch>2026-03-15 10:57:18 +0100
commite4e09f936d38a89082f40354fdf451ad875baffa (patch)
tree0b80d727e6e75fb46cc0b7f193dca24465baddb5
parent7d7db184eacd1407c87d01355ae587acccccf7ac (diff)
downloadcolonial-twilight-e4e09f936d38a89082f40354fdf451ad875baffa.zip
colonial-twilight-e4e09f936d38a89082f40354fdf451ad875baffa.tar.gz
Rally & Agitate action classes
-rw-r--r--lib/colonial_twilight/actions/action.rb79
-rw-r--r--lib/colonial_twilight/actions/fln/agitate.rb51
-rw-r--r--lib/colonial_twilight/actions/fln/fln_action.rb13
-rw-r--r--lib/colonial_twilight/actions/fln/rally.rb66
-rw-r--r--spec/mock_board.rb20
5 files changed, 229 insertions, 0 deletions
diff --git a/lib/colonial_twilight/actions/action.rb b/lib/colonial_twilight/actions/action.rb
new file mode 100644
index 0000000..3be8483
--- /dev/null
+++ b/lib/colonial_twilight/actions/action.rb
@@ -0,0 +1,79 @@
+# frozen_string_literal: true
+
+module ColonialTwilight
+ module Actions
+ class GameAction
+ def initialize(faction:, space:, cost: 0, mode: nil)
+ @data = {
+ faction: faction,
+ space: space,
+ cost: cost,
+ mode: mode
+ }
+ validate!
+ end
+
+ def faction
+ @data[:faction]
+ end
+
+ def space
+ @data[:space]
+ end
+
+ def cost
+ @data[:cost]
+ end
+
+ def mode
+ @data[:mode]
+ end
+
+ def to_h
+ @data.merge(type: self.class.name.split('::')[-1])
+ end
+
+ def validate!
+ raise "not applicable to #{@data[:space]}" unless self.class.applicable?(@data[:space])
+
+ available = self.class.available_modes(@data[:space])
+ @data[:mode].each do |key, value|
+ raise "mode: #{key} is not available" unless available.key?(key)
+
+ max_allowed = available[key]
+ raise "value: #{key} set to #{value}, max is #{max_allowed}" if value > max_allowed
+ end
+ end
+
+ def apply!
+ raise NotImplementedError
+ end
+
+ def revert!
+ raise NotImplementedError
+ end
+
+ class << self
+ def op?
+ false
+ end
+
+ def special?
+ false
+ end
+
+ def applicable?(space)
+ raise NotImplementedError
+ end
+
+ def available_modes(_space)
+ nil
+ end
+
+ def possible_spaces(board)
+ board.search(&method(:applicable?))
+ end
+ end
+ end
+ end
+end
diff --git a/lib/colonial_twilight/actions/fln/agitate.rb b/lib/colonial_twilight/actions/fln/agitate.rb
new file mode 100644
index 0000000..fd0e287
--- /dev/null
+++ b/lib/colonial_twilight/actions/fln/agitate.rb
@@ -0,0 +1,51 @@
+# frozen_string_literal: true
+
+require_relative 'fln_action'
+require_relative 'rally'
+
+module ColonialTwilight
+ module Actions
+ module FLN
+ # Agitate 3.3.1
+ class Agitate < FlnAction
+ # 1 resources per Terror marker, then 1 resource for the level shift
+ def initialize(space, mode)
+ super(space, mode, cost: (mode[:remove_terror] || 0) + (mode[:shift_oppose] || 0))
+ end
+
+ def validate!
+ super
+ raise 'select at least 1 mode' unless mode.keys.size.positive?
+
+ return if space.terror.zero? || (mode.key?(:remove_terror) && mode[:remove_terror] == space.terror)
+
+ raise 'remove Terror marker first' if mode.key?(:shift_oppose)
+ end
+
+ # remove Terror first, then shift 1 level toward Oppose
+ def apply!(board)
+ raise NotImplementedError
+ end
+
+ class << self
+ def op?
+ true
+ end
+
+ # with Base and or Control && terror or shift to oppose possible
+ def applicable?(space)
+ Rally.applicable?(space) &&
+ (space.fln_bases.positive? || space.fln_control?) && (space.terror.positive? || !space.oppose?)
+ end
+
+ def available_modes(space)
+ modes = {}
+ modes[:remove_terror] = space.terror if space.terror.positive?
+ modes[:shift_oppose] = 1 unless space.oppose?
+ modes
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/colonial_twilight/actions/fln/fln_action.rb b/lib/colonial_twilight/actions/fln/fln_action.rb
new file mode 100644
index 0000000..3a571aa
--- /dev/null
+++ b/lib/colonial_twilight/actions/fln/fln_action.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+require_relative '../action'
+
+module ColonialTwilight
+ module Actions
+ class FlnAction < GameAction
+ def initialize(space, mode, cost: 1)
+ super(faction: :FLN, space: space, mode: mode, cost: cost)
+ end
+ end
+ end
+end
diff --git a/lib/colonial_twilight/actions/fln/rally.rb b/lib/colonial_twilight/actions/fln/rally.rb
new file mode 100644
index 0000000..c9ed489
--- /dev/null
+++ b/lib/colonial_twilight/actions/fln/rally.rb
@@ -0,0 +1,66 @@
+# frozen_string_literal: true
+
+require_relative 'fln_action'
+require_relative 'agitate'
+
+module ColonialTwilight
+ module Actions
+ module FLN
+ # Rally 3.3.1
+ class Rally < FlnAction
+ def initialize(space, mode)
+ super(space, mode)
+ @agitate = nil
+ end
+
+ def cost
+ super + (@agitate.nil? ? 0 : @agitate.cost)
+ end
+
+ def validate!
+ super
+ raise 'select 1 mode' if mode.keys.size != 1
+ raise 'flip all Guerrillas' if mode.key?(:underground) && mode[:underground] != space.fln_active
+ end
+
+ # France track: shift 1 level toward "F"
+ # in space place 1 underground Guerrilla, or replace 2 Guerrillas with 1 Base
+ # or in space with FLN Base: add underground Guerrillas equal to population + Bases,
+ # or flip all Guerrillas underground
+ def apply!(board)
+ raise NotImplementedError
+ end
+
+ def agitate!(mode)
+ raise 'agitate! called twice' unless @agitate.nil?
+
+ @agitate = Agitate.new(@data[:space], mode)
+ self
+ end
+
+ class << self
+ def op?
+ true
+ end
+
+ # Sectors, Cities not at Support, Independent Countries, France track
+ def applicable?(space)
+ return space.name == 'France track' && !space.max? if space.track?
+
+ space.sector? || (space.city? && !space.support?) || (space.country? && space.independent?)
+ end
+
+ def available_modes(space)
+ modes = { place_guerilla: 1 }
+ modes[:place_base] = 1 if space.guerrillas > 1 && space.bases < space.max_bases
+ unless space.fln_bases.zero?
+ modes[:place_guerilla] = space.fln_bases + space.pop
+ modes[:underground] = space.fln_active if space.fln_active.positive?
+ end
+ modes
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/spec/mock_board.rb b/spec/mock_board.rb
index 133c09e..7afbd6f 100644
--- a/spec/mock_board.rb
+++ b/spec/mock_board.rb
@@ -1,5 +1,21 @@
# frozen_string_literal: true
+class Track
+ attr_reader :value, :name
+ def initialize(value, name)
+ @value = value
+ @name = name
+ end
+
+ def track?
+ true
+ end
+
+ def max?
+ false
+ end
+end
+
class Sector
attr_reader :name
attr_writer :data
@@ -9,6 +25,10 @@ class Sector
@data = data
end
+ def track?
+ false
+ end
+
def sector?
@name == 'sector'
end