karel 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/README.rdoc +259 -0
- data/bin/karel +138 -0
- data/ext/fixnum_TIMES.rb +5 -0
- data/karel.rdoc +44 -0
- data/lib/karel.rb +12 -0
- data/lib/karel/commands.rb +152 -0
- data/lib/karel/exceptions.rb +15 -0
- data/lib/karel/karel.rb +67 -0
- data/lib/karel/square.rb +42 -0
- data/lib/karel/syntax_checker.rb +86 -0
- data/lib/karel/world.rb +140 -0
- metadata +104 -0
data/README.rdoc
ADDED
@@ -0,0 +1,259 @@
|
|
1
|
+
= Karel the Robot in Ruby
|
2
|
+
|
3
|
+
Author:: Dave Copeland (mailto:davetron5000 at g mail dot com)
|
4
|
+
Copyright:: Copyright (c) 2010 by Dave Copeland
|
5
|
+
License:: Distributes under the Apache License, see LICENSE.txt in the source distro
|
6
|
+
|
7
|
+
== What is this?
|
8
|
+
|
9
|
+
This is a ruby implementation of Karel The Robot[http://www.cs.mtsu.edu/~untch/karel/], a
|
10
|
+
programming language designed for extreme beginners. It concerns controlling
|
11
|
+
a robot, named Karel, in a grid-based world comprised of walls and beepers. Karel can pick up and put down beepers, move
|
12
|
+
forward, and turn left. Karel can also detect things about his environment.
|
13
|
+
|
14
|
+
== Install
|
15
|
+
|
16
|
+
For the time being, there's no gem so just clone the repo.
|
17
|
+
|
18
|
+
git clone https://davetron5000@github.com/davetron5000/rkarel.git
|
19
|
+
cd rkarel
|
20
|
+
bin/karel init example.karel
|
21
|
+
bin/karel run example.karel
|
22
|
+
bin/karel run -g example.karel
|
23
|
+
|
24
|
+
== Usage
|
25
|
+
|
26
|
+
While the karel language doesn't have any concept of the world, you must still define one in which your program will run. This is done
|
27
|
+
at the top of the program via:
|
28
|
+
|
29
|
+
WORLD <<END
|
30
|
+
W B
|
31
|
+
W
|
32
|
+
WWW
|
33
|
+
B
|
34
|
+
K
|
35
|
+
END
|
36
|
+
|
37
|
+
This defines Karel's world to be 5x9 with walls represented by W's, a beeper represented by B's, and Karel's
|
38
|
+
initial location represented as a "K". Karel is assumed to be facing north with no beepers at the start of all programs.
|
39
|
+
|
40
|
+
You then can define subroutines as such:
|
41
|
+
|
42
|
+
DEFINE('TURNRIGHT') {
|
43
|
+
TURNLEFT()
|
44
|
+
TURNLEFT()
|
45
|
+
TURNLEFT()
|
46
|
+
}
|
47
|
+
|
48
|
+
You can then call this subroutine as you would any other
|
49
|
+
|
50
|
+
MOVE()
|
51
|
+
TURNRIGHT()
|
52
|
+
MOVE()
|
53
|
+
MOVE()
|
54
|
+
|
55
|
+
Once you've defined your subroutines, you can begin writing your program:
|
56
|
+
|
57
|
+
MOVE()
|
58
|
+
TURNLEFT()
|
59
|
+
ITERATE(3.TIMES) {
|
60
|
+
MOVE
|
61
|
+
}
|
62
|
+
IF(front_clear) {
|
63
|
+
MOVE()
|
64
|
+
}
|
65
|
+
ELSE {
|
66
|
+
PUTBEEPER()
|
67
|
+
}
|
68
|
+
TURNLEFT()
|
69
|
+
WHILE(not_on_beeper) {
|
70
|
+
MOVE
|
71
|
+
}
|
72
|
+
PICKBEEPER()
|
73
|
+
|
74
|
+
=== Commands
|
75
|
+
|
76
|
+
[+MOVE+] Move Karel forward one square. If Karel can't, he explodes and the program aborts
|
77
|
+
[+TURNLEFT+] Rotate Karel, in place, to the left
|
78
|
+
[+PICKBEEPER+] Pick up the beeper at Karel's position. If there is no beeper, Karel explodes and the problem aborts
|
79
|
+
[+PUTBEEPER+] Put down a beeper at Karel's position. If there is a beeper or if Karel has no beepers, Karel explodes and the problem aborts
|
80
|
+
|
81
|
+
=== Control Flow
|
82
|
+
|
83
|
+
[+IF+] takes a condition and a curly-braced block to perform if the condition holds
|
84
|
+
[+ELSE+] when after an IF, executes the curly-braced block if the condition didn't hold
|
85
|
+
[<code>ITERATE(<i>N</i>.TIMES)</code>] perform something a constant number of times
|
86
|
+
[+WHILE+] takes a condition and a curly-braced block and repeatedly performs the block's statements until the condition holds
|
87
|
+
|
88
|
+
==== Conditions
|
89
|
+
|
90
|
+
[+on_beeper+] true if Karel is on the same square as a beeper
|
91
|
+
[+not_on_beeper+] true if Karel is not on the same square as a beeper
|
92
|
+
[+front_clear+] true if the square in front of Karel is clear
|
93
|
+
[+front_not_clear+] true if the square in front of Karel is not clear
|
94
|
+
[+left_clear+] true if the square to the left of Karel is clear
|
95
|
+
[+left_not_clear+] true if the square to the left of Karel is not clear
|
96
|
+
[+right_clear+] true if the square to the left of Karel is clear
|
97
|
+
[+right_not_clear+] true if the square to the left of Karel is not clear
|
98
|
+
[+facing_north+] true if Karel is facing north
|
99
|
+
[+not_facing_north+] true if Karel is not facing north
|
100
|
+
[+facing_south+] true if Karel is facing south
|
101
|
+
[+not_facing_south+] true if Karel is not facing south
|
102
|
+
[+facing_east+] true if Karel is facing east
|
103
|
+
[+not_facing_east+] true if Karel is not facing east
|
104
|
+
[+facing_west+] true if Karel is facing west
|
105
|
+
[+not_facing_west+] true if Karel is not facing west
|
106
|
+
|
107
|
+
=== Subroutines
|
108
|
+
|
109
|
+
You can define your own subroutines via the +DEFINE+ directive:
|
110
|
+
|
111
|
+
DEFINE('RUN_TO_WALL') {
|
112
|
+
WHILE(front_clear) {
|
113
|
+
MOVE()
|
114
|
+
}
|
115
|
+
}
|
116
|
+
RUN_TO_WALL()
|
117
|
+
|
118
|
+
|
119
|
+
=== Whitespace and comments
|
120
|
+
|
121
|
+
Whitespace is not significant and is ignored. Comments are lines starting with optional whitespace then followed by a +#+. The remainder
|
122
|
+
of the line is ignored.
|
123
|
+
|
124
|
+
== Example
|
125
|
+
|
126
|
+
Here's an example program that has Karel grabbing both beepers and stopping on the last one (the one in the upper-left corner):
|
127
|
+
|
128
|
+
WORLD <<END
|
129
|
+
B WW B
|
130
|
+
W
|
131
|
+
W
|
132
|
+
|
133
|
+
WWW K
|
134
|
+
END
|
135
|
+
|
136
|
+
WHILE(front_clear) {
|
137
|
+
MOVE()
|
138
|
+
}
|
139
|
+
TURNLEFT()
|
140
|
+
MOVE()
|
141
|
+
PICKBEEPER()
|
142
|
+
TURNLEFT()
|
143
|
+
# Karel is now facing south
|
144
|
+
WHILE(front_clear) {
|
145
|
+
MOVE()
|
146
|
+
}
|
147
|
+
TURNLEFT()
|
148
|
+
TURNLEFT()
|
149
|
+
MOVE()
|
150
|
+
TURNLEFT()
|
151
|
+
WHILE(front_clear) {
|
152
|
+
MOVE()
|
153
|
+
}
|
154
|
+
TURNLEFT()
|
155
|
+
TURNLEFT()
|
156
|
+
TURNLEFT()
|
157
|
+
WHILE(not_on_beeper) {
|
158
|
+
MOVE()
|
159
|
+
}
|
160
|
+
PICKBEEPER()
|
161
|
+
|
162
|
+
We can reduce the line count and make it more readable with some subroutines:
|
163
|
+
|
164
|
+
DEFINE('TURNRIGHT') {
|
165
|
+
ITERATE(3.TIMES) {
|
166
|
+
TURNLEFT()
|
167
|
+
}
|
168
|
+
}
|
169
|
+
DEFINE('TURNAROUND') {
|
170
|
+
TURNLEFT()
|
171
|
+
TURNLEFT()
|
172
|
+
}
|
173
|
+
DEFINE('RUN') {
|
174
|
+
WHILE(front_clear) {
|
175
|
+
MOVE()
|
176
|
+
}
|
177
|
+
}
|
178
|
+
DEFINE('BACKUP') {
|
179
|
+
TURNAROUND()
|
180
|
+
MOVE()
|
181
|
+
}
|
182
|
+
|
183
|
+
RUN()
|
184
|
+
TURNLEFT()
|
185
|
+
MOVE()
|
186
|
+
PICKBEEPER()
|
187
|
+
TURNLEFT()
|
188
|
+
RUN()
|
189
|
+
BACKUP()
|
190
|
+
TURNLEFT()
|
191
|
+
RUN()
|
192
|
+
TURNRIGHT()
|
193
|
+
WHILE(not_on_beeper) {
|
194
|
+
MOVE()
|
195
|
+
}
|
196
|
+
PICKBEEPER()
|
197
|
+
|
198
|
+
== Non-DSL Mode
|
199
|
+
|
200
|
+
If you wish to use this code as an engine to embed inside another application, the DSL mode is quite inconvienient. In this case, you can create an object and call the DSL methods on it.
|
201
|
+
|
202
|
+
engine = Engine.new
|
203
|
+
engine.WORLD <<END
|
204
|
+
B WW B
|
205
|
+
WW W
|
206
|
+
W WW
|
207
|
+
K W
|
208
|
+
END
|
209
|
+
|
210
|
+
Need to think this out more; what is possible and what isn't?
|
211
|
+
|
212
|
+
== Vim Syntax File
|
213
|
+
|
214
|
+
In <code>contrib</code> is a vim syntax file for karel source as described here.
|
215
|
+
|
216
|
+
== Command Line Interface
|
217
|
+
|
218
|
+
karel command_name [command-specific options] [--] arguments...
|
219
|
+
|
220
|
+
* Use the command +help+ to get a summary of commands
|
221
|
+
* Use the command <tt>help command_name</tt> to get a help for +command_name+
|
222
|
+
* Use <tt>--</tt> to stop command line argument processing; useful if your arguments have dashes in them
|
223
|
+
|
224
|
+
== Commands
|
225
|
+
[<tt>check</tt>] Performs a basic syntax check
|
226
|
+
[<tt>help</tt>] Shows list of commands or help for one command
|
227
|
+
[<tt>init</tt>] Creates a new example Karel program
|
228
|
+
[<tt>run</tt>] Executes a Karel program
|
229
|
+
|
230
|
+
=== <tt>check source_file</tt>
|
231
|
+
|
232
|
+
Performs a basic syntax check
|
233
|
+
|
234
|
+
=== <tt>help [command]</tt>
|
235
|
+
|
236
|
+
Shows list of commands or help for one command
|
237
|
+
|
238
|
+
=== <tt>init source_file</tt>
|
239
|
+
|
240
|
+
Creates a new example Karel program
|
241
|
+
|
242
|
+
|
243
|
+
|
244
|
+
==== Options
|
245
|
+
These options are specified *after* the command.
|
246
|
+
|
247
|
+
[<tt>-f, --force</tt>] Overwite files
|
248
|
+
=== <tt>run source_file</tt>
|
249
|
+
|
250
|
+
Executes a Karel program
|
251
|
+
|
252
|
+
|
253
|
+
|
254
|
+
==== Options
|
255
|
+
These options are specified *after* the command.
|
256
|
+
|
257
|
+
[<tt>-g, --debug</tt>] Show the world after each command is executed
|
258
|
+
[<tt>-s</tt>] Run silently; do not show the world before or after
|
259
|
+
[<tt>-x arg</tt>] Number of "ticks" to allow before assuming we are in an infinite loop <i>( default: <tt>1000</tt>)</i>
|
data/bin/karel
ADDED
@@ -0,0 +1,138 @@
|
|
1
|
+
#!/usr/bin/ruby
|
2
|
+
$: << File.expand_path(File.dirname(__FILE__) + '/../lib')
|
3
|
+
$: << File.expand_path(File.dirname(__FILE__) + '/../ext')
|
4
|
+
require 'rubygems'
|
5
|
+
require 'gli'
|
6
|
+
require 'karel'
|
7
|
+
require 'karel/syntax_checker'
|
8
|
+
|
9
|
+
include GLI
|
10
|
+
|
11
|
+
desc 'Executes a Karel program'
|
12
|
+
arg_name 'source_file'
|
13
|
+
command :run do |c|
|
14
|
+
|
15
|
+
c.desc 'Run silently; do not show the world before or after'
|
16
|
+
c.long_desc 'This will execute the karel source file, exiting with 0 if the program ran without incident. An exit code of -1 means that Karel exploded, and -2 means that a potential infinite loop was encountered'
|
17
|
+
c.switch :s
|
18
|
+
|
19
|
+
c.desc 'Number of "ticks" to allow before assuming we are in an infinite loop'
|
20
|
+
c.long_desc 'To keep this implementation simple, we will count the number of times we execute a Karel command. If this number exceeds the number here, we will assume a non-terminating program and exit'
|
21
|
+
c.default_value '1000'
|
22
|
+
c.flag [:x]
|
23
|
+
|
24
|
+
c.desc 'Show the world after each command is executed'
|
25
|
+
c.switch [:g,:debug]
|
26
|
+
|
27
|
+
c.action do |global_options,options,args|
|
28
|
+
raise "You must provide a karel source file" if args.empty?
|
29
|
+
include Karel
|
30
|
+
code = File.open(args[0]).readlines.join("")
|
31
|
+
checker = Karel::SyntaxChecker.new
|
32
|
+
unless checker.valid?(code)
|
33
|
+
checker.errors.keys.sort.each do |line_number|
|
34
|
+
$stderr.puts "#{line_number}: #{checker.errors[line_number]}"
|
35
|
+
end
|
36
|
+
raise "There were syntax errors"
|
37
|
+
end
|
38
|
+
DEBUG() if options[:g]
|
39
|
+
SILENT() if options[:s]
|
40
|
+
eval(code)
|
41
|
+
unless options[:s]
|
42
|
+
puts "Final State"
|
43
|
+
puts THE_WORLD.to_s
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
desc 'Performs a basic syntax check'
|
49
|
+
arg_name 'source_file'
|
50
|
+
command :check do |c|
|
51
|
+
c.action do |global_options,options,args|
|
52
|
+
raise "You must provide a karel source file" if args.empty?
|
53
|
+
checker = Karel::SyntaxChecker.new
|
54
|
+
code = File.open(args[0]).readlines.join("")
|
55
|
+
unless checker.valid?(code)
|
56
|
+
checker.errors.keys.sort.each do |line_number|
|
57
|
+
$stderr.puts "#{line_number}: #{checker.errors[line_number]}"
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
desc 'Creates a new example Karel program'
|
64
|
+
arg_name 'source_file'
|
65
|
+
command :init do |c|
|
66
|
+
c.desc 'Overwite files'
|
67
|
+
c.switch [:f,:force]
|
68
|
+
|
69
|
+
c.action do |global_options,options,args|
|
70
|
+
raise "You must provide a karel source file" if args.empty?
|
71
|
+
not_overwritten = []
|
72
|
+
args.each do |filename|
|
73
|
+
if File.exists?(filename) && !options[:f]
|
74
|
+
not_overwritten << filename
|
75
|
+
else
|
76
|
+
File.open(filename,'w') do |file|
|
77
|
+
file.puts <<EOF
|
78
|
+
# A 10x10 world
|
79
|
+
WORLD <<END
|
80
|
+
B WW WB
|
81
|
+
W
|
82
|
+
W
|
83
|
+
W
|
84
|
+
WB
|
85
|
+
WWWW
|
86
|
+
WW
|
87
|
+
W
|
88
|
+
B
|
89
|
+
K
|
90
|
+
END
|
91
|
+
|
92
|
+
# Create a subroutine
|
93
|
+
DEFINE('MOVE_TWICE') {
|
94
|
+
MOVE()
|
95
|
+
MOVE()
|
96
|
+
}
|
97
|
+
|
98
|
+
MOVE()
|
99
|
+
TURNLEFT()
|
100
|
+
TURNLEFT()
|
101
|
+
TURNLEFT()
|
102
|
+
MOVE()
|
103
|
+
PICKBEEPER()
|
104
|
+
MOVE_TWICE()
|
105
|
+
PUTBEEPER()
|
106
|
+
WHILE(front_clear) {
|
107
|
+
MOVE()
|
108
|
+
}
|
109
|
+
EOF
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
113
|
+
not_overwritten.each do |filename|
|
114
|
+
puts "Not overwriting #{filename}; file exists"
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
pre do |global,command,options,args|
|
120
|
+
true
|
121
|
+
end
|
122
|
+
|
123
|
+
post do |global,command,options,args|
|
124
|
+
end
|
125
|
+
|
126
|
+
on_error do |exception|
|
127
|
+
case exception
|
128
|
+
when Karel::Explosion
|
129
|
+
$stderr.puts "Karel Exploded"
|
130
|
+
$stderr.puts THE_WORLD.to_s
|
131
|
+
false
|
132
|
+
else
|
133
|
+
# puts exception.backtrace
|
134
|
+
true
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
GLI.run(ARGV)
|
data/ext/fixnum_TIMES.rb
ADDED
data/karel.rdoc
ADDED
@@ -0,0 +1,44 @@
|
|
1
|
+
= <tt>karel</tt>
|
2
|
+
|
3
|
+
karel command_name [command-specific options] [--] arguments...
|
4
|
+
|
5
|
+
* Use the command +help+ to get a summary of commands
|
6
|
+
* Use the command <tt>help command_name</tt> to get a help for +command_name+
|
7
|
+
* Use <tt>--</tt> to stop command line argument processing; useful if your arguments have dashes in them
|
8
|
+
|
9
|
+
== Commands
|
10
|
+
[<tt>check</tt>] Performs a basic syntax check
|
11
|
+
[<tt>help</tt>] Shows list of commands or help for one command
|
12
|
+
[<tt>init</tt>] Creates a new example Karel program
|
13
|
+
[<tt>run</tt>] Executes a Karel program
|
14
|
+
|
15
|
+
=== <tt>check source_file</tt>
|
16
|
+
|
17
|
+
Performs a basic syntax check
|
18
|
+
|
19
|
+
=== <tt>help [command]</tt>
|
20
|
+
|
21
|
+
Shows list of commands or help for one command
|
22
|
+
|
23
|
+
=== <tt>init source_file</tt>
|
24
|
+
|
25
|
+
Creates a new example Karel program
|
26
|
+
|
27
|
+
|
28
|
+
|
29
|
+
==== Options
|
30
|
+
These options are specified *after* the command.
|
31
|
+
|
32
|
+
[<tt>-f, --force</tt>] Overwite files
|
33
|
+
=== <tt>run source_file</tt>
|
34
|
+
|
35
|
+
Executes a Karel program
|
36
|
+
|
37
|
+
|
38
|
+
|
39
|
+
==== Options
|
40
|
+
These options are specified *after* the command.
|
41
|
+
|
42
|
+
[<tt>-g, --debug</tt>] Show the world after each command is executed
|
43
|
+
[<tt>-s</tt>] Run silently; do not show the world before or after
|
44
|
+
[<tt>-x arg</tt>] Number of "ticks" to allow before assuming we are in an infinite loop <i>( default: <tt>1000</tt>)</i>
|
data/lib/karel.rb
ADDED
@@ -0,0 +1,152 @@
|
|
1
|
+
require 'fixnum_TIMES.rb'
|
2
|
+
module Karel
|
3
|
+
module Commands
|
4
|
+
INFINITE_LOOP_NUM_STEPS = 1000
|
5
|
+
# Create the world with the given initialization string
|
6
|
+
def WORLD(string)
|
7
|
+
@subroutines ||= {}
|
8
|
+
world_instance.create_from_string(string,karel_instance)
|
9
|
+
unless @silent
|
10
|
+
puts "Initial State"
|
11
|
+
puts THE_WORLD.to_s
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def karel_instance; KAREL; end
|
16
|
+
def world_instance; THE_WORLD; end
|
17
|
+
|
18
|
+
def DEBUG
|
19
|
+
@debug = true
|
20
|
+
@silent = false
|
21
|
+
end
|
22
|
+
|
23
|
+
def SILENT
|
24
|
+
@silent = true
|
25
|
+
@debug = false
|
26
|
+
end
|
27
|
+
|
28
|
+
# Moves Karel forward one square
|
29
|
+
def MOVE
|
30
|
+
x,y = Karel.coordinates_after_move_from(karel_instance.direction,*world_instance.karel)
|
31
|
+
world_instance.karel=[x,y]
|
32
|
+
debug_command('MOVE')
|
33
|
+
end
|
34
|
+
|
35
|
+
# Turns karel to the left in place
|
36
|
+
def TURNLEFT
|
37
|
+
karel_instance.turnleft
|
38
|
+
debug_command('TURNLEFT')
|
39
|
+
end
|
40
|
+
|
41
|
+
def PICKBEEPER
|
42
|
+
karel = world_instance.karel
|
43
|
+
begin
|
44
|
+
world_instance.remove_beeper(*karel)
|
45
|
+
karel_instance.put_beeper_in_bag
|
46
|
+
rescue NoBeeper => x
|
47
|
+
raise Explosion
|
48
|
+
end
|
49
|
+
debug_command('PICKBEEPER')
|
50
|
+
end
|
51
|
+
|
52
|
+
def PUTBEEPER
|
53
|
+
karel = world_instance.karel
|
54
|
+
karel_instance.remove_beeper_from_bag
|
55
|
+
world_instance.add_beeper(*karel)
|
56
|
+
debug_command('PUTBEEPER')
|
57
|
+
end
|
58
|
+
|
59
|
+
def DEFINE(name,&block)
|
60
|
+
raise BadSubroutine unless subroutine_name_ok?(name)
|
61
|
+
@subroutines ||= {}
|
62
|
+
@subroutines[name.to_sym] = block;
|
63
|
+
end
|
64
|
+
|
65
|
+
# Handles calling subroutines defined
|
66
|
+
def method_missing(sym,*args)
|
67
|
+
if !args || args.size == 0
|
68
|
+
if @subroutines[sym]
|
69
|
+
puts "CALLING #{sym}" if @debug
|
70
|
+
@subroutines[sym].call
|
71
|
+
else
|
72
|
+
super.method_missing(sym,args)
|
73
|
+
end
|
74
|
+
else
|
75
|
+
super.method_missing(sym,args)
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
def ITERATE(num,&block)
|
80
|
+
num.times { block.call }
|
81
|
+
end
|
82
|
+
|
83
|
+
def WHILE(condition,&block)
|
84
|
+
steps = 0
|
85
|
+
while (condition_met? condition)
|
86
|
+
block.call
|
87
|
+
steps += 1
|
88
|
+
raise PossibleInfiniteLoop if steps > INFINITE_LOOP_NUM_STEPS
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
@last_condition = nil
|
93
|
+
def IF(condition,&block)
|
94
|
+
if condition_met? condition
|
95
|
+
block.call
|
96
|
+
end
|
97
|
+
@last_condition = condition
|
98
|
+
end
|
99
|
+
|
100
|
+
def ELSE(&block)
|
101
|
+
raise "No IF with this ELSE!" if @last_condition.nil?
|
102
|
+
condition = @last_condition
|
103
|
+
@last_condition = nil
|
104
|
+
unless (condition_met? condition)
|
105
|
+
block.call
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
def condition_met?(condition)
|
110
|
+
raise "No such condition #{condition}" unless CONDITIONS[condition]
|
111
|
+
CONDITIONS[condition].call(world_instance,karel_instance,*world_instance.karel)
|
112
|
+
end
|
113
|
+
|
114
|
+
def subroutine_name_ok?(name)
|
115
|
+
name =~ /^[A-Z][A-Z_0-9]*[A-Z]$/
|
116
|
+
end
|
117
|
+
|
118
|
+
def debug_command(command)
|
119
|
+
if @debug
|
120
|
+
puts "#{karel_instance.num_beepers} beepers> #{command}"
|
121
|
+
puts world_instance.to_s
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
CONDITIONS = {
|
126
|
+
:on_beeper => lambda{ |world,karel,row,column| world.clear?(row,column) && world[row,column].beeper? },
|
127
|
+
:front_clear => lambda{ |world,karel,row,column| world.clear?(*Karel.coordinates_after_move_from(karel.direction,row,column)) },
|
128
|
+
:left_clear => lambda{ |world,karel,row,column| world.clear?(*Karel.coordinates_after_move_from(Karel.left_of(karel.direction),row,column)) },
|
129
|
+
:right_clear => lambda{ |world,karel,row,column| world.clear?(*Karel.coordinates_after_move_from(Karel.right_of(karel.direction),row,column)) },
|
130
|
+
:facing_north => lambda{ |world,karel,row,column| karel.direction == :north },
|
131
|
+
:facing_south => lambda{ |world,karel,row,column| karel.direction == :south },
|
132
|
+
:facing_east => lambda{ |world,karel,row,column| karel.direction == :east },
|
133
|
+
:facing_west => lambda{ |world,karel,row,column| karel.direction == :west },
|
134
|
+
|
135
|
+
:not_on_beeper => lambda{ |world,karel,row,column| !CONDITIONS[:on_beeper].call(world,karel,row,column) },
|
136
|
+
:front_not_clear => lambda{ |world,karel,row,column| !CONDITIONS[:front_clear].call(world,karel,row,column) },
|
137
|
+
:left_not_clear => lambda{ |world,karel,row,column| !CONDITIONS[:left_clear].call(world,karel,row,column) },
|
138
|
+
:right_not_clear => lambda{ |world,karel,row,column| !CONDITIONS[:right_clear].call(world,karel,row,column) },
|
139
|
+
:not_facing_north => lambda{ |world,karel,row,column| !CONDITIONS[:facing_north].call(world,karel,row,column) },
|
140
|
+
:not_facing_south => lambda{ |world,karel,row,column| !CONDITIONS[:facing_south].call(world,karel,row,column) },
|
141
|
+
:not_facing_east => lambda{ |world,karel,row,column| !CONDITIONS[:facing_east].call(world,karel,row,column) },
|
142
|
+
:not_facing_west => lambda{ |world,karel,row,column| !CONDITIONS[:facing_west].call(world,karel,row,column) },
|
143
|
+
}
|
144
|
+
|
145
|
+
CONDITIONS.each_key do |condition|
|
146
|
+
define_method condition do
|
147
|
+
condition
|
148
|
+
end
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module Karel
|
2
|
+
# Thrown when Karel does something that causes him to explode
|
3
|
+
class Explosion < Exception; end
|
4
|
+
# Thrown if the world was not created in a valid state
|
5
|
+
class InvalidWorld < Exception; end
|
6
|
+
# Thrown when a square is occupied when an operation requires it not to be
|
7
|
+
class SquareOccupied < Exception; end
|
8
|
+
# Thrown when there is no beeper on a square when an operation require there to be
|
9
|
+
class NoBeeper < Exception; end
|
10
|
+
# Thrown when a subroutine is defined that has a bad or not-allowed name
|
11
|
+
class BadSubroutine < Exception; end
|
12
|
+
# Thrown when a while loop has gone on too long and we think there might
|
13
|
+
# be an infinite loop
|
14
|
+
class PossibleInfiniteLoop < Exception; end
|
15
|
+
end
|
data/lib/karel/karel.rb
ADDED
@@ -0,0 +1,67 @@
|
|
1
|
+
module Karel
|
2
|
+
# The robot itself. Knows only its internal state, not much about where it is
|
3
|
+
class Karel
|
4
|
+
attr_reader :direction
|
5
|
+
attr_reader :num_beepers
|
6
|
+
|
7
|
+
def initialize
|
8
|
+
reset
|
9
|
+
end
|
10
|
+
|
11
|
+
def reset
|
12
|
+
@direction = :north
|
13
|
+
@num_beepers = 0
|
14
|
+
end
|
15
|
+
|
16
|
+
def put_beeper_in_bag
|
17
|
+
@num_beepers += 1
|
18
|
+
end
|
19
|
+
|
20
|
+
def remove_beeper_from_bag
|
21
|
+
raise Explosion if @num_beepers <= 0
|
22
|
+
@num_beepers -= 1
|
23
|
+
end
|
24
|
+
|
25
|
+
def turnleft
|
26
|
+
@direction = Karel.left_of(@direction)
|
27
|
+
end
|
28
|
+
|
29
|
+
def to_s
|
30
|
+
STRINGS[direction]
|
31
|
+
end
|
32
|
+
|
33
|
+
def self.left_of(direction)
|
34
|
+
new_index = DIRECTIONS.index(direction) + 1
|
35
|
+
new_index = 0 if new_index >= DIRECTIONS.size
|
36
|
+
DIRECTIONS[new_index]
|
37
|
+
end
|
38
|
+
|
39
|
+
def self.right_of(direction)
|
40
|
+
new_index = DIRECTIONS.index(direction) - 1
|
41
|
+
new_index = DIRECTIONS.size - 1 if new_index < 0
|
42
|
+
DIRECTIONS[new_index]
|
43
|
+
end
|
44
|
+
|
45
|
+
def self.coordinates_after_move_from(direction,x,y)
|
46
|
+
if direction == :north
|
47
|
+
[x-1,y]
|
48
|
+
elsif direction == :south
|
49
|
+
[x+1,y]
|
50
|
+
elsif direction == :west
|
51
|
+
[x,y-1]
|
52
|
+
elsif direction == :east
|
53
|
+
[x,y+1]
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
DIRECTIONS = [:north,:west,:south,:east]
|
58
|
+
|
59
|
+
private
|
60
|
+
STRINGS = {
|
61
|
+
:north => '^',
|
62
|
+
:west => '<',
|
63
|
+
:south => 'v',
|
64
|
+
:east => '>',
|
65
|
+
}
|
66
|
+
end
|
67
|
+
end
|
data/lib/karel/square.rb
ADDED
@@ -0,0 +1,42 @@
|
|
1
|
+
module Karel
|
2
|
+
# A square on the board
|
3
|
+
class Square; end
|
4
|
+
|
5
|
+
# A square that can hold a beeper, or Karel.
|
6
|
+
class FreeSquare < Square
|
7
|
+
def initialize(beeper=false)
|
8
|
+
@beeper = beeper
|
9
|
+
end
|
10
|
+
|
11
|
+
def wall?; false; end
|
12
|
+
|
13
|
+
def beeper?; @beeper; end
|
14
|
+
|
15
|
+
def pick_beeper
|
16
|
+
raise NoBeeper unless beeper?
|
17
|
+
@beeper = false
|
18
|
+
end
|
19
|
+
|
20
|
+
def put_beeper
|
21
|
+
raise SquareOccupied if beeper?
|
22
|
+
@beeper = true
|
23
|
+
end
|
24
|
+
|
25
|
+
def to_s
|
26
|
+
if beeper?
|
27
|
+
"B"
|
28
|
+
else
|
29
|
+
" "
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
# A wall square
|
35
|
+
class Wall < Square
|
36
|
+
def wall?; true; end
|
37
|
+
def beeper?; false; end
|
38
|
+
def to_s; "W"; end
|
39
|
+
def put_beeper; raise SquareOccupied; end
|
40
|
+
def pick_beeper; raise NoBeeper; end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,86 @@
|
|
1
|
+
require 'karel/commands'
|
2
|
+
|
3
|
+
module Karel
|
4
|
+
# A very basic and very hacky syntax checker. This won't ensure the program runs
|
5
|
+
# and is pretty lame overall, but it more or less gets the job done
|
6
|
+
class SyntaxChecker
|
7
|
+
def initialize
|
8
|
+
@builtins = []
|
9
|
+
Commands.public_instance_methods.each do |method|
|
10
|
+
@builtins << method if method =~ /^[A-Z_]*$/
|
11
|
+
end
|
12
|
+
@control = [ 'WHILE', 'IF', 'ELSE', 'ITERATE' ]
|
13
|
+
end
|
14
|
+
|
15
|
+
def valid?(code)
|
16
|
+
line_number = 1
|
17
|
+
@errors = {}
|
18
|
+
in_world = false
|
19
|
+
defines = []
|
20
|
+
code.split(/\n/).each do |line|
|
21
|
+
line_number += 1
|
22
|
+
trimmed = line.strip.gsub(/\(\)/,'').strip
|
23
|
+
if in_world
|
24
|
+
if trimmed =~ /^END\s*$/
|
25
|
+
in_world = false
|
26
|
+
else
|
27
|
+
if trimmed =~ /^[ KWB]*$/
|
28
|
+
# ok
|
29
|
+
else
|
30
|
+
@errors[line_number] = "Bad world definition: #{trimmed}"
|
31
|
+
end
|
32
|
+
end
|
33
|
+
elsif (trimmed =~ /^WORLD\s+<<END$/)
|
34
|
+
in_world = true
|
35
|
+
else
|
36
|
+
next if trimmed =~/^\s*$/
|
37
|
+
next if trimmed =~/^\s*#.*$/
|
38
|
+
if trimmed =~ /^DEFINE\s*\(\s*["'](.[^"']*)["']\s*\)\s*\{\s*$/
|
39
|
+
define = $1
|
40
|
+
if define =~ /^[A-Z_]*$/
|
41
|
+
defines << define
|
42
|
+
else
|
43
|
+
@errors[line_number] = "Bad DEFINE symbol #{define}"
|
44
|
+
end
|
45
|
+
else
|
46
|
+
to_the_bone = trimmed.gsub(/[^A-Z_].*$/,'')
|
47
|
+
if @control.include? to_the_bone
|
48
|
+
if trimmed =~ /^#{to_the_bone}\s*\((.*)\)\s*\{\s*$/
|
49
|
+
condition = $1
|
50
|
+
if to_the_bone == 'ITERATE'
|
51
|
+
if condition =~ /^\s*[0-9]\.TIMES\s*$/
|
52
|
+
# ok
|
53
|
+
else
|
54
|
+
@errors[line_number] = "Bad #{to_the_bone} condition: #{condition}"
|
55
|
+
end
|
56
|
+
else
|
57
|
+
if (trimmed =~ /^ELSE/)
|
58
|
+
@errors[line_number] = "Bad #{to_the_bone} statement: #{trimmed}"
|
59
|
+
end
|
60
|
+
if condition =~ /^[a-z_]+$/
|
61
|
+
# ok
|
62
|
+
else
|
63
|
+
@errors[line_number] = "Bad #{to_the_bone} condition: #{condition}"
|
64
|
+
end
|
65
|
+
end
|
66
|
+
else
|
67
|
+
if trimmed =~ /^ELSE\s*\{\s*$/
|
68
|
+
# ok
|
69
|
+
else
|
70
|
+
@errors[line_number] = "Bad #{to_the_bone} statement: #{trimmed}"
|
71
|
+
end
|
72
|
+
end
|
73
|
+
elsif trimmed =~ /^\s*\}\s*$/
|
74
|
+
# ignore
|
75
|
+
else
|
76
|
+
@errors[line_number] = "Unknown symbol #{trimmed}" unless (@builtins.include? trimmed) || (defines.include? trimmed)
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
@errors.empty?
|
82
|
+
end
|
83
|
+
|
84
|
+
def errors; @errors; end
|
85
|
+
end
|
86
|
+
end
|
data/lib/karel/world.rb
ADDED
@@ -0,0 +1,140 @@
|
|
1
|
+
module Karel
|
2
|
+
# The world in which Karel operates.
|
3
|
+
class World
|
4
|
+
|
5
|
+
attr_reader :karel
|
6
|
+
attr_reader :width
|
7
|
+
attr_reader :height
|
8
|
+
|
9
|
+
# Creates the world from a string that is a multi-line representation of the world.
|
10
|
+
# This isn't the constructor because our DSL requires uppercase and we don't want
|
11
|
+
# warnings by creating the world twice (once when declaring it as a constant in the Karel
|
12
|
+
# module and once inside the module method WORLD).
|
13
|
+
def create_from_string(definition,karel)
|
14
|
+
@karel = nil
|
15
|
+
@karel_instance = karel
|
16
|
+
@karel_instance.reset
|
17
|
+
@world = []
|
18
|
+
@width = 0
|
19
|
+
row = 0
|
20
|
+
column = 0
|
21
|
+
rows = definition.split(/\n/)
|
22
|
+
@height = rows.size
|
23
|
+
rows.each do |row_data|
|
24
|
+
@width = row_data.length if row_data.length > @width
|
25
|
+
row_data.split(//).each do |square|
|
26
|
+
@karel = [row,column] if square == 'K'
|
27
|
+
@world[row] ||= []
|
28
|
+
@world[row][column] = square_for(square)
|
29
|
+
column += 1
|
30
|
+
end
|
31
|
+
column = 0
|
32
|
+
row += 1
|
33
|
+
end
|
34
|
+
raise InvalidWorld,"There is no Karel in this World" unless @karel
|
35
|
+
fill_in_empty_spaces
|
36
|
+
end
|
37
|
+
|
38
|
+
# Removes a beeper from the world at the given row/column
|
39
|
+
#
|
40
|
+
# raises a NoBeeper exception if there is no beeper at those coordinates.
|
41
|
+
def remove_beeper(row,column)
|
42
|
+
self[row,column].pick_beeper
|
43
|
+
end
|
44
|
+
|
45
|
+
# Adds a beeper tot he world at the given row/column
|
46
|
+
#
|
47
|
+
# raises a SquareOccupied exception if there is a beeper or wall at those coordinates.
|
48
|
+
def add_beeper(row,column)
|
49
|
+
self[row,column].put_beeper
|
50
|
+
end
|
51
|
+
|
52
|
+
# Set the location of Karel
|
53
|
+
#
|
54
|
+
# location - an array of size 2, with the row/column where karel should be
|
55
|
+
#
|
56
|
+
# Raises an Explosion if you try to put Karel where there is a wall
|
57
|
+
def karel=(location)
|
58
|
+
raise Explosion,"#{location.inspect} is out of bounds" unless in_bounds?(*location)
|
59
|
+
raise Explosion if self[*location].wall?
|
60
|
+
@karel = location
|
61
|
+
end
|
62
|
+
|
63
|
+
# True if the square could be moved into by Karel
|
64
|
+
def clear?(row,column)
|
65
|
+
if in_bounds?(row,column)
|
66
|
+
!self[row,column].wall?
|
67
|
+
else
|
68
|
+
false
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
# Provides access to the square at the given row/column
|
73
|
+
#
|
74
|
+
# Returns a Square instance, on which you can call handy methods like beeper? and wall?
|
75
|
+
def [](row,column)
|
76
|
+
@world[row][column]
|
77
|
+
end
|
78
|
+
|
79
|
+
# Returns the string representation, which looks like the
|
80
|
+
# the string used to construct this
|
81
|
+
def to_s
|
82
|
+
string = "+"
|
83
|
+
@width.times { string += '-' }
|
84
|
+
string += "+\n"
|
85
|
+
@height.times do |row|
|
86
|
+
string += "|"
|
87
|
+
@width.times do |column|
|
88
|
+
kr,kc = karel
|
89
|
+
if (kr == row) && (kc == column)
|
90
|
+
string += @karel_instance.to_s
|
91
|
+
else
|
92
|
+
string += self[row,column].to_s
|
93
|
+
end
|
94
|
+
end
|
95
|
+
string += "|\n"
|
96
|
+
end
|
97
|
+
string += "+"
|
98
|
+
@width.times { string += '-' }
|
99
|
+
string += "+\n"
|
100
|
+
string
|
101
|
+
end
|
102
|
+
|
103
|
+
private
|
104
|
+
|
105
|
+
SQUARE_FACTORIES = {
|
106
|
+
'B' => lambda { FreeSquare.new(true) },
|
107
|
+
'W' => lambda { Wall.new },
|
108
|
+
' ' => lambda { FreeSquare.new },
|
109
|
+
'K' => lambda { FreeSquare.new },
|
110
|
+
}
|
111
|
+
|
112
|
+
# Given a character, returns the square type
|
113
|
+
# that should go there
|
114
|
+
def square_for(square)
|
115
|
+
raise InvalidWorld,"Square type #{square} is not valid" if SQUARE_FACTORIES[square].nil?
|
116
|
+
SQUARE_FACTORIES[square].call
|
117
|
+
end
|
118
|
+
|
119
|
+
def in_bounds?(row,column)
|
120
|
+
if row < 0 || column < 0
|
121
|
+
false
|
122
|
+
elsif row >= @height
|
123
|
+
false
|
124
|
+
elsif column >= @width
|
125
|
+
false
|
126
|
+
else
|
127
|
+
true
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
def fill_in_empty_spaces
|
132
|
+
@height.times do |row|
|
133
|
+
@world[row] = [] if @world[row].nil?
|
134
|
+
@width.times do |column|
|
135
|
+
@world[row][column] = FreeSquare.new if @world[row][column].nil?
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|
metadata
ADDED
@@ -0,0 +1,104 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: karel
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
hash: 27
|
5
|
+
prerelease: false
|
6
|
+
segments:
|
7
|
+
- 0
|
8
|
+
- 1
|
9
|
+
- 0
|
10
|
+
version: 0.1.0
|
11
|
+
platform: ruby
|
12
|
+
authors:
|
13
|
+
- Dave Copeland
|
14
|
+
autorequire:
|
15
|
+
bindir: bin
|
16
|
+
cert_chain: []
|
17
|
+
|
18
|
+
date: 2010-09-26 00:00:00 -04:00
|
19
|
+
default_executable:
|
20
|
+
dependencies:
|
21
|
+
- !ruby/object:Gem::Dependency
|
22
|
+
name: gli
|
23
|
+
prerelease: false
|
24
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ">="
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
hash: 17
|
30
|
+
segments:
|
31
|
+
- 1
|
32
|
+
- 1
|
33
|
+
- 1
|
34
|
+
version: 1.1.1
|
35
|
+
type: :runtime
|
36
|
+
version_requirements: *id001
|
37
|
+
description: |
|
38
|
+
This is a ruby implementation of Karel The Robot, a
|
39
|
+
programming language designed for extreme beginners. It concerns controlling
|
40
|
+
a robot, named Karel, in a grid-based world comprised of walls and beepers. Karel can pick up and put down beepers, move
|
41
|
+
forward, and turn left. Karel can also detect things about his environment.
|
42
|
+
|
43
|
+
email: davetron5000@gmail.com
|
44
|
+
executables:
|
45
|
+
- karel
|
46
|
+
extensions: []
|
47
|
+
|
48
|
+
extra_rdoc_files:
|
49
|
+
- README.rdoc
|
50
|
+
- karel.rdoc
|
51
|
+
files:
|
52
|
+
- bin/karel
|
53
|
+
- ext/fixnum_TIMES.rb
|
54
|
+
- lib/karel/commands.rb
|
55
|
+
- lib/karel/exceptions.rb
|
56
|
+
- lib/karel/karel.rb
|
57
|
+
- lib/karel/square.rb
|
58
|
+
- lib/karel/syntax_checker.rb
|
59
|
+
- lib/karel/world.rb
|
60
|
+
- lib/karel.rb
|
61
|
+
- README.rdoc
|
62
|
+
- karel.rdoc
|
63
|
+
has_rdoc: true
|
64
|
+
homepage: http://www.naildrivin5.com
|
65
|
+
licenses: []
|
66
|
+
|
67
|
+
post_install_message:
|
68
|
+
rdoc_options:
|
69
|
+
- --title
|
70
|
+
- Karel The Robot
|
71
|
+
- --main
|
72
|
+
- README.rdoc
|
73
|
+
- -ri
|
74
|
+
require_paths:
|
75
|
+
- lib
|
76
|
+
- lib
|
77
|
+
- ext
|
78
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
79
|
+
none: false
|
80
|
+
requirements:
|
81
|
+
- - ">="
|
82
|
+
- !ruby/object:Gem::Version
|
83
|
+
hash: 3
|
84
|
+
segments:
|
85
|
+
- 0
|
86
|
+
version: "0"
|
87
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
88
|
+
none: false
|
89
|
+
requirements:
|
90
|
+
- - ">="
|
91
|
+
- !ruby/object:Gem::Version
|
92
|
+
hash: 3
|
93
|
+
segments:
|
94
|
+
- 0
|
95
|
+
version: "0"
|
96
|
+
requirements: []
|
97
|
+
|
98
|
+
rubyforge_project:
|
99
|
+
rubygems_version: 1.3.7
|
100
|
+
signing_key:
|
101
|
+
specification_version: 3
|
102
|
+
summary: An implementation of Karel the Robot as a Ruby DSL
|
103
|
+
test_files: []
|
104
|
+
|