summaryrefslogtreecommitdiffstats
path: root/lib/colonial_twilight/board.rb
diff options
context:
space:
mode:
authorJérémy Zurcher <jeremy@asynk.ch>2020-08-05 19:14:26 +0200
committerJérémy Zurcher <jeremy@asynk.ch>2020-08-05 19:14:26 +0200
commit94d2e329a4b9c325b32de5bd641e5e1e99a1c8f4 (patch)
tree1d9f733e12eb8dc27610da0d52674ae1aec9fefc /lib/colonial_twilight/board.rb
downloadcolonial-twilight-94d2e329a4b9c325b32de5bd641e5e1e99a1c8f4.zip
colonial-twilight-94d2e329a4b9c325b32de5bd641e5e1e99a1c8f4.tar.gz
Inital commit
Diffstat (limited to 'lib/colonial_twilight/board.rb')
-rw-r--r--lib/colonial_twilight/board.rb492
1 files changed, 492 insertions, 0 deletions
diff --git a/lib/colonial_twilight/board.rb b/lib/colonial_twilight/board.rb
new file mode 100644
index 0000000..3a1679a
--- /dev/null
+++ b/lib/colonial_twilight/board.rb
@@ -0,0 +1,492 @@
+#! /usr/bin/env ruby
+# -*- coding: UTF-8 -*-
+
+require 'json'
+
+# FIXME :
+# - json should not be here
+# - has_?
+# - can_?
+#
+# check min/max bases, points, tracks
+#
+# delegate [syms].each do |s| define_method s delegate.instance_method(s) end
+#
+# scenario as JSON ?, tools to generate them
+#
+
+module ColonialTwilight
+
+ class Forces
+
+ attr_accessor :algerian_troops, :algerian_police
+ attr_accessor :french_troops, :french_police
+ attr_accessor :fln_underground, :fln_active
+ attr_accessor :fln_bases, :gov_bases
+ attr_reader :control
+
+ def initialize k
+ @algerian_troops = 0
+ @algerian_police = 0
+ @french_troops = 0
+ @french_police = 0
+ @gov_bases = 0
+ @fln_underground = 0
+ @fln_active = 0
+ @fln_bases = 0
+ @max_bases = 2
+ @control = :none
+ rm = nil
+ case k
+ when :available
+ rm = [:@control, :@fln_active]
+ when :casualties
+ rm = [:@control, :@fln_active, :@fln_bases]
+ when :out_of_play
+ rm = [:@control, :@algerian_troops, :@algerian_police, :@fln_active, :@fln_bases]
+ when :Country
+ @max_bases = 3
+ rm = [:@control, :@algerian_troops, :@algerian_police, :@french_troops, :@french_police, :@gov_bases]
+ end
+ rm.each do |sym|
+ # maybe remove :sym= instead or set @sym to nil
+ remove_instance_variable sym
+ end unless rm.nil?
+ end
+
+ def to_s
+ "
+ #{gov_bases} GOV bases
+ #{french_troops} french troops
+ #{french_police} french police
+ #{algerian_troops} algerian troops
+ #{algerian_police} algerian police
+ #{fln_bases} FLN bases
+ #{fln_underground} underground Guerrillas
+ #{fln_active} active Guerrillas"
+ end
+
+ def data
+ h={}
+ [:algerian_troops, :algerian_police, :french_troops, :french_police,
+ :fln_underground, :fln_active, :fln_bases, :gov_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 add t, n=1
+ case t
+ when :french_troops; @french_troops += n
+ when :french_police; @french_police += n
+ when :algerian_troops; @algerian_troops += n
+ when :algerian_police; @algerian_police += n
+ when :fln_underground; @fln_underground += n
+ when :fln_active; @fln_active += n
+ else
+ raise "unknown force type : #{t}"
+ end
+ update_control
+ end
+
+ def add_base t, n=1
+ raise "too much bases in #@name (#{bases} + #{n}) > #@max_bases" if (bases + n) > @max_bases
+ @gov_bases += n if t == :gov
+ @fln_bases += n if t == :fln
+ update_control
+ end
+
+ private
+
+ def update_control
+ return if @control.nil?
+ gov = @algerian_troops + @algerian_police + @french_troops + @french_police + @gov_bases
+ fln = @fln_underground + @fln_active + @fln_bases
+ if gov == fln; @control = :none
+ elsif gov > fln; @control = :GOV
+ else @control = :FLN
+ end
+ end
+
+ end
+
+ class Sector
+
+ MOUNTAIN=1
+ COASTAL=2
+ BORDER=4
+
+ attr_reader :wilaya, :sector, :name, :resettled
+ attr_accessor :pop, :adjacents
+ attr_accessor :alignment
+
+ def initialize n, w, s, p, attrs=0
+ @name = n
+ @wilaya = w
+ @sector = s
+ @pop = p
+ @attributes = attrs
+ @descr = "#{self.class.name} #@name #{@wilaya == 0 ? '' : @wilaya}" + (@sector == 0 ? '' : "-#{@sector}")
+ @terrain = [mountain? ? 'mountain' : nil ,coastal? ? 'coastal' : nil, border? ? 'border' : nil].reject(&:nil?).join '/'
+ @forces = Forces.new self.class.name.split('::')[-1].to_sym
+ @alignment = :neutral
+ @resettled = false
+ end
+
+ def to_s
+ "#@descr #@terrain
+ control : #{@forces.control}
+ alignment : #@alignment
+ population : #{@pop}#{@resettled ? ' resettled' : ''}
+ forces : #{@forces}
+ adjs : #{@adjacents}"
+ end
+
+ def data
+ { :name=>@name, :alignment=>@alignment, :pop=>@pop, :resettled=>@resettled }.merge(@forces.data)
+ end
+
+ def city?; false; end
+ def country?; false; end
+ def border?; (@attributes & BORDER) == BORDER end
+ def coastal?; (@attributes & COASTAL) == COASTAL end
+ def mountain?; (@attributes & MOUNTAIN) == MOUNTAIN end
+ def control; @forces.control; end
+ def add_gov_base n=1; @forces.add_base :gov, n; end
+ def add_fln_base n=1; @forces.add_base :fln, n; end
+ def add_french_troops n=1; @forces.add :french_troops, n; end
+ def add_french_police n=1; @forces.add :french_police, n; end
+ def add_algerian_troops n=1; @forces.add :algerian_troops, n; end
+ def add_algerian_police n=1; @forces.add :algerian_police, n; end
+ def add_fln_underground n=1; @forces.add :fln_underground, n; end
+ def add_fln_active n=1; @forces.add :fln_active, n; end
+ def french_troops; @forces.french_troops end
+ def french_police; @forces.french_police end
+ def algerian_troops; @forces.algerian_troops end
+ def algerian_police; @forces.algerian_police end
+ def fln_underground; @forces.fln_underground end
+ def fln_active; @forces.fln_active end
+ def gov_bases; @forces.gov_bases end
+ def fln_bases; @forces.fln_bases end
+ def resettle!
+ raise "can't resettle a country " if country?
+ raise "can't resettle a sector with a population > 1" if @pop != 1
+ @pop = 0
+ @resettled = true
+ end
+
+ end
+
+ class City < Sector
+ def initialize n, w, p, attrs=0
+ super n, w, 0, p, attrs
+ end
+ def city?; true; end
+ end
+
+ class Country < Sector
+
+ attr_reader :independant
+
+ def initialize n
+ super n, 0, 0, 1, MOUNTAIN|BORDER|COASTAL
+ @descr += " #{@independant ? 'Independant' : 'French'}"
+ end
+ def add_gov_base n=1; raise "no gov bases allowed in #@name" end
+ def country?; true; end
+ end
+
+
+ class Board
+
+ FRANCE_TRACK=['A','B','C','D','E'].freeze
+
+ attr_accessor :commitment
+ attr_accessor :gov_resources, :fln_resources
+ attr_accessor :support_commitment, :opposition_bases
+ attr_accessor :resettled_sectors
+ attr_accessor :france_track, :border_zone_track
+
+ attr_reader :spaces, :names
+
+ def initialize
+ @names = []
+ @spaces = {}
+ @capabilities = []
+ @available = Forces.new :available
+ @casualties = Forces.new :casualties
+ @out_of_play = Forces.new :out_of_play
+ feed
+ end
+
+ def load scenario
+ case scenario
+ when :short; short
+ when :medium; medium
+ when :full; full
+ else raise "unknown scenario : #{scenario}"
+ end
+ end
+
+ def sectors
+ @spaces.select{ |k,s| not s.country? }
+ end
+
+ def data
+ h = { }
+ [:commitment, :gov_resources, :fln_resources, :support_commitment, :opposition_bases, :resettled_sectors, :france_track, :border_zone_track].each do |sym|
+ h[sym] = send(sym)
+ end
+ h[:capabilities] = @capabilities
+ h[:available] = @available.data
+ h[:casualties] = @casualties.data
+ h[:out_of_play] = @out_of_play.data
+ h[:spaces] = @spaces.inject([])do |a,(k,s)| a << s.data end
+ h
+ end
+
+ def to_json
+ # JSON.pretty_generate(data)
+ JSON.generate(data)
+ end
+
+ def save
+ File.open('save.json','w') do |f|
+ f.write(JSON.generate(data))
+ end
+ end
+
+ def resettle sector
+ @spaces[sector].resettle!
+ @resettled_sectors += 1
+ end
+
+ def compute_victory
+ @opposition_bases = 0
+ @support_commitment = @commitment
+ @spaces.each do |n,s|
+ @opposition_bases += s.fln_bases
+ @opposition_bases += s.pop if s.alignment == :oppose
+ @support_commitment += s.pop if s.alignment == :support
+ end
+ end
+
+ private
+
+ def add k, *args
+ s = k.new *args
+ # puts s
+ @names << s.name
+ @spaces[s.name] = s
+ end
+
+ def adjacents i, *args
+ @spaces[@names[i]].adjacents = args
+ # @spaces[@names[i]].adjacents = args.map { |i| @names[i] }
+ # puts @spaces[@names[i]]
+ end
+
+ def feed
+ mountain=Sector::MOUNTAIN
+ border=Sector::BORDER
+ coastal=Sector::COASTAL
+ add Sector, 'Barika', 'I', 1, 1, mountain # 0
+ add Sector, 'Batna', 'I', 2, 0, mountain # 1
+ add Sector, 'Biskra', 'I', 3, 0, border # 2
+ add Sector, 'Oum El Bouaghi', 'I', 4, 0, mountain # 3
+ add Sector, 'Tebessa', 'I', 5, 1, mountain|border # 4
+ add Sector, 'Negrine', 'I', 6, 0, mountain|border # 5
+ add City, 'Constantine', 'II', 2 # 6
+ add Sector, 'Setif', 'II', 1, 1, mountain|coastal # 7
+ add Sector, 'Philippeville', 'II', 2, 2, mountain|coastal # 8
+ add Sector, 'Souk Ahras', 'II', 3, 2, coastal|border # 9
+ add Sector, 'Tizi Ouzou', 'III', 1, 2, mountain|coastal # 10
+ add Sector, 'Bordj Bou Arreridj', 'III', 2, 1, mountain # 11
+ add Sector, 'Bougie', 'III', 3, 2, mountain|coastal # 12
+ add City, 'Algiers', 'IV', 3, coastal # 13
+ add Sector, 'Medea', 'IV', 1, 2, mountain|coastal # 14
+ add Sector, 'Orleansville', 'IV', 2, 2, mountain|coastal # 15
+ add City, 'Oran', 'V', 2, coastal # 16
+ add Sector, 'Mecheria', 'V', 1, 0, mountain|border # 17
+ add Sector, 'Tlemcen', 'V', 2, 1, border|coastal # 18
+ add Sector, 'Sidi Bel Abbes', 'V', 3, 1, coastal # 19
+ add Sector, 'Mostaganem', 'V', 4, 2, mountain|coastal # 20
+ add Sector, 'Saida', 'V', 5, 0, mountain # 21
+ add Sector, 'Mascara', 'V', 6, 0, mountain # 22
+ add Sector, 'Tiaret', 'V', 7, 0, mountain # 23
+ add Sector, 'Ain Sefra', 'V', 8, 0, border # 24
+ add Sector, 'Laghouat', 'V', 9, 0 # 25
+ add Sector, 'Sidi Aissa', 'VI', 1, 0, mountain # 26
+ add Sector, 'Ain Oussera', 'VI', 2, 1, mountain # 27
+ add Country, 'Moroco' # 28
+ add Country, 'Tunisia' # 29
+ adjacents 0, 1, 2, 3, 7, 8, 11, 19
+ adjacents 1, 0, 2, 3, 5
+ adjacents 2, 0, 1, 5, 25, 26, 29
+ adjacents 3, 0, 1, 4, 5, 8, 9
+ adjacents 4, 3, 5, 9, 29
+ adjacents 5, 1, 2, 3, 4, 29
+ adjacents 6, 7, 8
+ adjacents 7, 0, 6, 8, 11, 12
+ adjacents 8, 0, 3, 7, 6, 9
+ adjacents 9, 3, 4, 8, 29
+ adjacents 10, 11, 12, 14
+ adjacents 11, 0, 7, 10, 12, 14, 26
+ adjacents 12, 7, 10, 11
+ adjacents 13, 14
+ adjacents 14, 10, 11, 13, 15, 26, 27
+ adjacents 15, 14, 20, 23, 27
+ adjacents 16, 19
+ adjacents 17, 18, 21, 24, 28
+ adjacents 18, 17, 19, 21, 28
+ adjacents 19, 16, 18, 20, 21, 22
+ adjacents 20, 15, 19, 22, 23
+ adjacents 21, 17, 18, 19, 22, 24
+ adjacents 22, 19, 20, 21, 23, 24
+ adjacents 23, 15, 20, 22, 24, 27
+ adjacents 24, 17, 21, 22, 23, 25, 27, 28
+ adjacents 25, 2, 24, 26, 27
+ adjacents 26, 0, 2, 11, 14, 25, 27
+ adjacents 27, 14, 15, 23, 24, 25, 26
+ adjacents 28, 17, 18, 24
+ adjacents 29, 2, 4, 5, 8
+ end
+
+ def set_sector i, h, align=nil
+ s = @spaces[@names[i]]
+ s.alignment = align unless align.nil?
+ s.add_gov_base h[:govb] if h.has_key? :govb
+ s.add_fln_base h[:flnb] if h.has_key? :flnb
+ s.add_french_troops h[:ft] if h.has_key? :ft
+ s.add_french_police h[:fp] if h.has_key? :fp
+ s.add_algerian_troops h[:at] if h.has_key? :at
+ s.add_algerian_police h[:ap] if h.has_key? :ap
+ s.add_fln_underground h[:fln] if h.has_key? :fln
+ # puts s
+ end
+
+ def short
+ self.commitment = 15
+ self.fln_resources = 15
+ self.gov_resources = 20
+ self.resettled_sectors = 0
+ self.france_track = 4
+ self.border_zone_track = 3
+ @out_of_play.fln_underground = 5
+ @available.gov_bases = 2
+ @available.french_police = 4
+ @available.fln_bases = 7
+ @available.fln_underground = 8
+ resettle 'Setif'
+ resettle 'Tlemcen'
+ resettle 'Bordj Bou Arreridj'
+ raise "resettled sectors not counted" if resettled_sectors != 3
+ set_sector 0, {:ap=>1, :fln=>1}, :oppose
+ set_sector 2, {:fp=>1}
+ set_sector 4, {:ap=>1, :fln=>1}, :oppose
+ set_sector 5, {:fp=>1}
+ set_sector 6, {:fp=>1}, :support
+ set_sector 7, {:fln=>1}
+ set_sector 8, {:ft=>4, :ap=>1, :govb=>1}
+ set_sector 9, {:ft=>1, :ap=>1, :govb=>1, :fln=>1, :flnb=>1}, :oppose
+ set_sector 10, {:fp=>1, :fln=>1, :flnb=>1}, :oppose
+ set_sector 11, {:fp=>1}
+ set_sector 12, {:fp=>1, :fln=>1, :flnb=>1}, :oppose
+ set_sector 13, {:ft=>4, :at=>1, :fp=>1}, :support
+ set_sector 14, {:at=>1, :govb=>1}
+ set_sector 15, {:fp=>1, :ap=>1, :fln=>1, :flnb=>1}, :oppose
+ set_sector 16, {:at=>1, :fp=>1, :ap=>1}, :support
+ set_sector 17, {:fp=>1, :ap=>1}
+ set_sector 18, {:fp=>2, :fln=>1}
+ set_sector 19, {:fp=>1, :govb=>1}
+ set_sector 20, {:fp=>1}
+ set_sector 22, {:fp=>1}
+ set_sector 23, {:fp=>1}
+ set_sector 24, {:fp=>1}
+ set_sector 27, {}, :oppose
+ set_sector 28, {:fln=>4, :flnb=>2}
+ set_sector 29, {:fln=>5, :flnb=>2}
+ compute_victory
+ raise "wrong opposition bases" if @opposition_bases != 19
+ raise "wrong support_commitment" if @support_commitment != 22
+ end
+
+ def medium
+ raise 'MEDIUM scenario net implemented yet'
+ end
+
+ def full
+ raise 'FULL scenario net implemented yet'
+ end
+
+ end
+
+end
+
+# class ColonialTwilight::Sector
+# undef :adjacents=
+# end
+
+if $PROGRAM_NAME == __FILE__
+ def check b
+ # puts '--- Coastal'
+ # b.spaces.select{ |k,s| s.coastal? }.each { |k,s| puts s.name }
+ raise "coastal sectors error" if b.spaces.select{ |k,s| s.coastal? }.size != 14
+ # puts '--- not Mountain'
+ # b.spaces.select{ |k,s| not s.mountain? }.each { |k,s| puts s.name }
+ raise "not moauntain sectors error" if b.spaces.select{ |k,s| not s.mountain? }.size != 9
+ # puts '--- Border'
+ # b.spaces.select{ |k,s| s.border? }.each { |k,s| puts s.name }
+ raise "border sectors error" if b.spaces.select{ |k,s| s.border? }.size != 9
+ # puts '--- City'
+ # b.spaces.select{ |k,s| s.city? }.each { |k,s| puts s.name }
+ raise "city sectors error" if b.spaces.select{ |k,s| s.city? }.size != 3
+ [[0,11],[1,9],[2,9],[3,1]].each do |p,n|
+ # puts "--- Population #{p}"
+ # b.spaces.select{ |k,s| s.pop==p }.each { |k,s| puts s.name }
+ raise "population #{p} error" if b.spaces.select{ |k,s| s.pop==p}.size != n
+ end
+ raise "sectors count wrong" if b.sectors.size != 28
+ end
+
+ def check_forces what, b, v
+ sup, opp, gov, fln = 0, 0, 0, 0
+ ft, fp, at, ap, g = 0, 0, 0, 0, 0
+ gb, fb = 0, 0
+ b.spaces.each do |n,s|
+ sup += 1 if s.alignment == :support
+ opp += 1 if s.alignment == :oppose
+ gov += 1 if s.control == :GOV
+ fln += 1 if s.control == :FLN
+ ft += s.french_troops unless s.french_troops.nil?
+ fp += s.french_police unless s.french_police.nil?
+ at += s.algerian_troops unless s.algerian_troops.nil?
+ ap += s.algerian_police unless s.algerian_police.nil?
+ g += s.fln_underground
+ gb += s.gov_bases unless s.gov_bases.nil?
+ fb += s.fln_bases
+ end
+ raise "wrong support #{sup} != #{v[0]}" if sup != v[0]
+ raise "wrong oppose #{opp} != #{v[1]}" if opp != v[1]
+ raise "wrong GOV control #{gov} != #{v[2]}" if gov != v[2]
+ raise "wrong FLN control #{fln} != #{v[3]}" if fln != v[3]
+ raise "wrong french troops #{ft} != #{v[4]}" if ft != v[4]
+ raise "wrong french police #{fp} != #{v[5]}" if fp != v[5]
+ raise "wrong algerian troops #{at} != #{v[6]}" if at != v[6]
+ raise "wrong algerian police #{ap} != #{v[7]}" if ap != v[7]
+ raise "wrong Guerrillas #{g} != #{v[8]}" if g != v[8]
+ raise "wrong GOV bases #{gb} != #{v[9]}" if gb != v[9]
+ raise "wrong FLN bases #{fb} != #{v[10]}" if fb != v[10]
+ end
+
+ b = ColonialTwilight::Board.new
+ puts 'check'
+ check b
+ b.load :short
+ check_forces 'short', b, [3, 7, 16, 3, 9, 17, 3, 7, 17, 4, 8]
+ puts 'ok'
+end