diff options
author | Jérémy Zurcher <jeremy@asynk.ch> | 2020-08-05 19:14:26 +0200 |
---|---|---|
committer | Jérémy Zurcher <jeremy@asynk.ch> | 2020-08-05 19:14:26 +0200 |
commit | 94d2e329a4b9c325b32de5bd641e5e1e99a1c8f4 (patch) | |
tree | 1d9f733e12eb8dc27610da0d52674ae1aec9fefc | |
download | colonial-twilight-94d2e329a4b9c325b32de5bd641e5e1e99a1c8f4.zip colonial-twilight-94d2e329a4b9c325b32de5bd641e5e1e99a1c8f4.tar.gz |
Inital commit
-rw-r--r-- | .gitignore | 1 | ||||
-rw-r--r-- | Gemfile | 3 | ||||
-rw-r--r-- | Gemfile.lock | 19 | ||||
-rw-r--r-- | LICENSE.md | 19 | ||||
-rw-r--r-- | README.md | 12 | ||||
-rwxr-xr-x | bin/ColonialTwilight.rb | 77 | ||||
-rw-r--r-- | colonial_twilight.gemspec | 19 | ||||
-rw-r--r-- | lib/colonial_twilight.rb | 10 | ||||
-rw-r--r-- | lib/colonial_twilight/board.rb | 492 | ||||
-rw-r--r-- | lib/colonial_twilight/cards.rb | 128 | ||||
-rw-r--r-- | lib/colonial_twilight/cli.rb | 136 | ||||
-rw-r--r-- | lib/colonial_twilight/colorized_string.rb | 85 | ||||
-rw-r--r-- | lib/colonial_twilight/fln_bot.rb | 21 | ||||
-rw-r--r-- | lib/colonial_twilight/game.rb | 101 | ||||
-rw-r--r-- | lib/colonial_twilight/player.rb | 27 | ||||
-rwxr-xr-x | run | 2 |
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 @@ +*~ @@ -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 @@ -0,0 +1,2 @@ +#! /bin/bash +ruby -Ilib ./bin/ColonialTwilight.rb $@ |