summaryrefslogtreecommitdiffstats
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
downloadcolonial-twilight-94d2e329a4b9c325b32de5bd641e5e1e99a1c8f4.zip
colonial-twilight-94d2e329a4b9c325b32de5bd641e5e1e99a1c8f4.tar.gz
Inital commit
-rw-r--r--.gitignore1
-rw-r--r--Gemfile3
-rw-r--r--Gemfile.lock19
-rw-r--r--LICENSE.md19
-rw-r--r--README.md12
-rwxr-xr-xbin/ColonialTwilight.rb77
-rw-r--r--colonial_twilight.gemspec19
-rw-r--r--lib/colonial_twilight.rb10
-rw-r--r--lib/colonial_twilight/board.rb492
-rw-r--r--lib/colonial_twilight/cards.rb128
-rw-r--r--lib/colonial_twilight/cli.rb136
-rw-r--r--lib/colonial_twilight/colorized_string.rb85
-rw-r--r--lib/colonial_twilight/fln_bot.rb21
-rw-r--r--lib/colonial_twilight/game.rb101
-rw-r--r--lib/colonial_twilight/player.rb27
-rwxr-xr-xrun2
16 files changed, 1152 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..b25c15b
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1 @@
+*~
diff --git a/Gemfile b/Gemfile
new file mode 100644
index 0000000..c80ee36
--- /dev/null
+++ b/Gemfile
@@ -0,0 +1,3 @@
+source "http://rubygems.org"
+
+gemspec
diff --git a/Gemfile.lock b/Gemfile.lock
new file mode 100644
index 0000000..f382c94
--- /dev/null
+++ b/Gemfile.lock
@@ -0,0 +1,19 @@
+PATH
+ remote: .
+ specs:
+ ColonialTwilight (0.1.0)
+ colorize
+
+GEM
+ remote: http://rubygems.org/
+ specs:
+ colorize (0.8.1)
+
+PLATFORMS
+ ruby
+
+DEPENDENCIES
+ ColonialTwilight!
+
+BUNDLED WITH
+ 2.1.4
diff --git a/LICENSE.md b/LICENSE.md
new file mode 100644
index 0000000..dfa5e67
--- /dev/null
+++ b/LICENSE.md
@@ -0,0 +1,19 @@
+Copyright (c) 2020, Jérémy Zurcher
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..3a701e8
--- /dev/null
+++ b/README.md
@@ -0,0 +1,12 @@
+
+# ColonialTwilight
+
+I played many solo games of GMT's [Labyrinth](https://www.gmtgames.com/p-720-labyrinth-4th-printing.aspx)
+using [awakening](https://github.com/sellmerfud/awakening), a scala implementation of both the Jihadist and the USA bots.
+
+The only bad point I see in that console application is the poor readability of the output.
+
+So there it is, my proof of concept, a quick and dirty ruby implementation of the FLN bot of GMT's [Colonial Twilight](https://www.gmtgames.com/p-548-colonial-twilight-the-french-algerian-war-1954-62.aspx), with a nice and colorful output and place to implement a GUI, once maybe.
+
+CLI behaviour is copied on [coltwi](https://github.com/sellmerfud/coltwi).
+
diff --git a/bin/ColonialTwilight.rb b/bin/ColonialTwilight.rb
new file mode 100755
index 0000000..9a376c9
--- /dev/null
+++ b/bin/ColonialTwilight.rb
@@ -0,0 +1,77 @@
+#! /usr/bin/env ruby
+# -*- coding: UTF-8 -*-
+
+require 'optparse'
+
+require 'colonial_twilight'
+
+class OptParser
+
+ class Options
+
+ attr_accessor :verbose, :clearscreen
+
+ def initialize
+ @verbose = false
+ @gui = false
+ @clearscreen = false
+ end
+
+ def define_options(parser)
+ parser.banner = "Usage: ColonialTwilight.rb [options]"
+ parser.separator ""
+ parser.separator "Specific options:"
+
+ add_verbose(parser)
+ add_gui(parser)
+ add_clearscreen(parser)
+
+ parser.separator ""
+ parser.separator "Common options:"
+ parser.on_tail("-h", "--help", "Show this message") do
+ puts parser
+ exit
+ end
+ parser.on_tail("--version", "Show version") do
+ puts ColonialTwilight::VERSION
+ exit
+ end
+ end
+
+ def add_verbose(parser)
+ parser.on("-v", "--[no-]verbose", "Run verbosely") do |v|
+ @verbose = v
+ end
+ end
+ def add_gui(parser)
+ parser.on("-g", "--gui", "Run verbosely") do
+ @gui = true
+ puts "gui is not implemented yet ..."
+ exit
+ end
+ end
+ def add_clearscreen(parser)
+ parser.on("-c", "--clearscreen", "Clear screen before each player turn") do |v|
+ @clearscreen = true
+ end
+ end
+ end
+
+ def parse(args)
+ @options = Options.new
+ @parser = OptionParser.new do |parser|
+ @options.define_options(parser)
+ parser.parse!(args)
+ end
+ @options
+ end
+ attr_reader :parser, :options
+
+end
+
+parser = OptParser.new
+options = parser.parse(ARGV)
+
+require 'colonial_twilight/cli'
+game = ColonialTwilight::Cli.new options
+game.start
diff --git a/colonial_twilight.gemspec b/colonial_twilight.gemspec
new file mode 100644
index 0000000..51f0af3
--- /dev/null
+++ b/colonial_twilight.gemspec
@@ -0,0 +1,19 @@
+#! /usr/bin/env ruby
+# -*- coding: UTF-8 -*-
+
+$:.push File.expand_path('../lib', __FILE__)
+require 'colonial_twilight'
+
+Gem::Specification.new do |s|
+ s.name = 'ColonialTwilight'
+ s.version = ColonialTwilight::VERSION
+ s.licenses = ['MIT']
+ s.authors = ['Jérémy Zurcher']
+ s.email = ['jeremy@asynk.ch']
+ s.homepage = 'https://asynk.ch'
+ s.summary = %q{FLN bot for GMT's Colonial Twilight.}
+ s.description = %q{This is a colorful FLN bot for GMT's COIN series' Colonial Twilight.}
+
+ s.files = Dir['lib/**/*.rb']
+ s.executables = 'ColonialTwilight.rb'
+end
diff --git a/lib/colonial_twilight.rb b/lib/colonial_twilight.rb
new file mode 100644
index 0000000..5785241
--- /dev/null
+++ b/lib/colonial_twilight.rb
@@ -0,0 +1,10 @@
+#! /usr/bin/env ruby
+# -*- coding: UTF-8 -*-
+
+module ColonialTwilight
+ MAJOR = 0
+ MINOR = 1
+ REVISION = 0
+ VERSION = [MAJOR,MINOR,REVISION].join '.'
+end
+
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
diff --git a/lib/colonial_twilight/cards.rb b/lib/colonial_twilight/cards.rb
new file mode 100644
index 0000000..4bc7a9f
--- /dev/null
+++ b/lib/colonial_twilight/cards.rb
@@ -0,0 +1,128 @@
+#! /usr/bin/env ruby
+# -*- coding: UTF-8 -*-
+
+module ColonialTwilight
+
+ CARD_SINGLE=1
+ CARD_FLN_MARKED=2
+ CARD_ALWAYS_PLAY=4
+
+ class Card
+ attr_reader :num, :title
+ def initialize n, t, attr, a0=nil, a1=nil
+ @num = n
+ @title = t
+ @attributes = attr
+ @a0 = a0
+ @a1 = a1
+ end
+ def dual?; @attributes & CARD_SINGLE == 0 end
+ def single?; @attributes & CARD_SINGLE == CARD_SINGLE end
+ def flnmarked?; @attributes & CARD_FLN_MARKED == CARD_FLN_MARKED end
+ def alwaysplay?; @attributes & CARD_ALWAYS_PLAY == CARD_ALWAYS_PLAY end
+ def check
+ # @attributes.each do |attr| raise "unknown attribute : #{attr}" if attr not in ATTRS end
+ puts single?
+ puts dual?
+ puts flnmarked?
+ puts alwaysplay?
+ end
+ end
+
+ class CardAction
+ def initialize t, c
+ @txt = t
+ @condition=c
+ end
+ end
+
+ class Deck
+ attr_reader :cards
+ def initialize
+ @cards = {}
+ add_card 1, 'Quadrillage', 0, CardAction.new('Place up to all French Police in Available in up to 3 spaces', {:what=>:french_police,:from=>:available})
+ end
+
+ def pull n; @cards[n] end
+
+ private
+
+ def add_card num, title, attrs, action
+ @cards[num] = Card.new num, title, attrs
+ @cards[num].check
+ end
+
+ end
+
+end
+
+ # 'Balky Conscripts'
+ # 'Leadership Snatch'
+ # 'Oil & Gas Discoveries'
+ # 'Peace of the Brave'
+ # 'Factionalism'
+ # '5th Bureau'
+ # 'Cross-border air strike'
+ # 'Beni-Oui-Oui'
+ # 'Moudjahidine'
+ # 'Bananes'
+ # 'Ventilos'
+ # 'SAS'
+ # 'Protest in Paris'
+ # 'Jean-Paul Sarte'
+ # 'NATO'
+ # 'Commandos'
+ # 'Torture'
+ # 'General Strike'
+ # 'Sauve qui peut'
+ # 'United Nations Resolution'
+ # 'The Government of USA is Convinced...'
+ # 'Diplomatic Leanings'
+ # 'Economic Development'
+ # 'Purge'
+ # 'Casbah'
+ # 'Covert Movement'
+ # 'Atrocities and Reprisals'
+ # 'The Call Up'
+ # 'Change in Tactics'
+ # 'Intimidation'
+ # 'Teleb the Bomb-maker'
+ # 'Overkill'
+ # 'Elections'
+ # 'Napalm'
+ # 'Assassination'
+ # 'Integration'
+ # 'Economic Crisis in France'
+ # 'Retreat into Djebel'
+ # 'Strategic Movement'
+ # 'Egypt'
+ # 'Czech Arms Deal'
+ # 'Refugees'
+ # 'Paranoia'
+ # 'Challe Plan'
+ # 'Moghazni'
+ # 'Third Force'
+ # 'Ultras'
+ # 'Factional Plot'
+ # 'Bleuite'
+ # 'Stripey Hole'
+ # 'Cabinet Shuffle'
+ # 'Population Control'
+ # 'Operation 744'
+ # 'Development'
+ # 'Hardened Attitudes'
+ # 'Peace Talks'
+ # 'Army in Waiting'
+ # 'Bandung Conference'
+ # 'Soummam Conference'
+ # 'Morocco and Tunisia Independent'
+ # 'Suez Crisis'
+ # 'OAS'
+ # 'Mobilization'
+ # 'Recall De Gaulle'
+ # "Coup d'etat"
+ # "Propaganda!"
+ # "Propaganda!"
+ # "Propaganda!"
+ # "Propaganda!"
+ # "Propaganda!"
diff --git a/lib/colonial_twilight/cli.rb b/lib/colonial_twilight/cli.rb
new file mode 100644
index 0000000..d5a5c53
--- /dev/null
+++ b/lib/colonial_twilight/cli.rb
@@ -0,0 +1,136 @@
+#! /usr/bin/env ruby
+# -*- coding: UTF-8 -*-
+
+require 'colonial_twilight'
+require 'colonial_twilight/colorized_string'
+require 'colonial_twilight/game'
+
+module ColonialTwilight
+
+ class Cli
+
+ def initialize options
+ @options = options
+ @game = ColonialTwilight::Game.new options
+ end
+
+ def start
+ logo
+ ret = []
+ ret << chose('Choose a scenario', @game.scenarios) { |s| a = s.split(':'); a[0] = a[0].yellow; a.join(':') }
+ exit(0) if ret[-1] < 0
+ ret << chose('Choose a ruleset', @game.rules) { |s| a = s.split('-'); a[0] = a[0].yellow; a.join('-') }
+ exit(0) if ret[-1] < 0
+ @game.start self, *ret
+ end
+
+ def logo
+ clear_screen
+ puts (' ____ _ _ _ _____ _ _ _ _ _ '+
+ "\n"' / ___|___ | | ___ _ __ (_) __ _| | |_ _|_ _(_) (_) __ _| |__ | |_ ' +
+ "\n"'| | / _ \| |/ _ \| \'_ \| |/ _` | | | | \ \ /\ / / | | |/ _` | \'_ \| __| ' +
+ "\n"'| |__| (_) | | (_) | | | | | (_| | | | | \ V V /| | | | (_| | | | | |_ ' +
+ "\n"' \____\___/|_|\___/|_| |_|_|\__,_|_| |_| \_/\_/ |_|_|_|\__, |_| |_|\__| ' +
+ "\n"' |___/ ').white.bold.on_light_green
+ puts "version : #{ColonialTwilight::VERSION.red}".black.on_white
+ end
+
+ def clear_screen
+ puts String::CLS
+ end
+
+ PS = { :FLN => 'FLN'.red, :GOV => 'Government'.red }
+
+ def turn_start turn, first, second
+ clear_screen if @options.clearscreen
+ puts
+ puts ("=" * 80).white.bold.on_light_green
+ puts " Turn : #{turn.to_s.red} ".black.on_white + "\t First Eligible : #{PS[first.faction]} ".black.on_white
+ puts "\t\t Second Eligible : #{PS[second.faction]} ".black.on_white
+ end
+
+ def pull_card max
+ puts
+ printf "Enter the current #{'card number'.yellow} : "
+ while true
+ s = gets.chomp
+ if s.to_i.to_s == s.to_s
+ ret = s.to_i
+ return ret if ret < max
+ end
+ puts "\t\t\t\t'#{s}' is not valid, must be one of [1..#{max}]"
+ printf "\t$ "
+ end
+ end
+
+ def show_card card
+ puts
+ puts "Current event card : ##{card.num.to_s.yellow} #{card.title.red}"
+ end
+
+ def player p, first
+ puts
+ clear_screen if @options.clearscreen
+ puts
+ puts " #{PS[p.faction]} is #{first ? 'First Eligible' : 'Second Eligible'}".black.on_white
+ end
+
+ def chose prompt, list, quit=false
+ puts
+ puts " => #{prompt.yellow}:"
+ puts ('-'*(prompt.size + 5)).white.bold
+ list.each_with_index do |el, i|
+ puts "\t#{(i+1).to_s.bold}) : #{block_given? ? yield(el): el}"
+ end
+ puts "\tq) : Quit" if quit
+
+ printf "\t$ "
+ ret = -1
+ while true
+ s = gets.chomp
+ return -1 if s == 'q'
+ if s.to_i.to_s == s.to_s
+ ret = s.to_i
+ return ret - 1 if ret >= 1 and ret <= list.length
+ end
+ puts "\t\t\t\t'#{s}' is not valid, must be one of [1..#{list.length}]"
+ printf "\t$ "
+ end
+ end
+
+ YES=['y','yes']
+ NO=['n','no']
+ def ask prompt, default=nil
+ puts
+ c = (default.nil? ? 'y/n' : (default ? 'Y/n' : 'y/N'))
+ printf " => #{prompt.yellow} (#{c}) ? "
+ while true
+ ret = gets.chomp.downcase
+ return true if YES.include? ret
+ return false if NO.include? ret
+ return default if not default.nil?
+ puts "\t\t\t\t'#{ret}' is not valid, (y/n) ?"
+ printf "\t$ "
+ end
+ end
+
+ end
+
+end
+
+if $PROGRAM_NAME == __FILE__
+ io = ColonialTwilight::Cli.new
+ io.logo
+ puts
+ l = ['Short: 1960-1962: The End Game','Medium: 1957-1962: Midgame Development','Full: 1955-1962: Algerie Francaise!']
+ ret = io.chose('Choose a scenario', l) { |s| a = s.split(':'); a[0] = a[0].yellow; a.join(':') }
+ puts l[ret]
+ ret = io.ask 'Are you sure'
+ puts ret
+ ret = io.ask 'Are you sure', true
+ puts ret
+ ret = io.ask 'Are you sure', false
+ puts ret
+ puts
+ io.turn_start(1, [:FLN, :GOV])
+end
diff --git a/lib/colonial_twilight/colorized_string.rb b/lib/colonial_twilight/colorized_string.rb
new file mode 100644
index 0000000..6f1d55b
--- /dev/null
+++ b/lib/colonial_twilight/colorized_string.rb
@@ -0,0 +1,85 @@
+#! /usr/bin/env ruby
+# -*- coding: UTF-8 -*-
+
+class String
+
+ CLS="\033[0;0f\033\[2J".freeze
+
+ @color_codes = {
+ :black => 0, :light_black => 60,
+ :red => 1, :light_red => 61,
+ :green => 2, :light_green => 62,
+ :yellow => 3, :light_yellow => 63,
+ :blue => 4, :light_blue => 64,
+ :magenta => 5, :light_magenta => 65,
+ :cyan => 6, :light_cyan => 66,
+ :white => 7, :light_white => 67,
+ :default => 9
+ }
+ @color_codes.default=9
+ @color_modes = {
+ :default => 0, # Turn off all attributes
+ :bold => 1, # Set bold mode
+ :italic => 3, # Set italic mode
+ :underline => 4, # Set underline mode
+ :blink => 5, # Set blink mode
+ :swap => 7, # Exchange foreground and background colors
+ :hide => 8 # Hide text (foreground color would be the same as background)
+ }
+ @color_modes.default=0
+ @syms = [:fg, :bg, :mode]
+
+ class << self
+ attr_reader :color_codes, :color_modes, :syms
+ def create_methods
+ color_codes.keys.each do |cc|
+ next if cc == :default
+ define_method cc do colorize(:fg=>cc) end
+ define_method "on_#{cc}" do colorize(:bg=>cc) end
+ end
+ color_modes.keys.each do |cc|
+ next if cc == :default
+ define_method cc do colorize(:mode=>cc) end
+ end
+
+ end
+ end
+ create_methods
+
+ START="\033[".freeze
+ RESET="\033[0m".freeze
+ START_RE=/^\033\[([0-9;]+)m/
+ RESET_RE=/(?<!^)\033\[0m(?!$)/
+
+ def colorize h
+ code = h.inject([]) { |a,(k,v)| a<<resolve(k,v) if self.class.syms.include? k; a }.join(';')
+ return code if code.empty?
+ s = (
+ if self =~ START_RE # merge with existing escape sequence
+ prev = /(?<!^)\033\[#{$1}m(?!$)/
+ code = START + $1 + ';' + code + 'm'
+ self.sub(START_RE, code)
+ else
+ prev = RESET_RE
+ code = START + code + 'm'
+ code + self
+ end
+ )
+ s.gsub!(prev, code)
+ s+= RESET unless s[-4..] == RESET
+ s
+ end
+
+ private
+
+ def resolve k, v
+ return self.class.color_codes[v] + 30 if k == :fg
+ return self.class.color_codes[v] + 40 if k == :bg
+ return self.class.color_modes[v] if k == :mode
+ end
+
+end
+
+if $PROGRAM_NAME == __FILE__
+ puts "RED >> #{"blue".colorize(:fg=>:blue,:bg=>nil)} #{"green".colorize(:fg=>nil).on_green} << DER".white.on_red.underline
+end
diff --git a/lib/colonial_twilight/fln_bot.rb b/lib/colonial_twilight/fln_bot.rb
new file mode 100644
index 0000000..9944507
--- /dev/null
+++ b/lib/colonial_twilight/fln_bot.rb
@@ -0,0 +1,21 @@
+#! /usr/bin/env ruby
+# -*- coding: UTF-8 -*-
+
+module ColonialTwilight
+
+ class FLNBot
+
+ attr_reader :faction
+
+ def initialize game, faction
+ @game = game
+ @faction = faction
+ end
+
+ def play possible_actions
+ puts 'FLNBot.play' #FIXME
+ end
+
+ end
+
+end
diff --git a/lib/colonial_twilight/game.rb b/lib/colonial_twilight/game.rb
new file mode 100644
index 0000000..43d6318
--- /dev/null
+++ b/lib/colonial_twilight/game.rb
@@ -0,0 +1,101 @@
+#! /usr/bin/env ruby
+# -*- coding: UTF-8 -*-
+
+require 'colonial_twilight/board'
+require 'colonial_twilight/cards'
+require 'colonial_twilight/player'
+require 'colonial_twilight/fln_bot'
+
+module ColonialTwilight
+
+ class Game
+
+ @scenarios = ['Short: 1960-1962: The End Game',
+ 'Medium: 1957-1962: Midgame Development',
+ 'Full: 1955-1962: Algérie Francaise!'].freeze
+ @rules = ['Standard Rules - No Support Phase in final Propaganda round',
+ 'Optional Rule 8.5.1 - Conduct Support Phase in final Propaganda round'].freeze
+ @states = {
+ :event => 'Event: execute the Event card',
+ :ope_special => 'Operation & Special Activity: conduct an Operation in any number of spaces with a Special Activity',
+ :ope_only => 'Operation Only: conduct an Operation in any number of spaces without a Special Activity',
+ :ope_limited => 'Limited Operation: conduct an Operation in 1 space without a Special Activity',
+ :pass => 'Pass: increase your Resources'
+ }.freeze
+ class << self
+ attr_reader :scenarios, :rules, :states, :cards
+ end
+ def rules; Game.rules end
+ def scenarios; Game.scenarios end
+ def possible_actions used=nil
+ ks = Game.states.keys
+ if not used.nil?
+ if used == :event
+ ks.delete :event
+ ks.delete :ope_only
+ ks.delete :ope_limited
+ elsif used == :ope_special
+ ks.delete :ope_special
+ ks.delete :ope_only
+ elsif used == :ope_limited
+ ks.delete :ope_limited
+ ks.delete :event
+ elsif used == :ope_only
+ ks.delete :ope_only
+ ks.delete :event
+ ks.delete :ope_special
+ end
+ end
+ Game.states.select { |k,v| ks.include? k }
+ end
+
+ attr_reader :scenario, :ruleset, :board, :ui, :cards
+ def initialize options
+ @options = options
+ @board = ColonialTwilight::Board.new
+ @deck = ColonialTwilight::Deck.new
+ end
+
+ def start ui, s, rs
+ @ui = ui
+ @ruleset = rs
+ @scenario = s
+ @board.load [:short, :medium, :long][s]
+ @max_card = 71
+ @turn = 1
+ @cards = []
+ @actions = []
+ @players = [FLNBot.new(self, :FLN), Player.new(self, :GOV)]
+ play
+ end
+
+ def play
+ while true
+ ui.turn_start @turn, *@players
+ c = ui.pull_card @max_card
+ @cards << @deck.pull(1) # FIXME
+ ui.show_card @cards[-1]
+
+ continue? @players[0].instance_of? FLNBot
+ ui.player @players[0], true
+ @actions[0] = @players[0].play possible_actions
+
+ continue? @players[1].instance_of? FLNBot
+ ui.player @players[1], false
+ @actions[1] = @players[1].play possible_actions @actions[0]
+
+ @cards.shift if @cards.length > 2
+ @turn += 1
+ # TURN END ...
+ end
+ end
+
+ def continue? bot
+ l = bot ? ["FLN :\t\tlet the FLN bot play", "Pivotal Event:\tplay a Pivotal Event"] : ["Play:\t\tplay your turn"]
+ ret = ui.chose('Next action', l, true) { |s| a = s.split(':'); a[0] = a[0].yellow; a.join(':') }
+ exit(0) if ret < 0
+ end
+
+ end
+
+end
diff --git a/lib/colonial_twilight/player.rb b/lib/colonial_twilight/player.rb
new file mode 100644
index 0000000..16e0b04
--- /dev/null
+++ b/lib/colonial_twilight/player.rb
@@ -0,0 +1,27 @@
+#! /usr/bin/env ruby
+# -*- coding: UTF-8 -*-
+
+module ColonialTwilight
+
+ class Player
+
+ attr_reader :faction
+
+ def initialize game, faction
+ @game = game
+ @faction = faction
+ end
+
+ def to_s
+ @faction.to_s
+ end
+
+ def play possible_actions
+ action = @game.ui.chose( 'Choose an action', possible_actions.values) { |s| a = s.split(':'); a[0] = a[0].yellow; a.join(':') }
+ puts 'Player.play' # FIXME
+ return action
+ end
+
+ end
+
+end
diff --git a/run b/run
new file mode 100755
index 0000000..2b0cabd
--- /dev/null
+++ b/run
@@ -0,0 +1,2 @@
+#! /bin/bash
+ruby -Ilib ./bin/ColonialTwilight.rb $@