elus 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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