Created
September 7, 2016 17:33
-
-
Save smaximov/49958538c02ab0a811954817d6e0b15d to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env ruby | |
# frozen_string_literal: true | |
require 'forwardable' | |
require 'optparse' | |
module Hurkle | |
VERSION = '0.1.0' | |
InvalidInput = Class.new(StandardError) | |
class Game | |
LABELS = ('A'..'Z').to_a | |
extend Forwardable | |
def_delegators :@opts, :dimensions, :size, :turns, :first_turn | |
def initialize(opts) | |
@opts = opts | |
raise ArgumentErrror, "Too many dimensions: #{dimensions}" if dimensions > LABELS.size | |
@secret = Array.new(dimensions) { rand(size) } | |
@labels = LABELS.sample(dimensions) | |
@turn = 0 | |
@turn_order = [:player_turn, :enemy_turn] | |
@turn_order.rotate! unless first_turn == :player | |
end | |
def strategy | |
@strategy ||= Strategy.get(@opts.strategy).new(dimensions, size) | |
end | |
def run | |
display_intro | |
turns.times do | |
guess = play_turn | |
return game_over(current_player) if correct?(guess) | |
estimate = guess_estimate(guess) | |
strategy.update(estimate) | |
display(estimate) | |
next_turn | |
end | |
game_over | |
end | |
private | |
def display_intro | |
grid = Array.new(dimensions) { size }.join('x') | |
enemy_behavior = if @opts.strategy == :optimal | |
'trying to be optimal' | |
else | |
'acting at random' | |
end | |
puts <<~INTRO | |
Trap a hurkle in a #{dimensions}-dimensional grid (#{grid}) | |
You've got #{turns} turns | |
Enemy is #{enemy_behavior} | |
INTRO | |
end | |
def display(estimate) | |
message = @labels.zip(estimate).map do |(label, (value, cmp))| | |
op = case cmp | |
when -1 then '>' | |
when +1 then '<' | |
else '=' | |
end | |
"#{label} #{op} #{value}" | |
end.join('; ') | |
puts message | |
end | |
def play_turn | |
send(current_player) | |
end | |
def next_turn | |
@turn_order.rotate! | |
end | |
def game_over(winner = nil) | |
message = case winner | |
when :player_turn then 'CONGRATULATION! A WINRAR IS YOU!' | |
when :enemy_turn then 'You lose!' | |
else "The hurkle managed to get away! It was here: #{@secret.join(', ')}" | |
end | |
puts message | |
@secret | |
end | |
def enemy_turn | |
guess = strategy.guess | |
puts "Enemy's guess: (#{guess.join(', ')})..." | |
guess | |
end | |
def player_turn | |
input = prompt("Take a guess (#{@labels.join(', ')})") | |
parse_input(input) | |
rescue InvalidInput => e | |
$stderr.puts e.message | |
retry | |
end | |
def parse_input(input) | |
guess = input.split(',').map(&:strip).map do |value| | |
begin | |
Integer(value, 10) | |
rescue ArgumentError | |
raise InvalidInput, "Invalid coordinate: #{value}" | |
end | |
end | |
raise InvalidInput, 'Dimensions mismatch' if guess.size != dimensions | |
guess | |
end | |
def prompt(str) | |
print str, ': ' | |
$stdout.flush | |
input = gets | |
abort("\nBye!") if input.nil? # EOF | |
input.strip | |
end | |
def guess_estimate(guess) | |
guess.zip(@secret).map { |(value, secret)| [value, value <=> secret] } | |
end | |
def correct?(guess) | |
@secret == guess | |
end | |
def current_player | |
@turn_order.first | |
end | |
end | |
class Options | |
attr_accessor :dimensions, :size, :turns, :first_turn, :strategy | |
DIMENSIONS = 2 | |
SIZE = 16 | |
TURNS = 7 | |
FIRST_TURN = :player | |
STRATEGY = :optimal | |
TURNS_CHOICE = %i(player enemy).freeze | |
STRATEGIES = %i(optimal random).freeze | |
def initialize | |
@dimensions = DIMENSIONS | |
@size = SIZE | |
@turns = TURNS | |
@first_turn = FIRST_TURN | |
@strategy = STRATEGY | |
end | |
class << self | |
def parse(args) | |
opts = new | |
OptionParser.new do |p| | |
p.banner = <<~BANNER | |
Hurkle v#{VERSION} | |
Usage: hurkle [options] | |
BANNER | |
p.separator 'Options:' | |
p.on('-d', '--dimensions NUM', Integer, | |
"Number of dimensions (default #{DIMENSIONS})") do |dimensions| | |
assert_positive dimensions | |
opts.dimensions = dimensions | |
end | |
p.on('-s', '--size NUM', Integer, | |
"Size of each dimension (default #{SIZE})") do |size| | |
assert_positive size | |
opts.size = size | |
end | |
p.on('-t', '--turns NUM', Integer, | |
"Number of turns (default #{TURNS})") do |turns| | |
assert_positive turns | |
opts.turns = turns | |
end | |
p.on('-f', "--first <#{TURNS_CHOICE.join('|')}>", TURNS_CHOICE, | |
"Select whose turn is first (default #{FIRST_TURN})") do |first_turn| | |
opts.first_turn = first_turn | |
end | |
p.on("--strategy <#{STRATEGIES.join('|')}>", STRATEGIES, | |
"Enemy's strategy (default #{STRATEGY})") do |strategy| | |
opts.strategy = strategy | |
end | |
p.separator '' | |
p.on_tail('-h', '--help', 'Display this message') do | |
puts p | |
exit | |
end | |
p.on_tail('-v', '--version', 'Display version') do | |
puts "Hurkle v#{VERSION}" | |
exit | |
end | |
end.parse!(args) | |
opts | |
end | |
private | |
def assert_positive(number) | |
raise OptionParser::InvalidArgument, 'is not positive' unless number.positive? | |
end | |
end | |
end | |
module Strategy | |
class Optimal | |
def initialize(dimensions, size) | |
@estimate = Array.new(dimensions) { [0, size] } | |
end | |
def guess | |
@estimate.map { |(lower, upper)| (lower + upper) / 2 } | |
end | |
def update(estimate) | |
@estimate = @estimate.zip(estimate).map do |(lower, upper), (value, cmp)| | |
case cmp | |
when -1 then [[value, lower].max, upper] | |
when +1 then [lower, [value, upper].min] | |
else [value, value] | |
end | |
end | |
end | |
end | |
class Random | |
def initialize(dimensions, size) | |
@dimensions = dimensions | |
@size = size | |
end | |
def guess | |
Array.new(@dimensions) { rand(@size) } | |
end | |
def update(_); end | |
end | |
module_function | |
def get(strategy) | |
case strategy | |
when :optimal then Optimal | |
when :random then Random | |
else raise ArgumentError, "Unkown strategy: #{strategy}" | |
end | |
end | |
end | |
end | |
def main(args) | |
options = Hurkle::Options.parse(args) | |
game = Hurkle::Game.new(options) | |
game.run | |
rescue Interrupt | |
abort("\nBye!") | |
rescue OptionParser::ParseError => e | |
abort(e.message) | |
end | |
main(ARGV) if __FILE__ == $PROGRAM_NAME |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment