diff options
| author | Jérémy Zurcher <jeremy@asynk.ch> | 2026-03-11 16:09:25 +0100 |
|---|---|---|
| committer | Jérémy Zurcher <jeremy@asynk.ch> | 2026-03-11 16:09:25 +0100 |
| commit | ea3fcbe27cc2f6b613d5b91a56b5759c41612755 (patch) | |
| tree | 9fe9294b2e9c97509127e75b847a5b8409d1141c /lib | |
| parent | 9458b6413e3609e12f563dcb321d493b5f317017 (diff) | |
| download | colonial-twilight-ea3fcbe27cc2f6b613d5b91a56b5759c41612755.zip colonial-twilight-ea3fcbe27cc2f6b613d5b91a56b5759c41612755.tar.gz | |
update Cli
Diffstat (limited to 'lib')
| -rw-r--r-- | lib/colonial_twilight/cli.rb | 522 |
1 files changed, 298 insertions, 224 deletions
diff --git a/lib/colonial_twilight/cli.rb b/lib/colonial_twilight/cli.rb index 7302b6f..4334d86 100644 --- a/lib/colonial_twilight/cli.rb +++ b/lib/colonial_twilight/cli.rb @@ -1,55 +1,57 @@ +#! /usr/bin/env ruby # frozen_string_literal: true require 'io/console' -require 'colonial_twilight/colorized_string' +require_relative 'colorized_string' -module ColonialTwilight +class Integer + def from + to_s.cyan + end - class Sector - def cli - name.magenta - end - def towhat?; true end + def to + to_s.red end +end - class Box - def cli - Cli::BOXES[name].cyan - end - def towhat?; false end +class Symbol + def from + to_s.cyan end - class Cli + def to + to_s.red + end +end module ColonialTwilight - class Cli - LOGO = ' ____ _ _ _ _____ _ _ _ _ _ '"\n" \ - ' / ___|___ | | ___ _ __ (_) __ _| | |_ _|_ _(_) (_) __ _| |__ | |_ '"\n" \ - '| | / _ \| |/ _ \| \_ `| |/ _` | | | | \ \ /\ / / | | |/ _` | \_ `| __| '"\n" \ - '| |__| (_) | | (_) | | | | | (_| | | | | \ V V /| | | | (_| | | | | |_ '"\n" \ - ' \____\___/|_|\___/|_| |_|_|\__,_|_| |_| \_/\_/ |_|_|_|\__, |_| |_|\__| '"\n" \ - ' |___/ ' + module CliDefinitions FRANCE_TRACK = [' A ', ' B ', ' C ', ' D ', ' E ', ' F '].map { |e| e.white.on_blue }.freeze + TRACK = { gov_score: ' Support & Commitment '.blue.on_black, gov_resources: ' Resources '.blue.on_black, fln_score: ' FLN bases & Opposition '.green.on_black, fln_resources: ' Resources '.green.on_black }.freeze + FACTION = { FLN: ' FLN '.black.on_green, GOV: ' Government '.white.on_blue }.freeze + CONTROL = { FLN: ' FLN '.black.on_green, GOV: ' Government '.white.on_blue, uncontrolled: ' Uncontrolled '.black.on_light_white }.freeze + ALIGNMENT = { oppose: ' Oppose '.black.on_green, support: ' Support '.white.on_blue, neutral: ' Neutral '.black.on_light_white }.freeze + BOXES = { available: 'Available'.cyan, out_of_play: 'Out Of Play'.cyan, @@ -57,6 +59,7 @@ module ColonialTwilight france_track: 'France Track'.white.on_blue, border_track: 'Border Track'.yellow.on_black }.freeze + FORCES = { fln_active: 'Active Guerrillas'.red.on_black, fln_underground: 'Underground Guerrillas'.green.on_black, @@ -67,41 +70,159 @@ module ColonialTwilight algerian_troops: 'Algerian Troops'.black.on_green, algerian_police: 'Algerian Police'.black.on_light_green }.freeze + MARKERS = { terror: 'Terror Marker'.yellow, control: 'Control'.yellow, alignment: 'Alignment'.yellow }.freeze + end + + module CliUtils + def clear_screen + puts String::CLS + end + + def wait_enter + print "\u21b5 " # carriage return + gets + print "\033[1A\033[K\u2713\n" # up, erase line, check + end + + def logo + puts ' ____ _ _ _ _____ _ _ _ _ _ '.white.bold.on_light_green + puts ' / ___|___ | | ___ _ __ (_) __ _| | |_ _|_ _(_) (_) __ _| |__ | |_ '.white.bold.on_light_green + puts '| | / _ \| |/ _ \| \_ `| |/ _` | | | | \ \ /\ / / | | |/ _` | \_ `| __| '.white.bold.on_light_green + puts '| |__| (_) | | (_) | | | | | (_| | | | | \ V V /| | | | (_| | | | | |_ '.white.bold.on_light_green + puts ' \____\___/|_|\___/|_| |_|_|\__,_|_| |_| \_/\_/ |_|_|_|\__, |_| |_|\__| '.white.bold.on_light_green + puts ' |___/ '.white.bold.on_light_green + puts "version : #{ColonialTwilight::VERSION.red}".black.on_white + end + + def split_colon(txt) + a = txt.split(':') + a[0] = a[0].yellow + a.join(':') + end + + def chose(prompt, list, quit: true) + puts "\n => #{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 + print "\t$ " + read_int_or_quit(list.length) + end + + def read_int_or_quit(max) + loop do + s = gets.chomp.strip + exit(0) if s == 'q' + if s =~ /^([0-9]+)$/ + i = ::Regexp.last_match(1).to_i + return i - 1 if i.positive? && (max.nil? || i <= max) + end + puts "\t #{s} is not valid#{max.nil? ? '' : ", must be one of [1..#{max}]"} or 'q'" + print "\t$ " + end + end + + def select_from(list) + l = [] + chrs = [] + show_list(chrs, l) + loop do + chrs = add_char(chrs, $stdin.getch) + if chrs[-1].nil? + chrs = chrs[..-2] + return l[0] if !l.empty? && !chrs.empty? + end + l = strict_match(list, chrs.join) + l = chars_match(list, chrs) if l.empty? + show_list(chrs, l) + end + end + + def show_list(chrs, list) + s = ( + if chrs.size < 2 + "#{(chrs.empty? ? '?' : chrs.join).white.bold} : ..." + else + "#{chrs.join.white.bold} : #{ansi_list(chrs, list)}" + end) + print "#{String::CLEAR_LINE}#{s}" + end + + def ansi_list(chrs, list) + list.join(' * ').chars.map { |c| chrs.include?(c.downcase) ? c.white.bold : c }.join + end + + def add_char(chrs, chr) + case chr.ord + when 13 + chrs << nil + when 8 + chrs[..-2] + else + chrs << chr.downcase + end + end + + def strict_match(list, chars) + list.select { |s| s.downcase =~ /#{chars}/ } + end + + def chars_match(list, chars) + list.select { |s| chars.inject(true) { |v, c| v & s.downcase.include?(c) } } + end + + def yes_no_string(default) + case default + when nil then 'y/n' + when true then 'Y/n' + when false then 'y/N' + end + end + + YES = %w[y yes].freeze + NO = %w[n no].freeze + def ask(prompt, default = nil) + c = yes_no_string(default) + puts + print " => #{prompt.yellow} (#{c}) ? " + loop do + ret = gets.chomp.strip.downcase + return true if YES.include? ret + return false if NO.include? ret + return default if ret.empty? && !default.nil? + + puts "\t\t\t\t'#{ret}' is not valid, (#{c}) ?" + print "\t$ " + end + end + end + + class Cli + include CliDefinitions + include CliUtils def initialize(options) @options = options - @game = ColonialTwilight::Game.new options end - def start + def welcome + clear_screen if @options.clearscreen 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 - @game.play 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 + def chose_scenario(scenarios) + chose('Choose a scenaroo', scenarios) { |_k, v| split_colon(v) } end - def clear_screen - puts String::CLS + def chose_rules(rules) + chose('Choose a ruleset', rules) { |_k, v| split_colon(v) } end def turn_start(turn, first, second) @@ -112,224 +233,177 @@ module ColonialTwilight puts "\t\t 2nd Eligible : ".black.on_white + FACTION[second.faction] 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$ " + def continue?(player) + if player.instance_of?(FLNBot) + l = ["#{'FLN'.yellow} :\t\tlet the FLN bot play", "#{'Pivotal Event'.yellow} :\tplay a Pivotal Event"] + elsif player.instance_of?(GOVPlayer) + l = ["#{'Play'.yellow}:\t\tplay your turn"] + else + puts '' + exit(1) end + chose('Next action', l) end - def show_card card - puts - puts "Current event card : ##{card.num.to_s.yellow} #{card.title.red}" + def d6 + # FIXME: add options to roll dice -> read_int(1-6) + rand(7) end - def player p, first - puts + def pull_card(max) + print "\nEnter the current #{'card number'.yellow} : " + read_int_or_quit(max) + 1 + end + + def show_card(card) + puts "\nCurrent card : ##{card.num.to_s.yellow} #{card.title.red} #{show_capability card} #{show_momentum card}" + end + + def show_capability(card) + s = '' + s += ' FLN capability'.white.on_green if card.fln_capability? + s += ' GOV capability'.white.on_blue if card.gov_capability? + s += 'DUAL capability'.white if card.dual_capability? + s + end + + def show_momentum(card) + s = '' + s += ' FLN momentum'.white.on_green if card.fln_momentum? + s += ' GOV momentum'.white.on_blue if card.gov_momentum? + s += 'DUAL momentum'.white if card.dual_momentum? + s + end + + def show_player(player, first) clear_screen if @options.clearscreen - puts - puts " #{FACTION[p.faction]} is #{first ? '1st Eligible' : '2nd Eligible'}".black.on_white + st = first ? '1st Eligible' : '2nd Eligible' + puts "\n\n #{FACTION[player.faction]} #{st}".black.on_white + " - #{resources(player)} - #{score(player)}\n\n" end - def adjust_track ar - puts - puts "\tadjust #{TRACK[:support_commitment]} from #{ar[0].to_s.cyan} to #{ar[2].to_s.red}" if ar[0] != ar[2] - puts "\tadjust #{TRACK[:opposition_bases]} from #{ar[1].to_s.cyan} to #{ar[3].to_s.red}" if ar[1] != ar[3] + def show_action(action) + show_steps action + show_cost action + wait_enter end - def show_player_action player, h - selected = h[:selected] - faction = FACTION[player.faction] + def select_by_name(list) puts - show_action(faction, selected, h) if h.has_key? :action - # show_activity(faction, selected, h) if h.has_key? :activity - show_transfers(selected, h[:transfers]) if h.has_key? :transfers - show_markers(selected, h[:markers]) if h.has_key? :markers - show_controls(selected, h[:controls]) if h.has_key? :controls - case selected - when :france_track - puts "\t => shift #{1.to_s.cyan} space onto #{FRANCE_TRACK[h[:france_track]]}" - when :border_track - puts "\t => shift #{1.to_s.cyan} space onto #{h[:border_track].to_s.yellow.on_black}" + r = nil + loop do + r = select_from(list) + break if ask "\n => #{r}", true end - print "\u21b5 " # carriage return - gets - print "\033[1A\033[K\u2713\n\n" # up, erase line, check + r end - # def show_control selected, control - # s = "\t => shift #{'control'.cyan} to #{CONTROL[control]}" - # s += " in #{selected.magenta}" if @options.verbose - # puts s - # end + private - def show_controls selected, controls - controls.each do |k,v| - puts "\t => shift #{'control'.cyan} to #{CONTROL[v[1]]} in #{k.cli}" if k != selected - end - if controls.has_key? selected - s = "\t => shift #{'control'.cyan} to #{CONTROL[controls[selected][1]]}" - s += " in #{selected.cli}" if @options.verbose - puts s - end + def resources(player) + resources = "#{player.faction.to_s.downcase}_resources".to_sym + "#{player.resources.to_s.yellow} #{TRACK[resources]}" end - def show_action faction, selected, h - action = h[:action] - rcs = h[:resources] - incr = (rcs[:cost] <=> 0) - v = rcs[:cost].abs.to_s.cyan - if action == :pass - puts "\t#{'PASS'.red} increase #{'resources'.yellow} by #{v} to #{rcs[:value].to_s.red}" - else - action = action.to_s.capitalize - sym = selected.is_a?(Symbol) # FIXME Symbol => France track or Border track - where = sym ? "on #{BOXES[selected]}" : "in #{selected.cli}" - cost = (incr == 0 ? nil : "#{incr < 0 ? 'increase' : 'decrease'} #{'resources'.yellow} by #{v} to #{rcs[:value].to_s.red}") - if @options.verbose - puts "\t#{faction} executes a #{action.yellow} Operation #{where}" - puts "\t#{cost}" unless cost.nil? - puts "\tin #{selected.cli} :" unless sym - else - s = "\t#{action.black.on_white} #{where} " - s += cost unless cost.nil? - puts s - end - end + def score(player) + score = "#{player.faction.to_s.downcase}_score".to_sym + "#{player.score.to_s.yellow} #{TRACK[score]}" end - def show_transfers selected, transfers - transfers.each do |tr| - n = tr[:n].to_s.cyan - from, to = tr[:from], tr[:to] - what, towhat = tr[:what], tr[:towhat] - if from == to - s = "\t => flip #{n} #{FORCES[what]} to #{FORCES[towhat]}" - s += " in #{selected.cyan}" if @options.verbose - puts s - else - s = "\t => transfer #{n} #{FORCES[what]}" - s += " from #{from.cli}" if @options.verbose or from != selected - s += " to #{to.cli}" if @options.verbose or to != selected - s += " as #{FORCES[towhat]}" if @options.verbose or (what != towhat and to.towhat?) - # puts "\t => shift #{'control'.cyan} to #{CONTROL[controls[from]]} in #{from.cli}" if from != selected and controls.has_key?(from) - # puts "\t => shift #{'control'.cyan} to #{CONTROL[controls[to]]} in #{to.cli}" if to != selected and controls.has_key?(to) - puts s - end - end + # ACTIONS + + def show_cost(action) + return if action.cost.zero? + + v = action.cost.abs + sign = action.cost.negative? ? 'increase' : 'decrease' + puts "\t=> #{sign} #{'resources'.yellow} by #{v.from} to #{action.resources.to}" end - def show_markers selected, markers - markers.each do |mk| - what, n, towards, from, to = mk - case what - when :terror - act = (n > 0 ? 'add' : 'remove').red - s = "\t => #{act} #{n.to_s.cyan} #{'Terror'.yellow} markers" - s += " in #{selected.cli} from #{from.to_s.cyan} to #{to.to_s.red}" if @options.verbose - puts s - when :alignment - if @options.verbose - puts "\t => shift #{selected.cli} #{n.to_s.cyan} times towards #{ALIGNMENT[towards]} from #{ALIGNMENT[from]} to #{ALIGNMENT[to]}" - else - puts "\t => shift #{'Alignment'.yellow} onto #{ALIGNMENT[to]}" - end - end + def show_steps(action) + return show_pass(action) if action.type == :pass + + puts " #{action.name.yellow} in #{to_cli(action.space)}#{to_agitate(action)}" + action.steps.each do |step| + s = case step[:kind] + when :transfer then show_transfer(step, action.space) + when :shift then show_shift(step) + when :set then show_set(step) + when :extort then show_extort(step) + when :agitate then show_agitate(step) + end + # show_action(faction, selected, action) if action.key? :action + # # show_activity(faction, selected, action) if action.key? :activity + # show_markers(selected, action[:markers]) if action.key? :markers + # show_controls(selected, action[:controls]) if action.key? :controls + # if selected == :france_track + # puts "\t => shift #{1.to_s.cyan} space onto #{FRANCE_TRACK[action[:france_track]]}" + # elsif selected == :border_track + # puts "\t => shift #{1.to_s.cyan} space onto #{action[:border_track].to_s.yellow.on_black}" + # end + puts s unless s.nil? 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 = chose('Next action', l, true) { |s| a = s.split(':'); a[0] = a[0].yellow; a.join(':') } - exit(0) if ret < 0 + def to_agitate(action) + action.to_agitate_in.nil? ? '' : " to #{'Agitate'.yellow} in #{to_cli(action.to_agitate_in)}" 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 + def show_pass(action) + puts " #{action.name.yellow}" + end - 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$ " + def show_transfer(step, selected) + src = to_cli(step[:src]) + dst = to_cli(step[:dst]) + flip = !step[:flip] + s = "\t=> transfer #{step[:num].to} #{FORCES[step[:what]]}" + s += " from #{src}" if @options.verbose || step[:src] != selected + s += " to #{dst}" if @options.verbose || step[:dst] != selected + s += " as #{FORCES[step[:flip]]}" unless flip + # FIXME: only if last step, what does it mean ?? + show_control_shift(s, step, src, dst) + end + + def show_shift(step) + n = step[:num].from + case step[:dst] + when :france_track then "\t=> shift #{n} space onto #{FRANCE_TRACK[step[:track]]}" + when :border_track then "\t=> shift #{n} space onto #{BOXES[:border_track]}" end end - YES=['y','yes'] - NO=['n','no'] - def ask prompt, default=nil - puts - c = (default.nil? ? 'y/n' : (default ? 'Y/n' : 'y/N')) - print " => #{prompt.yellow} (#{c}) ? " - loop do - ret = gets.chomp.strip.downcase - return true if YES.include? ret - return false if NO.include? ret - return default unless default.nil? + def show_set(step) + return if (w = step[:what]).nil? - puts "\t\t\t\t'#{ret}' is not valid, (y/n) ?" - print "\t$ " - end + t = (v = step[w]).instance_of?(Integer) ? v.to : ALIGNMENT[v] + "\t=> set #{MARKERS[w]} to #{t}" end - end + def show_extort(_step) + # src = to_cli(step[:src]) + "\t=> flip 1 #{FORCES[:fln_underground]} to #{FORCES[:fln_active]}" + end -end + def show_agitate(step) + s = (step[:terror].zero? ? '' : "\t=> remove #{step[:terror].from} #{MARKERS[:terror]} to #{step[:terror_level].to}") + s += "\n" if !s.empty? && step[:shift].positive? + s += "\t=> shift #{MARKERS[:alignment]} to #{ALIGNMENT[step[:alignment]]}" if step[:shift] + s + end -if $PROGRAM_NAME == __FILE__ - class O - def clearscreen; false end - end - class P - def initialize f; @faction = f end - def faction; @faction end - end - io = ColonialTwilight::Cli.new O.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 666, P.new(:FLN), P.new(:GOV) - ColonialTwilight::Cli::FACTION.each do |k,f| - puts " #{k} => #{f}" - end - ColonialTwilight::Cli::CONTROL.each do |k,f| - puts " #{k} => #{f}" - end - ColonialTwilight::Cli::ALIGNMENT.each do |k,f| - puts " #{k} => #{f}" - end - ColonialTwilight::Cli::BOXES.each do |k,f| - puts " #{k} => #{f}" - end - ColonialTwilight::Cli::FORCES.each do |k,f| - puts " #{k} => #{f}" + # def show_cube_shift + # s += "\t => flip #{n} #{FORCES[what]} to #{FORCES[towhat]}" + # s += " in #{selected.cyan}" if @options.verbose + # end + + def show_control_shift(str, step, src, dst) + str += "\n\t=> shift #{MARKERS[:control]} to #{CONTROL[step[:src_control]]} in #{src}" if step.key? :src_control + str += "\n\t=> shift #{MARKERS[:control]} to #{CONTROL[step[:dst_control]]} in #{dst}" if step.key? :dst_control + str + end + + def to_cli(obj) + obj.instance_of?(Symbol) ? BOXES[obj] : obj.name.magenta + end end end |
