elus 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,5 @@
1
+ require 'elus/game'
2
+ require 'elus/generator'
3
+ require 'elus/piece'
4
+ require 'elus/rule'
5
+ require 'elus/solver'
@@ -0,0 +1,74 @@
1
+ module Elus
2
+ class Game
3
+ def initialize(options = {})
4
+ rules_generator = options[:generator]
5
+ @board = options[:board]
6
+ @free = options[:free]
7
+ raise Elus::Invalid, "Wrong Board or Free set: #{@board}, #{@free}" unless [@board, @free].all? {|set| Array === set} # Check if all pieces are correct
8
+ raise Elus::Invalid, "Wrong Game Pieces: #{@board}, #{@free}" unless (@board + @free).all? {|piece| Piece === piece} # Check if all pieces are correct
9
+ raise Elus::Invalid, "Wrong number of Game Pieces: #{@board}, #{@free}" unless @board.size >= 3 and @free.size == 3 # Check for correct Board/Free size
10
+ raise Elus::Invalid, "Wrong Rules generator" unless rules_generator.respond_to? :generate_rules
11
+
12
+ @rules = rules_generator.generate_rules
13
+ test_rules
14
+ count_moves
15
+ end
16
+
17
+ # Count Moves
18
+ def count_moves
19
+ @moves = @free.map do |piece|
20
+ count = @rules.count {|rule| piece == rule.apply(@board.last)}
21
+ count > 0 ? "#{piece.name}(#{count})" : nil
22
+ end.compact
23
+ end
24
+
25
+ def move(piece, new_free=nil)
26
+ if new_free # The move was right!
27
+ @board << piece
28
+ @free = new_free
29
+ else
30
+ @free.delete(piece)
31
+ end
32
+ test_rules
33
+ count_moves
34
+ end
35
+
36
+ def state
37
+ "Free:\n" + @free.join("\n") + "\nBoard:\n" + @board.join("\n") + "\n"
38
+ end
39
+
40
+ def hint
41
+ "Rules(#{@rules.size}):\n" + @rules.map(&:name).join("\n") +"\n" +
42
+ "Moves(#{@moves.size}):\n" + @moves.join("\n") + "\n"
43
+ end
44
+
45
+ def finished?
46
+ @board.size>=8
47
+ end
48
+
49
+ def valid_move? piece
50
+ @free.include? piece
51
+ end
52
+ private
53
+
54
+ # Tests rules against current Game state, drops inconsistent rules
55
+ def test_rules
56
+ # Drop rule if not consistent with the Board sequence
57
+ @rules.delete_if do |rule|
58
+ @board.each_cons(2).to_a.inject(false) do |delete, pieces|
59
+ delete or rule.apply(pieces.first)!=pieces.last
60
+ end
61
+ end
62
+
63
+ # Drop rule if applied to last Board Piece does not have single match among Free Pieces
64
+ @rules.delete_if do |rule|
65
+ @free.count {|piece| piece == rule.apply(@board.last)} != 1
66
+ end
67
+ end
68
+
69
+ end
70
+
71
+ # An exception of this class indicates invalid input
72
+ class Invalid < StandardError
73
+ end
74
+ end
@@ -0,0 +1,48 @@
1
+ module Elus
2
+ # Rules Generators
3
+ class Generator
4
+ def generate_rules
5
+ []
6
+ end
7
+
8
+ # iterates through all permutations of string chars with 2 dots
9
+ def permutate(string='01!=')
10
+ string.split(//).product(['.'],['.']).map {|set| set.permutation.to_a.uniq}.flatten(1).map {|chars| chars.join}
11
+ end
12
+ end
13
+
14
+ class EmptyGenerator < Generator
15
+ end
16
+
17
+ class Turn1Generator < Generator
18
+ def generate_rules
19
+ yes_branches = permutate
20
+ ['...'].product(yes_branches).map do |condition, yes|
21
+ Rule.new(Piece.create(condition), Piece.create(yes))
22
+ end
23
+ end
24
+ end
25
+
26
+ class Turn2Generator < Generator
27
+ def generate_rules
28
+ conditions = permutate('1')
29
+ branches = permutate.map {|code| [code, Piece.different(code)]}
30
+ conditions.product(branches.uniq).map do |condition, yes_no|
31
+ yes,no = yes_no
32
+ Rule.new(Piece.create(condition), Piece.create(yes), Piece.create(no))
33
+ end
34
+ end
35
+ end
36
+
37
+ class Turn3Generator < Generator
38
+ def generate_rules
39
+ conditions = permutate('1')
40
+ yes_branches = permutate
41
+ no_branches = permutate
42
+ conditions.product(yes_branches, no_branches).map do|condition, yes, no|
43
+ Rule.new(Piece.create(condition), Piece.create(yes), Piece.create(no))
44
+ end
45
+ end
46
+ end
47
+
48
+ end
@@ -0,0 +1,93 @@
1
+ module Elus
2
+ class Piece
3
+ # These constants are used for translating between the external string representation of a Game and the internal representation.
4
+ VALID = "01sbgycdr=!." # Valid (meaningful) chars in code string (downcase)
5
+ SORT = "01stghcdd=!." # Special unique chars good for sorting (B)ig->(T)itanic, (Y)ellow->(H)ellow, (R)hombus->(D)iamond
6
+ FINAL = "010101011=!." # Final (place-dependent) representation as 0/1 digits
7
+ INVALID = Regexp.new "[^#{VALID}]" # Regexp matching all non-valid chars
8
+ PATTERN = /^[01st=!.][01gh=!.][01cd=!.]$/ # Correct code pattern for Piece creation
9
+ # Names for Piece characteristics (based on code)
10
+ NAMES = [ {'0'=>'Small', '1'=>'Big', '='=>'Same size', '!'=>'Different size'},
11
+ {'0'=>'Green', '1'=>'Yellow', '='=>'Same color', '!'=>'Different color'},
12
+ {'0'=>'Circle', '1'=>'Diamond', '='=>'Same shape', '!'=>'Different shape'} ]
13
+
14
+ attr_reader :code
15
+ private_class_method :new
16
+
17
+ # Factory method: takes an input string and tries to convert it into valid Piece code. Returns new Piece if successful, otherwise nil
18
+ def Piece.create(input)
19
+ return nil unless String === input
20
+ if input_code = convert_code(input) then new(input_code) else nil end
21
+ end
22
+
23
+ # Pre-processes string into valid code for Piece creation
24
+ def Piece.convert_code(input)
25
+ # Remove all invalid chars from input and transcode it into sort chars
26
+ input_code = input.downcase.gsub(INVALID, '').tr(VALID, SORT)
27
+ # Remove dupes and sort unless code contains digits or special chars (place-dependent)
28
+ input_code = input_code.scan(/\w/).uniq.sort.reverse.join unless input_code =~ /[01!=.]/
29
+ # Translate sort chars into final chars
30
+ input_code.tr!(SORT, FINAL) if input_code =~ PATTERN
31
+ end
32
+
33
+ # Finds different (opposite, complimentary) code character
34
+ def Piece.different(string)
35
+ string.tr('01!=.', '10=!.')
36
+ end
37
+
38
+ # Returns Any Piece (should be a singleton object)
39
+ def Piece.any
40
+ @@any ||= Piece.create('...')
41
+ end
42
+
43
+ # Assumes valid code (should be pre-filtered by Piece.create)
44
+ def initialize(input_code)
45
+ @code = input_code
46
+ end
47
+
48
+ # Returns full text name of this Piece
49
+ def name
50
+ (0..2).map {|i| NAMES[i][@code[i]]}.compact.join(' ').
51
+ gsub(Regexp.new('^$'), 'Any').
52
+ gsub(Regexp.new('Same size Same color Same shape'), 'All Same').
53
+ gsub(Regexp.new('Different size Different color Different shape'), 'All Different')
54
+ end
55
+
56
+ def to_s; name end
57
+
58
+ def * (other)
59
+ new_code = case other
60
+ when nil then nil
61
+ when String then self * Piece.create(other)
62
+ when Piece then
63
+ (0..2).map do |i|
64
+ case other.code[i]
65
+ when '=' then code[i]
66
+ when '!' then Piece.different code[i]
67
+ else other.code[i]
68
+ end
69
+ end.join
70
+ else raise(ArgumentError, 'Piece compared with a wrong type')
71
+ end
72
+ Piece.create(new_code)
73
+ end
74
+
75
+ include Comparable
76
+
77
+ def <=> (other)
78
+ case other
79
+ when nil then 1
80
+ when String then self <=> Piece.create(other)
81
+ when Piece then
82
+ return 0 if code == other.code
83
+ return 0 if code =~ Regexp.new(other.code)
84
+ return 0 if other.code =~ Regexp.new(code)
85
+ else raise(ArgumentError, 'Piece compared with a wrong type')
86
+ end
87
+ end
88
+
89
+ def eql? (other)
90
+ (Piece === other) ? @code.eql?(other.code) : false
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,36 @@
1
+ module Elus
2
+ class Rule
3
+ def initialize(condition, yes, no=nil)
4
+ @condition = Piece === condition ? condition : Piece.create(condition)
5
+ raise Invalid, "Wrong condition" unless @condition
6
+ @yes = Piece === yes ? yes : Piece.create(yes)
7
+ raise Invalid, "Wrong yes branch" unless @yes
8
+ @no = if Piece === no
9
+ no
10
+ elsif Piece.create(no)
11
+ Piece.create(no)
12
+ elsif no
13
+ raise( Invalid, "Wrong no branch")
14
+ else
15
+ nil
16
+ end
17
+ end
18
+
19
+ # Returns full text name of this Rule
20
+ def name
21
+ "If last Piece is #{@condition.name} Piece, #{@yes.name} Piece is next" + if @no then ", otherwise #{@no.name} Piece is next" else "" end
22
+ end
23
+
24
+ def to_s; name end
25
+
26
+ def apply(other)
27
+ piece = Piece === other ? other : Piece.create(other)
28
+ return nil unless piece
29
+ if piece == @condition
30
+ piece * @yes
31
+ else
32
+ piece * @no
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,84 @@
1
+ module Elus
2
+ class Solver
3
+ attr_reader :game
4
+
5
+ def initialize(stdin, stdout)
6
+ @stdin = stdin
7
+ @stdout = stdout
8
+ end
9
+
10
+ def start (generator)
11
+ @generator = generator
12
+ @stdout.puts "Welcome to Elus Solver!"
13
+ @stdout.puts "Enter Game state:"
14
+ end
15
+
16
+ # Inputs Game state either from given code string or interactively (from @stdin)
17
+ def input_state(codes=nil)
18
+ if codes
19
+ pieces = codes.split("\n").map {|code| Piece.create(code)}.compact
20
+ free=pieces[0..2]
21
+ board=pieces[3..-1]
22
+ else
23
+ free = input_pieces "Free", 3
24
+ board = input_pieces "Board"
25
+ end
26
+ @game = Game.new(:free=>free, :board=>board, :generator=>@generator)
27
+ end
28
+
29
+ def input_pieces(label, num=10)
30
+ pieces = []
31
+ while pieces.size < num and piece = input_piece(:prompt => "Enter #{label} Piece code (#{pieces.size+1}):") do
32
+ @stdout.puts "You entered #{label} Piece (#{pieces.size+1}): #{piece.name}"
33
+ pieces << piece
34
+ end
35
+ pieces
36
+ end
37
+
38
+ # Inputs single correct code from stdin, returns correct piece or nil if break requested. Rejects wrong codes.
39
+ def input_piece(options = {})
40
+ loop do
41
+ @stdout.puts options[:prompt] || "Enter code:"
42
+ code = @stdin.gets
43
+ return nil if code == "\n"
44
+ if piece = Piece.create(code)
45
+ return piece
46
+ else
47
+ @stdout.puts options[:failure] || "Invalid code: #{code}"
48
+ end
49
+ end
50
+ end
51
+
52
+ def make_moves
53
+ while not @game.finished?
54
+ make_move
55
+ end
56
+ end
57
+
58
+ def make_move
59
+ @stdout.puts @game.state
60
+ @stdout.puts @game.hint
61
+ piece = input_piece(:prompt => "Make your move:")
62
+ if piece and @game.valid_move? piece
63
+ @stdout.puts "You moved: #{piece.name}"
64
+ @stdout.puts "Was the move right(Y/N)?:"
65
+ if @stdin.gets =~ /[Yy]/
66
+ @stdout.puts "Great, now enter new Free set:"
67
+ free = input_pieces "Free", 3
68
+ @game.move piece, free
69
+ else
70
+ @stdout.puts "Too bad"
71
+ @game.move piece
72
+ end
73
+ elsif piece
74
+ @stdout.puts "Wrong move (not in free set): #{piece.name}"
75
+ else
76
+ @stdout.puts "Wrong move (no piece given)"
77
+ end
78
+ end
79
+
80
+ def state; @game.state end
81
+ def hint; @game.hint end
82
+
83
+ end #Solver
84
+ end
@@ -0,0 +1,13 @@
1
+ require File.join(File.dirname(__FILE__), ".." ,"spec_helper" )
2
+ require File.join(File.dirname(__FILE__), ".." ,".." ,"features" ,"support" ,"stats" )
3
+
4
+ module ElusTest
5
+ # describe Stats do
6
+ # it 'ignores messages that do not contain relevant data' do
7
+ # stats = Stats.new ['BYD', 'BYD', 'BYD'], ElusTest::CODES
8
+ # stats.puts "Small Yellow Diamond"
9
+ # stats.count_for('BYD').should == 0
10
+ # stats.count_for('SYD').should == nil
11
+ # end
12
+ # end
13
+ end
@@ -0,0 +1,116 @@
1
+ require File.join(File.dirname(__FILE__), ".." ,"spec_helper" )
2
+
3
+ module Elus
4
+ include ElusTest
5
+
6
+ def generator_stub
7
+ stub('generator', :generate_rules => [])
8
+ end
9
+
10
+ def should_be_in(string, *messages)
11
+ messages.each do |msg|
12
+ string.split("\n").should include(msg)
13
+ end
14
+ end
15
+
16
+ describe Game do
17
+ before :each do
18
+ @free = [ Piece.create("BGC"),
19
+ Piece.create("sgd"),
20
+ Piece.create("syc") ]
21
+ @board = [Piece.create("BYD"),
22
+ Piece.create("SYD"),
23
+ Piece.create("BGD") ]
24
+ @pieces = @free + @board
25
+ @new = [ Piece.create("BYC"),
26
+ Piece.create("BYD"),
27
+ Piece.create("SGC") ]
28
+ end
29
+
30
+ context "creating" do
31
+ it "should raise exception if not enough Pieces" do
32
+ lambda{Game.new(:generator=>generator_stub)}.should raise_error(Invalid)
33
+ lambda{Game.new(:free=>@free, :generator=>generator_stub)}.should raise_error(Invalid)
34
+ lambda{Game.new(:board=>@board,:generator=>generator_stub)}.should raise_error(Invalid)
35
+ (0..2).each {|n| lambda{Game.new(:free=>@pieces[0,n], :board=>@board, :generator=>@wrong_generator)}.should raise_error(Invalid)}
36
+ (0..2).each {|n| lambda{Game.new(:free=>@free, :board=>@pieces[3,n], :generator=>@wrong_generator)}.should raise_error(Invalid)}
37
+ end
38
+
39
+ it "should raise exception if it is given wrong generator_stub" do
40
+ lambda{Game.new(:free=>@free, :board=>@board, :generator=>@wrong_generator)}.should raise_error(Invalid)
41
+ end
42
+
43
+ it "should contain Free, Board and matching Piece name in Game state" do
44
+ CODES.each do |code, name|
45
+ code1, name1 = CODES.to_a[rand(CODES.size-1)]
46
+ free = @pieces[0..1] << Piece.create(code)
47
+ board = @pieces[2..3] << Piece.create(code1)
48
+ game = Game.new(:free=>free, :board=>board, :generator=>generator_stub)
49
+ should_be_in game.state, name, name1, "Free:", "Board:"
50
+ end
51
+ end
52
+ end
53
+
54
+ context 'generating hints' do
55
+ it "should generate appropriate hints about Rules" do
56
+ game = Game.new(:free=>@free, :board=>@board, :generator=>Turn1Generator.new)
57
+ should_be_in game.hint, 'Rules(2):',
58
+ 'If last Piece is Any Piece, Diamond Piece is next',
59
+ 'If last Piece is Any Piece, Same shape Piece is next'
60
+ end
61
+ it "should generate appropriate hints about Moves" do
62
+ game = Game.new(:free=>@free, :board=>@board, :generator=>Turn1Generator.new)
63
+ should_be_in game.hint, 'Moves(1):',
64
+ 'Small Green Diamond(2)'
65
+ end
66
+ end
67
+ context 'making moves' do
68
+ it 'should add moved piece to board upon right move' do
69
+ game = Game.new(:free=>@free, :board=>@board, :generator=>Turn1Generator.new)
70
+ piece = Piece.create('SGD')
71
+ game.move(piece, @new)
72
+ game.state.should =~ Regexp.new("Board:[\\w\\s]*#{piece.name}")
73
+ end
74
+ it 'should reset free and moves sets upon right move' do
75
+ game = Game.new(:free=>@free, :board=>@board, :generator=>Turn1Generator.new)
76
+ piece = Piece.create('SGD')
77
+ game.move(piece, @new)
78
+ game.state.should =~ Regexp.new('Free:\s'+@new.map(&:name).join('\s'))
79
+ game.hint.should =~ /Moves\(1\):\sBig Yellow Diamond/
80
+ end
81
+ it 'should not add moved piece to board upon wrong move' do
82
+ game = Game.new(:free=>@free, :board=>@board, :generator=>Turn1Generator.new)
83
+ piece = Piece.create('BGC')
84
+ game.move(piece)
85
+ game.state.should_not =~ Regexp.new("Board:[\\w\\s]*#{piece.name}")
86
+ end
87
+ it 'should remove moved piece from free set upon wrong move' do
88
+ game = Game.new(:free=>@free, :board=>@board, :generator=>Turn1Generator.new)
89
+ piece = Piece.create('BGC')
90
+ game.move(piece)
91
+ game.state.should_not =~ Regexp.new('Free:[\s.]*'+piece.name+'[\s.]Board')
92
+ game.hint.should =~ /Moves\(1\):\sSmall Green Diamond/
93
+ end
94
+ end
95
+ context 'predicate testing' do
96
+ it 'should be finished if more than 8 pieces on the board' do
97
+ game = Game.new(:free=>@free, :board=>@board+@board+@board, :generator=>Turn1Generator.new)
98
+ game.should be_finished
99
+ end
100
+ it 'should consider any free piece a valid move' do
101
+ ['BGC', 'SGD', 'SYC'].each do |code|
102
+ game = Game.new(:free=>@free, :board=>@board, :generator=>Turn1Generator.new)
103
+ piece = Piece.create(code)
104
+ game.should be_valid_move(piece)
105
+ end
106
+ end
107
+ it 'should not consider any non-free piece a valid move' do
108
+ CODES.each do |code, name|
109
+ game = Game.new(:free=>@free, :board=>@board, :generator=>Turn1Generator.new)
110
+ piece = Piece.create(code)
111
+ game.should_not be_valid_move(piece) unless @free.include? piece
112
+ end
113
+ end
114
+ end
115
+ end
116
+ end