Skip to content

Instantly share code, notes, and snippets.

@smaximov
Created September 7, 2016 17:33
Show Gist options
  • Save smaximov/49958538c02ab0a811954817d6e0b15d to your computer and use it in GitHub Desktop.
Save smaximov/49958538c02ab0a811954817d6e0b15d to your computer and use it in GitHub Desktop.
#!/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