summaryrefslogtreecommitdiffstats
path: root/lib/colonial_twilight/board
diff options
context:
space:
mode:
Diffstat (limited to 'lib/colonial_twilight/board')
-rw-r--r--lib/colonial_twilight/board/forces.rb153
-rw-r--r--lib/colonial_twilight/board/scenario.rb67
-rw-r--r--lib/colonial_twilight/board/setup.rb87
-rw-r--r--lib/colonial_twilight/board/spaces.rb229
4 files changed, 536 insertions, 0 deletions
diff --git a/lib/colonial_twilight/board/forces.rb b/lib/colonial_twilight/board/forces.rb
new file mode 100644
index 0000000..ecf27df
--- /dev/null
+++ b/lib/colonial_twilight/board/forces.rb
@@ -0,0 +1,153 @@
+# frozen_string_literal: true
+
+# rubocop:disable Style/AccessorGrouping
+# rubocop:disable Style/ParallelAssignment
+
+module ColonialTwilight
+ class Forces
+ attr_reader :name
+ attr_reader :algerian_troops, :algerian_police
+ attr_reader :french_troops, :french_police
+ attr_reader :fln_underground, :fln_active
+ attr_reader :fln_bases, :gov_bases
+ attr_reader :max_bases, :control
+
+ def initialize(sym)
+ @name = sym
+ @algerian_troops, @algerian_police = 0, 0
+ @french_troops, @french_police, @gov_bases = 0, 0, 0
+ @fln_underground, @fln_active, @fln_bases = 0, 0, 0
+ @max_bases = nil
+ @control = :uncontrolled
+ @max_bases = 2 if %i[Country Sector].include?(sym)
+ _variables_to_remove(sym)&.each do |s|
+ instance_variable_set(s, nil)
+ end
+ end
+
+ def init(data)
+ data.each { |k, v| add(k, v) }
+ end
+
+ private
+
+ def _variables_to_remove(sym)
+ case sym
+ when :available then %i[@control @fln_active]
+ when :casualties then %i[@control @fln_active @fln_bases]
+ when :out_of_play then %i[@control @algerian_troops @algerian_police @fln_active @fln_bases]
+ when :Country then %i[@control @algerian_troops @algerian_police @french_troops @french_police]
+ when :City then nil
+ when :Sector then nil
+ end
+ end
+
+ public
+
+ def inspect
+ "GOV bases: #{gov_bases}
+ french troops: #{french_troops}
+ french police: #{french_police}
+ algerian troops: #{algerian_troops}
+ algerian police: #{algerian_police}
+ FLN bases: #{fln_bases}
+ underground Guerrillas: #{fln_underground}
+ active Guerrillas: #{fln_active}"
+ end
+ alias to_s inspect
+
+ def data
+ h = {}
+ %i[algerian_troops algerian_police french_troops french_police gov_bases
+ fln_underground fln_active fln_bases control].each do |sym|
+ h[sym] = send(sym) unless send(sym).nil?
+ end
+ h
+ end
+
+ def bases
+ (@gov_bases || 0) + (@fln_bases || 0)
+ end
+
+ def fln
+ guerrillas + (@fln_bases || 0)
+ end
+
+ def guerrillas
+ (@fln_underground || 0) + (@fln_active || 0)
+ end
+
+ def gov
+ gov_cubes + (@gov_bases || 0)
+ end
+
+ def gov_cubes
+ french_cubes + algerian_cubes
+ end
+
+ def french_cubes
+ (@french_troops || 0) + (@french_police || 0)
+ end
+
+ def algerian_cubes
+ (@algerian_troops || 0) + (@algerian_police || 0)
+ end
+
+ def troops
+ (@french_troops || 0) + (@algerian_troops || 0)
+ end
+
+ def police
+ (@french_police || 0) + (@algerian_police || 0)
+ end
+
+ def add(type, num = 1)
+ type = :fln_underground if name == :available && type == :fln_active
+ case type
+ when :french_troops then @french_troops += num
+ when :french_police then @french_police += num
+ when :algerian_troops then @algerian_troops += num
+ when :algerian_police then @algerian_police += num
+ when :fln_underground then @fln_underground += num
+ when :fln_active then @fln_active += num
+ when :gov_bases then add_base(type, num)
+ when :fln_bases then add_base(type, num)
+ else
+ raise "unknown force type : #{type}"
+ end
+ update_control
+ end
+
+ def activate(num)
+ raise "cannot activate #{num}" if @fln_underground < num
+
+ @fln_underground -= num
+ @fln_active += num
+ end
+
+ private
+
+ def add_base(type, num = 1)
+ if !@max_bases.nil? && (bases + num) > @max_bases
+ raise "too much bases in #{@name} (#{bases} + #{num}) > #{@max_bases}"
+ end
+
+ @gov_bases += num if type == :gov_bases
+ @fln_bases += num if type == :fln_bases
+ end
+
+ def update_control
+ return nil if @control.nil?
+
+ ctr = @control
+ @control = (
+ case gov <=> fln
+ when 0 then :uncontrolled
+ when 1 then :GOV
+ when -1 then :FLN
+ end
+ )
+ @control != ctr
+ end
+ end
+end
diff --git a/lib/colonial_twilight/board/scenario.rb b/lib/colonial_twilight/board/scenario.rb
new file mode 100644
index 0000000..a0f5edf
--- /dev/null
+++ b/lib/colonial_twilight/board/scenario.rb
@@ -0,0 +1,67 @@
+# frozen_string_literal: true
+
+module ColonialTwilight
+ module Scenario
+ def resettle(name)
+ by_name(name).resettle!
+ end
+
+ def set_space(idx, opts, align = nil)
+ s = @spaces[idx]
+ s.alignment = align unless align.nil?
+ %i[gov_bases fln_bases french_troops french_police algerian_troops algerian_police
+ fln_underground].each { |sym| s.add(sym, opts[sym]) if opts.key?(sym) }
+ end
+
+ def short_scenario
+ @opposition_bases.v = 19
+ @support_commitment.v = 22
+ @commitment.v = 15
+ @fln_resources.v = 15
+ @gov_resources.v = 20
+ @france_track.v = 4
+ @border_zone_track.v = 3
+ @out_of_play.init(fln_underground: 5)
+ @available.init(gov_bases: 2, french_police: 4, fln_bases: 7, fln_underground: 8)
+ resettle 'Setif'
+ resettle 'Tlemcen'
+ resettle 'Bordj Bou Arreridj'
+
+ set_space 0, { algerian_police: 1, fln_underground: 1 }, :oppose
+ set_space 2, { french_police: 1 }
+ set_space 4, { algerian_police: 1, fln_underground: 1 }, :oppose
+ set_space 5, { french_police: 1 }
+ set_space 6, { french_police: 1 }, :support
+ set_space 7, { fln_underground: 1 }
+ set_space 8, { french_troops: 4, algerian_police: 1, gov_bases: 1 }
+ set_space 9, { french_troops: 1, algerian_police: 1, gov_bases: 1, fln_underground: 1, fln_bases: 1 }, :oppose
+ set_space 10, { french_police: 1, fln_underground: 1, fln_bases: 1 }, :oppose
+ set_space 11, { french_police: 1 }
+ set_space 12, { french_police: 1, fln_underground: 1, fln_bases: 1 }, :oppose
+ set_space 13, { french_troops: 4, algerian_troops: 1, french_police: 1 }, :support
+ set_space 14, { algerian_troops: 1, gov_bases: 1 }
+ set_space 15, { french_police: 1, algerian_police: 1, fln_underground: 1, fln_bases: 1 }, :oppose
+ set_space 16, { algerian_troops: 1, french_police: 1, algerian_police: 1 }, :support
+ set_space 17, { french_police: 1, algerian_police: 1 }
+ set_space 18, { french_police: 2, fln_underground: 1 }
+ set_space 19, { french_police: 1, gov_bases: 1 }
+ set_space 20, { french_police: 1 }
+ set_space 22, { french_police: 1 }
+ set_space 23, { french_police: 1 }
+ set_space 24, { french_police: 1 }
+ set_space 27, {}, :oppose
+ set_space 28, { fln_underground: 4, fln_bases: 2 }
+ set_space 29, { fln_underground: 5, fln_bases: 2 }
+ spaces[28].independent!
+ spaces[29].independent!
+ end
+
+ def medium_scenario
+ raise 'MEDIUM scenario net implemented yet'
+ end
+
+ def full_scenario
+ raise 'FULL scenario net implemented yet'
+ end
+ end
+end
diff --git a/lib/colonial_twilight/board/setup.rb b/lib/colonial_twilight/board/setup.rb
new file mode 100644
index 0000000..110a674
--- /dev/null
+++ b/lib/colonial_twilight/board/setup.rb
@@ -0,0 +1,87 @@
+# frozen_string_literal: true
+
+module ColonialTwilight
+ module Setup
+ def setup
+ set_spaces
+ set_adjacents
+ end
+
+ def add(kls, *args)
+ @spaces << kls.new(*args)
+ end
+
+ def set_spaces
+ mountain = Sector::MOUNTAIN
+ border = Sector::BORDER
+ coastal = Sector::COASTAL
+ add Sector, 'Barika', 'I', 1, 1, mountain
+ add Sector, 'Batna', 'I', 2, 0, mountain
+ add Sector, 'Biskra', 'I', 3, 0, border
+ add Sector, 'Oum El Bouaghi', 'I', 4, 0, mountain
+ add Sector, 'Tebessa', 'I', 5, 1, mountain | border
+ add Sector, 'Negrine', 'I', 6, 0, mountain | border
+ add City, 'Constantine', 'II', 2
+ add Sector, 'Setif', 'II', 1, 1, mountain | coastal
+ add Sector, 'Philippeville', 'II', 2, 2, mountain | coastal
+ add Sector, 'Souk Ahras', 'II', 3, 2, coastal | border
+ add Sector, 'Tizi Ouzou', 'III', 1, 2, mountain | coastal
+ add Sector, 'Bordj Bou Arreridj', 'III', 2, 1, mountain
+ add Sector, 'Bougie', 'III', 3, 2, mountain | coastal
+ add City, 'Algiers', 'IV', 3, coastal
+ add Sector, 'Medea', 'IV', 1, 2, mountain | coastal
+ add Sector, 'Orleansville', 'IV', 2, 2, mountain | coastal
+ add City, 'Oran', 'V', 2, coastal
+ add Sector, 'Mecheria', 'V', 1, 0, mountain | border
+ add Sector, 'Tlemcen', 'V', 2, 1, border | coastal
+ add Sector, 'Sidi Bel Abbes', 'V', 3, 1, coastal
+ add Sector, 'Mostaganem', 'V', 4, 2, mountain | coastal
+ add Sector, 'Saida', 'V', 5, 0, mountain
+ add Sector, 'Mascara', 'V', 6, 0, mountain
+ add Sector, 'Tiaret', 'V', 7, 0, mountain
+ add Sector, 'Ain Sefra', 'V', 8, 0, border
+ add Sector, 'Laghouat', 'V', 9, 0
+ add Sector, 'Sidi Aissa', 'VI', 1, 0, mountain
+ add Sector, 'Ain Oussera', 'VI', 2, 1, mountain
+ add Country, 'Morocco'
+ add Country, 'Tunisia'
+ end
+
+ def adjacents(name, *args)
+ by_name(name).adjacents = args
+ end
+
+ def set_adjacents
+ adjacents 'Barika', 1, 2, 3, 7, 8, 11, 26
+ adjacents 'Batna', 0, 2, 3, 5
+ adjacents 'Biskra', 0, 1, 5, 25, 26, 29
+ adjacents 'Oum El Bouaghi', 0, 1, 4, 5, 8, 9
+ adjacents 'Tebessa', 3, 5, 9, 29
+ adjacents 'Negrine', 1, 2, 3, 4, 29
+ adjacents 'Constantine', 7, 8
+ adjacents 'Setif', 0, 6, 8, 11, 12
+ adjacents 'Philippeville', 0, 3, 7, 6, 9
+ adjacents 'Souk Ahras', 3, 4, 8, 29
+ adjacents 'Tizi Ouzou', 11, 12, 14
+ adjacents 'Bordj Bou Arreridj', 0, 7, 10, 12, 14, 26
+ adjacents 'Bougie', 7, 10, 11
+ adjacents 'Algiers', 14
+ adjacents 'Medea', 10, 11, 13, 15, 26, 27
+ adjacents 'Orleansville', 14, 20, 23, 27
+ adjacents 'Oran', 19
+ adjacents 'Mecheria', 18, 21, 24, 28
+ adjacents 'Tlemcen', 17, 19, 21, 28
+ adjacents 'Sidi Bel Abbes', 16, 18, 20, 21, 22
+ adjacents 'Mostaganem', 15, 19, 22, 23
+ adjacents 'Saida', 17, 18, 19, 22, 24
+ adjacents 'Mascara', 19, 20, 21, 23, 24
+ adjacents 'Tiaret', 15, 20, 22, 24, 27
+ adjacents 'Ain Sefra', 17, 21, 22, 23, 25, 27, 28
+ adjacents 'Laghouat', 2, 24, 26, 27
+ adjacents 'Sidi Aissa', 0, 2, 11, 14, 25, 27
+ adjacents 'Ain Oussera', 14, 15, 23, 24, 25, 26
+ adjacents 'Morocco', 17, 18, 24
+ adjacents 'Tunisia', 2, 4, 5, 9
+ end
+ end
+end
diff --git a/lib/colonial_twilight/board/spaces.rb b/lib/colonial_twilight/board/spaces.rb
new file mode 100644
index 0000000..fad3170
--- /dev/null
+++ b/lib/colonial_twilight/board/spaces.rb
@@ -0,0 +1,229 @@
+# frozen_string_literal: true
+
+require_relative 'forces'
+
+module ColonialTwilight
+ class Track
+ attr_accessor :v
+
+ def initialize(max)
+ @v = 0
+ @max = max
+ end
+
+ def shift(val)
+ @v += val
+ raise "out of track #{@v}" if @v.negative? || @v > @max
+
+ @v
+ end
+
+ def clamp(val)
+ @v = (@v + val).clamp(0, @max)
+ end
+
+ def data
+ @v
+ end
+ end
+
+ class Box < Forces
+ end
+
+ class Sector
+ MOUNTAIN = 1
+ COASTAL = 2
+ BORDER = 4
+
+ attr_reader :wilaya, :sector, :name, :resettled
+ attr_accessor :pop, :terror, :adjacents, :alignment
+
+ def initialize(name, wilaya, sector, pop, attrs = 0)
+ @name = name
+ @wilaya = wilaya
+ @sector = sector
+ @pop = pop
+ @attrs = attrs
+ @alignment = :neutral
+ @resettled = false
+ @terror = 0
+ @forces = Forces.new self.class.name.split('::')[-1].to_sym
+ _compute_strings
+ end
+
+ private
+
+ def _compute_strings
+ @terrain = %i[mountain coastal border].map { |s| send("#{s}?") ? s : nil }.reject(&:nil?).join('/')
+ @descr = "#{@name} #{self.class.name.split('::')[-1]}#{number}"
+ end
+
+ def number
+ return '' if @wilaya.nil? && @sector.nil?
+
+ @descr = "(#{@wilaya}-#{@sector})"
+ end
+
+ public
+
+ def to_s
+ @name
+ end
+
+ def inspect
+ "\n#{@descr} : #{@terrain}
+ population : #{@pop}#{@resettled ? ' resettled' : ''}
+ control : #{control}
+ alignment : #{@alignment}
+ terror : #{@terror}
+ #{@forces}
+ adjs : #{@adjacents}"
+ end
+
+ def data
+ { name: @name, alignment: @alignment, terror: @terror, pop: @pop, resettled: @resettled }.merge(@forces.data)
+ end
+
+ %i[gov gov_bases gov_cubes french_cubes algerian_cubes troops police
+ french_troops french_police algerian_troops algerian_police
+ fln fln_bases guerrillas fln_underground fln_active max_bases control].each do |sym|
+ define_method(sym) { @forces.send(sym) }
+ end
+
+ def sector?
+ true
+ end
+
+ def city?
+ false
+ end
+
+ def country?
+ false
+ end
+
+ def border?
+ (@attrs & BORDER) == BORDER
+ end
+
+ def coastal?
+ (@attrs & COASTAL) == COASTAL
+ end
+
+ def mountain?
+ (@attrs & MOUNTAIN) == MOUNTAIN
+ end
+
+ def terror?
+ @terror.positive?
+ end
+
+ def support?
+ @alignment == :support
+ end
+
+ def oppose?
+ @alignment == :oppose
+ end
+
+ def neutral?
+ @alignment == :neutral
+ end
+
+ def uncontrolled?
+ control == :uncontrolled
+ end
+
+ def fln_control?
+ control == :FLN
+ end
+
+ def gov_control?
+ control == :GOV
+ end
+
+ def add(type, num = 1)
+ @forces.add(type, num)
+ end
+
+ def shift_terror(num = 1)
+ raise 'terror cant be negative' if @terror.zero? && num.negative?
+
+ @terror += num
+ end
+
+ def resettled?
+ @resettled
+ end
+
+ def resettle!
+ raise 'cannot resettle a country' if country?
+ raise 'cannot resettle a sector with a population =! 1' if @pop != 1
+
+ @pop = 0
+ @resettled = true
+ end
+
+ def shift(towards)
+ if towards == :oppose
+ raise 'cannot shift towards oppose' if oppose?
+
+ @alignment = (support? ? :neutral : :oppose)
+ elsif towards == :support
+ raise 'cannot shift towards support' if support?
+
+ @alignment = (oppose? ? :neutral : :support)
+ else
+ raise "unknown shift direction : #{towards}"
+ end
+ end
+
+ def activate(num)
+ @forces.activate(num)
+ end
+ end
+
+ class City < Sector
+ def initialize(name, wilaya, pop, attrs = 0)
+ super name, wilaya, 0, pop, attrs
+ end
+
+ def sector?
+ false
+ end
+
+ def city?
+ true
+ end
+ end
+
+ # if independent, FLN may Rally, March and Extort in these Countries,
+ # but their Population is never counted in the total Opposition
+ class Country < Sector
+ attr_reader :independent
+
+ def initialize(name)
+ super(name, nil, nil, 1, MOUNTAIN | BORDER | COASTAL)
+ @independent = false
+ @descr += ' : French'
+ end
+
+ def sector?
+ false
+ end
+
+ def country?
+ true
+ end
+
+ def independent?
+ @independent
+ end
+
+ def independent!
+ @independent = true
+ @descr.gsub!(/French/, 'Independent')
+ @independent
+ end
+ end
+end