├── .gitignore
├── DEVELOP.md
├── PROSOL.md
├── README.md
├── opengl
├── pom.xml
├── sloc
└── src
├── main
├── kotlin
│ ├── Main.kt
│ ├── common
│ │ ├── Diagnostic.kt
│ │ └── Lists.kt
│ ├── gui
│ │ ├── Autocompletion.kt
│ │ ├── BufferedImages.kt
│ │ ├── BytecodeFlexer.kt
│ │ ├── BytecodeTable.kt
│ │ ├── ControlPanel.kt
│ │ ├── Editor.kt
│ │ ├── Flexer.kt
│ │ ├── MainDesign.kt
│ │ ├── MainFlow.kt
│ │ ├── MainHandler.kt
│ │ ├── StackTable.kt
│ │ ├── SwingBridge.kt
│ │ ├── Toolkits.kt
│ │ ├── VirtualMachinePanel.kt
│ │ └── WorldPanel.kt
│ ├── logic
│ │ ├── Check.kt
│ │ ├── FloorPlan.kt
│ │ ├── KarelError.kt
│ │ ├── LabyrinthGenerator.kt
│ │ ├── Problem.kt
│ │ ├── World.kt
│ │ └── WorldEntropy.kt
│ ├── syntax
│ │ ├── lexer
│ │ │ ├── Lexer.kt
│ │ │ ├── LexerBase.kt
│ │ │ ├── Token.kt
│ │ │ └── TokenKind.kt
│ │ ├── parser
│ │ │ ├── Conditions.kt
│ │ │ ├── Parser.kt
│ │ │ ├── Sema.kt
│ │ │ └── Statements.kt
│ │ └── tree
│ │ │ └── Nodes.kt
│ └── vm
│ │ ├── Emitter.kt
│ │ ├── IllegalBytecode.kt
│ │ ├── Instruction.kt
│ │ ├── Label.kt
│ │ ├── Stack.kt
│ │ └── VirtualMachine.kt
└── resources
│ └── tiles
│ ├── 40
│ ├── beeper.png
│ ├── cross.png
│ ├── karel.png
│ └── wall.png
│ └── 64
│ ├── beeper.png
│ ├── cross.png
│ ├── karel.png
│ └── wall.png
└── test
└── kotlin
├── gui
└── AutocompletionTest.kt
├── logic
├── BeeperTest.kt
├── Week1Test.kt
├── Week2Test.kt
├── Week3Test.kt
└── WorldTestBase.kt
├── syntax
├── lexer
│ ├── LexerNegativeTest.kt
│ └── LexerTest.kt
└── parser
│ ├── ParserNegativeTest.kt
│ └── SemaTest.kt
└── vm
├── EmitterTest.kt
└── InstructionTest.kt
/.gitignore:
--------------------------------------------------------------------------------
1 | /target/
2 | /.idea/
3 | /*.iml
4 |
--------------------------------------------------------------------------------
/DEVELOP.md:
--------------------------------------------------------------------------------
1 | ## How do I compile karel from source?
2 |
3 | ```
4 | git clone https://github.com/fredoverflow/freditor
5 | cd freditor
6 | mvn install
7 | cd ..
8 | git clone https://github.com/fredoverflow/karel
9 | cd karel
10 | mvn package
11 | ```
12 |
13 | The executable `karel.jar` will be located inside the `target` folder.
14 |
15 | ## How do I install IntelliJ IDEA?
16 |
17 | Download the Community Edition `zip` or `tar.gz` from https://www.jetbrains.com/idea/download and extract it wherever you like.
18 | Navigate to the `bin` folder and run the `idea.bat` or `idea.sh` script.
19 | Then follow these instructions:
20 |
21 | ```
22 | Import IntelliJ IDEA Settings From...
23 | (o) Do not import settings
24 | OK
25 |
26 | JetBrains Privacy Policy
27 | [x] I confirm that I have read and accept the terms of this User Agreement
28 | Continue
29 |
30 | Data Sharing
31 | Don't send
32 |
33 | Skip Remaining and Set Defaults
34 | ```
35 |
36 | ## How do I import karel into IntelliJ IDEA?
37 |
38 | * If there are no projects open, pick the **Import Project** option from the *Welcome to IntelliJ IDEA* screen.
39 | * Otherwise, pick **File > New > Project from Existing Sources...**
40 |
41 | ```
42 | Windows: C:\Users\fred\git\karel
43 | Linux: /home/fred/git/karel
44 | OK
45 |
46 | Import Project
47 | (o) Import project from external model
48 | Maven
49 | Next
50 | Next
51 | Next
52 |
53 | Please select project SDK. This SDK will be used by default by all project modules.
54 | +
55 | JDK
56 | Windows: C:\Program Files\Java\jdk1.8.0_...
57 | Linux: /usr/lib/jvm/java-8-openjdk-amd64
58 | OK
59 |
60 | Next
61 | Finish
62 |
63 | Tip of the Day
64 | [ ] Show tips on startup
65 | Close
66 | ```
67 |
68 | ## How do I run karel from within IntelliJ IDEA?
69 |
70 | ```
71 | karel/src/main/kotlin/Main.kt (right-click)
72 | Run 'MainKt'
73 | ```
74 |
75 | ## What IntelliJ IDEA settings do you like to change after install?
76 |
77 | **File > Settings...**
78 |
79 | * Keymap
80 | * Eclipse
81 | * Editor > Font
82 | * Font: Fira Code (`sudo apt install fonts-firacode`)
83 | * Size: 20
84 | * Editor > General > Code Folding
85 | * [ ] One-line methods
86 | * Editor > General > Appearance
87 | * [ ] Caret blinking
88 | * [ ] Show intention bulb
89 |
--------------------------------------------------------------------------------
/PROSOL.md:
--------------------------------------------------------------------------------
1 | ### Problem solving
2 |
3 | Problem solving means translating human-understandable problem descriptions into machine-executable programs.
4 | Ideally, machine-executable programs should also be human-understandable; we attain that ideal with *abstractions*.
5 |
6 | Abstractions aid tremendously in developing solutions to problems in a top-down (decomposing a complex problem into simpler subproblems) or bottom-up (composing simple subsolutions into a complex solution) fashion.
7 |
8 | ### Abstractions
9 |
10 | Humans like to organize processes (for example, doing the laundry) in hierarchical levels of abstraction:
11 |
12 | do laundry:
13 | - wash laundry 🧼
14 | - wait 1 hour ⏳
15 | - hang laundry 🧺
16 |
17 | wash laundry: 🧼
18 | - put clothes into washing drum
19 | - apply laundry detergent
20 | - close washing drum
21 | - put plug into socket
22 | - choose temperature
23 | - press start button
24 |
25 | hang laundry: 🧺
26 | - open washing drum
27 | - put clothes into laundry basket
28 | - remove plug from socket
29 | - put clothes onto clothes line
30 |
31 | If we keep delving deeper into lower levels of abstraction until we reach individual muscle movements, even the most simple-minded being can do the laundry by following the given instructions carefully. And how do we call such beings? Robots!
32 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | 😕 Brauchst du Hilfe beim Installieren in Windows?
2 | 📺 Dann schau dir [Karel The Robot installieren in Windows](https://www.youtube.com/watch?v=69du58tb_48&list=PL5vhQpd0v6Y5dVcY8_9Ru6dANaz6UmPnL) an!
3 |
4 | 
5 |
6 | ## Table of contents
7 |
8 | 1. [Background](#background)
9 | 2. [Getting started](#getting-started)
10 | - [Autosave](#autosave)
11 | 3. [Language reference](#language-reference)
12 | - [Primitive commands](#primitive-commands)
13 | - [Custom commands](#custom-commands)
14 | - [Repeat](#repeat)
15 | - [If/else](#ifelse)
16 | - [Primitive conditions](#primitive-conditions)
17 | - [If/else if](#ifelse-if)
18 | - [Not `!`](#not-)
19 | - [And `&&`](#and-)
20 | - [Or `||`](#or-)
21 | - [Summary compound conditions](#summary-compound-conditions)
22 | - [While](#while)
23 | 4. [Keyboard shortcuts](#keyboard-shortcuts)
24 |
25 | ## Background
26 |
27 | Karel The Robot is a simple teaching environment for imperative programming basics.
28 | The original idea was developed in the 1970s by Richard Pattis at Stanford University:
29 |
30 | > In the 1970s, a Stanford graduate student named Rich Pattis decided that it would be easier to teach the fundamentals of programming if students could somehow learn the basic ideas in a simple environment free from the complexities that characterize most programming languages.
31 |
32 | 
33 |
34 | > In sophisticated languages like Java, there are so many details that learning these details often becomes the focus of the course.
35 | > When that happens, the much more critical issues of [problem solving](PROSOL.md) tend to get lost in the shuffle.
36 | > By starting with Karel, you can concentrate on solving problems from the very beginning.
37 | > And because Karel encourages imagination and creativity, you can have quite a lot of fun along the way.
38 |
39 | This project started in 2012 due to dissatisfaction with the available Karel environments.
40 | Since then, thousands of German university students have been introduced to the basics of imperative programming via this project.
41 |
42 | ## Getting started
43 |
44 | 😕 Brauchst du Hilfe beim Installieren in Windows?
45 | 📺 Dann schau dir [Karel The Robot installieren in Windows](https://www.youtube.com/watch?v=69du58tb_48&list=PL5vhQpd0v6Y5dVcY8_9Ru6dANaz6UmPnL) an!
46 |
47 | Please take the time to **read the following instructions carefully.**
48 | Most problems stem from skipping or misunderstanding important steps.
49 |
50 | ### ☕ Windows & macOS
51 |
52 | 1. Visit https://adoptium.net
53 |
54 | 2. Click "Latest release" button to download Java installer
55 |
56 | 3. Wait for download to finish
57 |
58 | 4. Open the `Downloads` folder (via Windows Explorer or Finder/Spotlight, respectively) and double-click `OpenJDK...` to start Java installer
59 |
60 | 5. Click Next, Next, Install, Finish
61 |
62 | 6. Click [karel.jar](https://raw.githubusercontent.com/fredoverflow/karel/release/karel.jar) to download Karel
63 | **If Karel fails to download**, continue with ⚠️ Troubleshooting *Windows*, or ⚠️ Troubleshooting *macOS*
64 |
65 | 7. Open the `Downloads` folder and double-click `karel.jar` to start Karel
66 | **If Karel fails to start**, continue with ⚠️ Troubleshooting *Windows*, or ⚠️ Troubleshooting *macOS*
67 |
68 | ### ⚠️ Troubleshooting *Windows*
69 |
70 | Steps 1 through 5 (install Java) worked, but steps 6 (download Karel) or 7 (start Karel) failed? Then read on.
71 |
72 | - Move your mouse over the script below
73 | - A button appears in the top right corner of the script
74 | - Click that button to copy the script
75 | ```cmd
76 | cd Downloads
77 | if exist karel.jar.zip erase karel.jar.zip
78 | curl -o karel.jar https://raw.githubusercontent.com/fredoverflow/karel/release/karel.jar
79 | echo java -version > karel.cmd
80 | echo java -jar karel.jar >> karel.cmd
81 | karel.cmd
82 |
83 | ```
84 | - Press the Windows key (the key on the bottom left with the Windows logo ⊞ on it)
85 | - Write `cmd` and confirm with Enter
86 | - A terminal appears
87 | - Right-click anywhere inside that terminal to paste and execute the script
88 |
89 | From now on, simply double-click `karel.cmd` in the `Downloads` folder to start Karel.
90 | Feel free to move `karel.jar` and `karel.cmd` to the Desktop or any other folder you prefer.
91 |
92 | ### ⚠️ Troubleshooting *macOS*
93 |
94 | Steps 1 through 5 (install Java) worked, but steps 6 (download Karel) or 7 (start Karel) failed? Then read on.
95 |
96 | - Move your mouse over the script below
97 | - A button appears in the top right corner of the script
98 | - Click that button to copy the script
99 | ```sh
100 | cd Downloads
101 | curl -o karel.jar https://raw.githubusercontent.com/fredoverflow/karel/release/karel.jar
102 | chmod +x karel.jar
103 | echo java -version > karel.sh
104 | echo java -jar karel.jar >> karel.sh
105 | chmod +x karel.sh
106 | ./karel.sh
107 |
108 | ```
109 | - Press `Command⌘ Space` (or click the magnifying glass 🔍 in the top right corner of the screen) to open Spotlight
110 | - Write `terminal` and confirm with Enter
111 | - A terminal appears
112 | - Press `Command⌘ V` to paste and execute the script
113 |
114 | From now on, simply double-click `karel.sh` in the `Downloads` folder to start Karel.
115 | Feel free to move `karel.jar` and `karel.sh` to the Desktop or any other folder you prefer.
116 |
117 | ### 🐧 Ubuntu, Linux Mint, Debian...
118 |
119 | ```sh
120 | sudo apt install default-jdk
121 | cd Downloads
122 | curl -o karel.jar https://raw.githubusercontent.com/fredoverflow/karel/release/karel.jar
123 | chmod +x karel.jar
124 | echo java -version > karel.sh
125 | echo java -jar -Dsun.java2d.opengl=True karel.jar >> karel.sh
126 | chmod +x karel.sh
127 | ./karel.sh
128 |
129 | ```
130 |
131 | From now on, simply double-click `karel.sh` in the `Downloads` folder to start Karel.
132 | Feel free to move `karel.jar` and `karel.sh` to the Desktop or any other folder you prefer.
133 |
134 | ### Autosave
135 |
136 | Your code is automatically saved to a new file each time you click the start button.
137 | The save folder is named `karel`, and it is located in your home directory.
138 | The full path is displayed in the title bar.
139 |
140 | ## Language reference
141 |
142 | ### Primitive commands
143 |
144 | | Shortcut | Command | Meaning |
145 | | -------- | ----------------- | ------- |
146 | | F1 | `moveForward();` | Karel moves one square forward in the direction he currently faces.
Fails if a wall blocks the way. |
147 | | F2 | `turnLeft();` | Karel turns 90° to the left. |
148 | | F3 | `turnAround();` | Karel turns 180° around. |
149 | | F4 | `turnRight();` | Karel turns 90° to the right. |
150 | | F5 | `pickBeeper();` | Karel picks a beeper from the square he currently stands on.
Fails if there is no beeper. |
151 | | F6 | `dropBeeper();` | Karel drops a beeper onto the square he currently stands on.
Fails if there already is a beeper. |
152 |
153 | ### Custom commands
154 |
155 | Sometimes the same sequence of commands appears multiple times:
156 | ```
157 | void roundTrip()
158 | {
159 | moveForward();
160 | moveForward();
161 | moveForward();
162 | moveForward();
163 | moveForward();
164 | moveForward();
165 | moveForward();
166 | moveForward();
167 | moveForward();
168 |
169 | turnAround();
170 |
171 | moveForward();
172 | moveForward();
173 | moveForward();
174 | moveForward();
175 | moveForward();
176 | moveForward();
177 | moveForward();
178 | moveForward();
179 | moveForward();
180 | }
181 | ```
182 | You can extract such a sequence of commands into a new, custom command:
183 | ```
184 | void moveAcrossWorld()
185 | {
186 | moveForward();
187 | moveForward();
188 | moveForward();
189 | moveForward();
190 | moveForward();
191 | moveForward();
192 | moveForward();
193 | moveForward();
194 | moveForward();
195 | }
196 | ```
197 | and use it just like a primitive command:
198 | ```
199 | void roundTrip()
200 | {
201 | moveAcrossWorld();
202 | turnAround();
203 | moveAcrossWorld();
204 | }
205 | ```
206 | Deciding when a sequence of commands is worth extracting and choosing a good name for the custom command are essential development skills you will acquire over time.
207 |
208 | ### Repeat
209 |
210 | Instead of writing the same sequence of commands multiple times:
211 | ```
212 | void dance()
213 | {
214 | moveForward();
215 | turnLeft();
216 | moveForward();
217 | turnLeft();
218 | moveForward();
219 | turnLeft();
220 | moveForward();
221 | turnLeft();
222 | }
223 | ```
224 | you can use `repeat` and only write it once:
225 | ```
226 | void dance()
227 | {
228 | repeat (4)
229 | {
230 | moveForward();
231 | turnLeft();
232 | }
233 | }
234 | ```
235 |
236 | ### If/else
237 |
238 | Sometimes you only want to do something if some condition holds:
239 | ```
240 | if (onBeeper())
241 | {
242 | pickBeeper();
243 | }
244 | ```
245 | Optionally, you can also specify what to do in case the condition does *not* hold:
246 | ```
247 | if (onBeeper())
248 | {
249 | pickBeeper();
250 | }
251 | else
252 | {
253 | dropBeeper();
254 | }
255 | ```
256 | Note that conditions are only checked when control flow actually reaches them (when the corresponding line is highlighted in the code editor).
257 | Conditions are *not* periodically checked in the background!
258 | In large programs with lots of potentially contradicting conditionals, such periodic background checks would quickly lead to incomprehensible program behavior.
259 |
260 | ### Primitive conditions
261 |
262 | | Shortcut | Condition | Meaning |
263 | | -------- | ---------------- | ------- |
264 | | F7 | `onBeeper()` | Karel checks whether a beeper is on the square he currently stands on. |
265 | | F8 | `beeperAhead()` | Karel checks whether a beeper is on the square immediately in front of him. |
266 | | F9 | `leftIsClear()` | Karel checks whether no wall is between him and the square to his left. |
267 | | F10 | `frontIsClear()` | Karel checks whether no wall is between him and the square in front of him. |
268 | | F11 | `rightIsClear()` | Karel checks whether no wall is between him and the square to his right. |
269 |
270 | ### If/else if
271 |
272 | An `else` with nothing but another `if` inside:
273 | ```
274 | if (leftIsClear())
275 | {
276 | turnLeft();
277 | }
278 | else
279 | {
280 | if (rightIsClear())
281 | {
282 | turnRight();
283 | }
284 | }
285 | ```
286 | can be simplified by leaving out the block between the `else` and `if`:
287 | ```
288 | if (leftIsClear())
289 | {
290 | turnLeft();
291 | }
292 | else if (rightIsClear())
293 | {
294 | turnRight();
295 | }
296 | ```
297 | Note that without the `else`, Karel might turn left and then immedately turn right again, given `frontIsClear()` originally held.
298 | The `else` prevents the second `if` from executing in case the first condition was already `true`.
299 |
300 | ### Not `!`
301 |
302 | An `if/else` with an empty first block:
303 | ```
304 | if (onBeeper())
305 | {
306 | }
307 | else
308 | {
309 | dropBeeper();
310 | }
311 | ```
312 | can be simplified by negating the condition with a leading `!`:
313 | ```
314 | if (!onBeeper())
315 | {
316 | dropBeeper();
317 | }
318 | ```
319 |
320 | ### And `&&`
321 |
322 | An `if` with nothing but another `if` inside:
323 | ```
324 | if (frontIsClear())
325 | {
326 | if (beeperAhead())
327 | {
328 | moveForward();
329 | pickBeeper();
330 | }
331 | }
332 | ```
333 | can be simplified by combining both conditions with `&&`:
334 | ```
335 | if (frontIsClear() && beeperAhead())
336 | {
337 | moveForward();
338 | pickBeeper();
339 | }
340 | ```
341 |
342 | ### Or `||`
343 |
344 | An `if/else if` with identical blocks:
345 | ```
346 | if (!frontIsClear())
347 | {
348 | turnRight();
349 | }
350 | else if (beeperAhead())
351 | {
352 | turnRight();
353 | }
354 | ```
355 | can be simplified by combining both conditions with `||`:
356 | ```
357 | if (!frontIsClear() || beeperAhead())
358 | {
359 | turnRight();
360 | }
361 | ```
362 |
363 | ### Summary compound conditions
364 |
365 | | Condition | Meaning |
366 | | --------- | ------- |
367 | | !a
| holds if a does **not** hold (and vice versa) |
368 | | a && b
| holds if both a **and** b hold |
369 | | a || b
| holds if a **or** b (or both) hold |
370 | | a || !b && c
| a || ((!b) && c)
|
371 |
372 | ### While
373 |
374 | `if` checks the condition and then executes the block at most once:
375 | ```
376 | void moveForwardSafely()
377 | {
378 | if (frontIsClear())
379 | {
380 | moveForward(); // This line is executed 0 or 1 times
381 | }
382 | }
383 | ```
384 | `while` re-checks the condition after the block is executed:
385 | ```
386 | void moveToWall()
387 | {
388 | while (frontIsClear())
389 | {
390 | moveForward(); // This line is executed 0 to 9 times
391 | }
392 | }
393 | ```
394 |
395 | ## Keyboard shortcuts
396 |
397 | | Windows | Effect | Macintosh |
398 | | -----------: | :-------------------------: | ---------------- |
399 | | F1 | `moveForward();` | F1 |
400 | | F2 | `turnLeft();` | F2 |
401 | | F3 | `turnAround();` | F3 |
402 | | F4 | `turnRight();` | F4 |
403 | | F5 | `pickBeeper();` | F5 |
404 | | F6 | `dropBeeper();` | F6 |
405 | | F7 | `onBeeper()` | F7 |
406 | | F8 | `beeperAhead()` | F8 |
407 | | F9 | `leftIsClear()` | F9 |
408 | | F10 | `frontIsClear()` | F10 |
409 | | F11 | `rightIsClear()` | F11 |
410 | | F12 | start
step into
reset | F12 |
411 | | Tab
Enter | auto-indent | Tab
Enter |
412 | | Ctrl Space | auto-complete | Control (Shift) Space |
413 | | Ctrl Alt R | rename command | Command Option R |
414 | | Ctrl D | delete line | Command D |
415 | | Ctrl C | copy | Command C |
416 | | Ctrl X | cut | Command X |
417 | | Ctrl V | paste | Command V |
418 | | Ctrl Z | undo | Command Z |
419 | | Ctrl Y | redo | Command Y |
420 |
--------------------------------------------------------------------------------
/opengl:
--------------------------------------------------------------------------------
1 | java -jar -Dsun.java2d.opengl=True karel.jar
2 |
--------------------------------------------------------------------------------
/pom.xml:
--------------------------------------------------------------------------------
1 |
3 | 4.0.0
4 |
5 | fredoverflow
6 | karel
7 | 0.1.0-SNAPSHOT
8 |
9 |
10 | UTF-8
11 | official
12 | 2.0.21
13 | 1.8
14 | 1.8
15 | 1.8
16 | MainKt
17 | ${java.home}/lib/rt.jar
18 |
19 |
20 |
21 |
22 | java-modules
23 |
24 | [9,)
25 |
26 |
27 | ${java.home}/jmods(!**.jar;!module-info.class)
28 |
29 |
30 |
31 |
32 |
33 |
34 | fredoverflow
35 | freditor
36 | 0.1.0-SNAPSHOT
37 |
38 |
39 |
40 | org.jetbrains.kotlin
41 | kotlin-stdlib
42 | ${kotlin.version}
43 |
44 |
45 |
46 | junit
47 | junit
48 | 4.13.1
49 | test
50 |
51 |
52 |
53 |
54 | src/main/kotlin
55 | src/test/kotlin
56 |
57 |
58 |
59 | org.jetbrains.kotlin
60 | kotlin-maven-plugin
61 | ${kotlin.version}
62 |
63 |
64 | compile
65 |
66 | compile
67 |
68 |
69 |
70 |
71 | test-compile
72 |
73 | test-compile
74 |
75 |
76 |
77 |
78 |
79 |
80 | org.apache.maven.plugins
81 | maven-jar-plugin
82 | 3.2.0
83 |
84 |
85 |
86 | ${main.class}
87 |
88 |
89 |
90 |
91 |
92 |
93 | com.github.wvengen
94 | proguard-maven-plugin
95 | 2.6.1
96 |
97 |
98 | package
99 |
100 | proguard
101 |
102 |
103 |
104 |
105 | true
106 | META-INF/MANIFEST.MF,!META-INF/**,!**.kotlin_*
107 | ${project.artifactId}.jar
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
--------------------------------------------------------------------------------
/sloc:
--------------------------------------------------------------------------------
1 | # sloc: count significant lines of code
2 | # INsignificant lines contain only spaces and/or braces
3 |
4 | # $1 directory
5 | # $2 extension
6 | function countInDirectory {
7 | lines=$(find "src/$1" -name "*.$2" -exec grep -vP "^[{ }]*\r?$" {} + | wc -l)
8 | if [ $lines -gt 0 ]
9 | then
10 | printf "$1: $lines\n"
11 | fi
12 | }
13 |
14 | # $1 language
15 | # $2 extension
16 | function countForLanguage {
17 | printf "$1\n"
18 | printf "==========\n"
19 | countInDirectory main $2
20 | countInDirectory test $2
21 | printf "\n"
22 | }
23 |
24 | countForLanguage "Kotlin" "kt"
25 |
--------------------------------------------------------------------------------
/src/main/kotlin/Main.kt:
--------------------------------------------------------------------------------
1 | import freditor.SwingConfig
2 | import gui.MainHandler
3 | import java.awt.EventQueue
4 |
5 | fun main() {
6 | SwingConfig.nimbusWithDefaultFont(SwingConfig.SANS_SERIF_PLAIN_16)
7 | EventQueue.invokeLater(::MainHandler)
8 | }
9 |
--------------------------------------------------------------------------------
/src/main/kotlin/common/Diagnostic.kt:
--------------------------------------------------------------------------------
1 | package common
2 |
3 | class Diagnostic(val position: Int, override val message: String) : Exception(message)
4 |
--------------------------------------------------------------------------------
/src/main/kotlin/common/Lists.kt:
--------------------------------------------------------------------------------
1 | package common
2 |
3 | fun List.subList(fromIndex: Int): List {
4 | return subList(fromIndex, size)
5 | }
6 |
--------------------------------------------------------------------------------
/src/main/kotlin/gui/Autocompletion.kt:
--------------------------------------------------------------------------------
1 | package gui
2 |
3 | private val command = Regex("""\bvoid\s+(\p{javaJavaIdentifierStart}\p{javaJavaIdentifierPart}*)""")
4 | private val reverse = Regex(""";?\)?\(?\p{javaJavaIdentifierPart}*\p{javaJavaIdentifierStart}""")
5 |
6 | fun autocompleteCall(sourceCode: String, lineBeforeSelection: String): List {
7 | val suffixes = fittingSuffixes(sourceCode, lineBeforeSelection)
8 | val lcp = longestCommonPrefix(suffixes)
9 | return if (lcp.isEmpty()) {
10 | suffixes
11 | } else {
12 | listOf(lcp)
13 | }
14 | }
15 |
16 | private fun fittingSuffixes(sourceCode: String, lineBeforeSelection: String): List {
17 | val prefix = reverse.find(lineBeforeSelection.reversed())?.value.orEmpty().reversed()
18 | val prefixLength = prefix.length
19 |
20 | return command
21 | .findAll(sourceCode)
22 | .map { it.groups[1]!!.value + "();" }
23 | .filter { it.length > prefixLength && it.startsWith(prefix) }
24 | .map { it.substring(prefixLength) }
25 | .toList()
26 | }
27 |
28 | private fun longestCommonPrefix(strings: List): String {
29 | val shortestString = strings.minByOrNull(String::length) ?: ""
30 | shortestString.forEachIndexed { index, ch ->
31 | if (!strings.all { command -> command[index] == ch }) {
32 | return shortestString.substring(0, index)
33 | }
34 | }
35 | return shortestString
36 | }
37 |
--------------------------------------------------------------------------------
/src/main/kotlin/gui/BufferedImages.kt:
--------------------------------------------------------------------------------
1 | package gui
2 |
3 | import java.awt.image.BufferedImage
4 |
5 | fun BufferedImage.scaled(scale: Int): BufferedImage {
6 | require(scale >= 2) { "scale $scale too small" }
7 |
8 | val srcWidth = width
9 | val srcHeight = height
10 | val src = IntArray(srcWidth * srcHeight)
11 | getRGB(0, 0, srcWidth, srcHeight, src, 0, srcWidth)
12 |
13 | val dstWidth = srcWidth * scale
14 | val dstHeight = srcHeight * scale
15 | val dst = IntArray(dstWidth * dstHeight)
16 |
17 | var j = 0
18 | for (y in 0 until dstHeight) {
19 | val i = (y / scale) * srcHeight
20 | for (x in 0 until dstWidth) {
21 | dst[j++] = src[i + (x / scale)]
22 | }
23 | }
24 |
25 | val scaled = BufferedImage(dstWidth, dstHeight, BufferedImage.TYPE_INT_ARGB)
26 | scaled.setRGB(0, 0, dstWidth, dstHeight, dst, 0, dstWidth)
27 | return scaled
28 | }
29 |
30 | fun BufferedImage.rotatedCounterclockwise(): BufferedImage {
31 |
32 | val srcWidth = width
33 | val srcHeight = height
34 | val src = IntArray(srcWidth * srcHeight)
35 | val dst = IntArray(srcHeight * srcWidth)
36 | getRGB(0, 0, srcWidth, srcHeight, src, 0, srcWidth)
37 |
38 | var j = 0
39 | for (x in srcWidth - 1 downTo 0) {
40 | var i = x
41 | for (y in 0 until srcHeight) {
42 | dst[j++] = src[i]
43 | i += srcWidth
44 | }
45 | }
46 |
47 | val rotated = BufferedImage(srcHeight, srcWidth, BufferedImage.TYPE_INT_ARGB)
48 | rotated.setRGB(0, 0, srcHeight, srcWidth, dst, 0, srcHeight)
49 | return rotated
50 | }
51 |
--------------------------------------------------------------------------------
/src/main/kotlin/gui/BytecodeFlexer.kt:
--------------------------------------------------------------------------------
1 | package gui
2 |
3 | import freditor.FlexerState
4 | import freditor.FlexerState.EMPTY
5 | import freditor.FlexerState.THIS
6 | import freditor.FlexerStateBuilder
7 | import freditor.persistent.ChampMap
8 |
9 | object BytecodeFlexer : freditor.Flexer() {
10 | private val NUMBER_TAIL = FlexerState("09af", THIS)
11 | private val NUMBER_HEAD = NUMBER_TAIL.head()
12 |
13 | private val START = FlexerStateBuilder()
14 | .set('\n', NEWLINE)
15 | .set(' ', SPACE_HEAD)
16 | .set("09af", NUMBER_HEAD)
17 | .build()
18 | .verbatim(
19 | EMPTY, "@", "CODE", "MNEMONIC",
20 | "RET",
21 | "MOVE", "TRNL", "TRNA", "TRNR", "PICK", "DROP",
22 | "BEEP", "HEAD", "LCLR", "FCLR", "RCLR",
23 | "PUSH", "LOOP", "CALL", "JUMP", "ELSE", "THEN",
24 | )
25 | .setDefault(ERROR)
26 |
27 | override fun start(): FlexerState = START
28 |
29 | override fun pickColorForLexeme(previousState: FlexerState, endState: FlexerState): Int {
30 | val colors = if (previousState === NEWLINE) afterNewline else lexemeColors
31 | return colors[endState] ?: 0x000000
32 | }
33 |
34 | private val lexemeColors = ChampMap.of(ERROR, 0x808080)
35 | .put(NUMBER_HEAD, NUMBER_TAIL, 0x6400c8)
36 | .put(START.read("@", "CODE", "MNEMONIC"), 0x808080)
37 | .put(START.read("BEEP", "HEAD", "LCLR", "FCLR", "RCLR", "PUSH"), 0x000080)
38 | .put(START.read("RET", "LOOP", "CALL", "JUMP", "ELSE", "THEN"), 0x400000)
39 |
40 | private val afterNewline = lexemeColors
41 | .put(NUMBER_HEAD, NUMBER_TAIL, 0x808080)
42 | }
43 |
--------------------------------------------------------------------------------
/src/main/kotlin/gui/BytecodeTable.kt:
--------------------------------------------------------------------------------
1 | package gui
2 |
3 | import common.subList
4 | import freditor.FreditorUI
5 | import freditor.Indenter
6 | import vm.ENTRY_POINT
7 | import vm.Instruction
8 |
9 | class BytecodeTable : FreditorUI(BytecodeFlexer, Indenter.instance, 18, 1) {
10 | fun setProgram(program: List) {
11 | load(program.subList(ENTRY_POINT).withIndex()
12 | .joinToString(prefix = " @ CODE MNEMONIC\n", separator = "\n") { (row, instruction) ->
13 | "%3x %4x %s".format(row + ENTRY_POINT, instruction.bytecode, instruction.mnemonic())
14 | })
15 | }
16 |
17 | fun highlightLine(line: Int) {
18 | val row = line - ENTRY_POINT + 1
19 | setCursorTo(row, 9)
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/src/main/kotlin/gui/ControlPanel.kt:
--------------------------------------------------------------------------------
1 | package gui
2 |
3 | import freditor.Fronts
4 | import logic.Problem
5 | import javax.swing.*
6 |
7 | fun T.sansSerif(): T {
8 | this.font = Fronts.sansSerif
9 | return this
10 | }
11 |
12 | class ControlPanel(problems: List) : JPanel() {
13 |
14 | val randomize = JButton("\uD83C\uDFB2").sansSerif().apply {
15 | isEnabled = false
16 | }
17 |
18 | val goal = JButton("goal").sansSerif()
19 |
20 | val problemPicker = JComboBox(problems.toTypedArray()).sansSerif().apply {
21 | maximumSize = minimumSize
22 | maximumRowCount = 20
23 | }
24 |
25 | val startStopReset = JButton("start").sansSerif()
26 |
27 | val check = JButton("\uD83D\uDC1C").sansSerif().apply {
28 | toolTipText = "check every ${problems[0].check.singular}"
29 | }
30 |
31 | val stepInto = JButton("step into (F12)").sansSerif()
32 | val stepOver = JButton("step over").sansSerif()
33 | val stepReturn = JButton("step return").sansSerif()
34 |
35 | private fun setEnabledStepButtons(enabled: Boolean) {
36 | stepInto.isEnabled = enabled
37 | stepOver.isEnabled = enabled
38 | stepReturn.isEnabled = enabled
39 | }
40 |
41 | val pause = JButton("\u23F8").sansSerif()
42 | val slider = JSlider(0, 11, 2).sansSerif()
43 | val fast = JButton("\u23E9").sansSerif()
44 |
45 | fun delayLogarithm(): Int {
46 | return if (slider.value == 0) -1 else slider.maximum - slider.value
47 | }
48 |
49 | init {
50 | layout = BoxLayout(this, BoxLayout.Y_AXIS)
51 | setEnabledStepButtons(false)
52 | add(horizontalBoxPanel(randomize, goal, problemPicker, startStopReset, check))
53 | add(Box.createVerticalStrut(16))
54 | add(horizontalBoxPanel(stepInto, stepOver, stepReturn))
55 | add(Box.createVerticalStrut(16))
56 | add(horizontalBoxPanel(pause, slider, fast))
57 | }
58 |
59 | fun executionStarted() {
60 | randomize.isEnabled = false
61 | goal.isEnabled = false
62 | problemPicker.isEnabled = false
63 | startStopReset.text = "stop"
64 | check.isEnabled = false
65 | setEnabledStepButtons(true)
66 | }
67 |
68 | fun executionFinished(isRandom: Boolean) {
69 | randomize.isEnabled = isRandom
70 | goal.isEnabled = true
71 | problemPicker.isEnabled = true
72 | startStopReset.text = "reset"
73 | check.isEnabled = true
74 | setEnabledStepButtons(false)
75 | }
76 |
77 | fun isRunning(): Boolean {
78 | return startStopReset.text === "stop"
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/src/main/kotlin/gui/Editor.kt:
--------------------------------------------------------------------------------
1 | package gui
2 |
3 | import freditor.Freditor
4 | import freditor.FreditorUI
5 | import syntax.lexer.keywords
6 | import vm.Instruction
7 | import vm.builtinCommands
8 | import java.awt.*
9 | import java.awt.event.KeyEvent
10 | import java.awt.geom.Line2D
11 | import javax.swing.JOptionPane
12 |
13 | private val NAME = Regex("""[A-Z_a-z][0-9A-Z_a-z]*""")
14 |
15 | class Editor(freditor: Freditor) : FreditorUI(freditor, 60, 1) {
16 | init {
17 | onKeyPressed { event ->
18 | when (event.keyCode) {
19 | KeyEvent.VK_F1 -> insertCommand("moveForward();")
20 | KeyEvent.VK_F2 -> insertCommand("turnLeft();")
21 | KeyEvent.VK_F3 -> insertCommand("turnAround();")
22 | KeyEvent.VK_F4 -> insertCommand("turnRight();")
23 | KeyEvent.VK_F5 -> insertCommand("pickBeeper();")
24 | KeyEvent.VK_F6 -> insertCommand("dropBeeper();")
25 |
26 | KeyEvent.VK_F7 -> insert("onBeeper()")
27 | KeyEvent.VK_F8 -> insert("beeperAhead()")
28 | KeyEvent.VK_F9 -> insert("leftIsClear()")
29 | KeyEvent.VK_F10 -> insert("frontIsClear()")
30 | KeyEvent.VK_F11 -> insert("rightIsClear()")
31 |
32 | KeyEvent.VK_SPACE -> if (event.isControlDown) {
33 | autocompleteCall()
34 | }
35 |
36 | KeyEvent.VK_R -> if (isControlRespectivelyCommandDown(event) && event.isAltDown) {
37 | renameCommand()
38 | }
39 | }
40 | }
41 | }
42 |
43 | private fun insertCommand(command: String) {
44 | if (lineIsBlankBefore(selectionStart())) {
45 | insert(command)
46 | } else {
47 | simulateEnter()
48 | insert(command)
49 | // Remove the commit between simulateEnter and insertString,
50 | // effectively committing both changes as a single commit
51 | uncommit()
52 | }
53 | }
54 |
55 | private fun autocompleteCall() {
56 | val suffixes = autocompleteCall(text, lineBeforeSelection)
57 | if (suffixes.size == 1) {
58 | insert(suffixes[0])
59 | } else {
60 | println(suffixes.sorted().joinToString(", "))
61 | }
62 | }
63 |
64 | private fun renameCommand() {
65 | val oldName = symbolNearCursor(Flexer.IDENTIFIER_TAIL)
66 | if (oldName.isEmpty() || oldName in keywords || oldName in builtinCommands) return
67 |
68 | val input = JOptionPane.showInputDialog(
69 | this,
70 | oldName,
71 | "rename command",
72 | JOptionPane.QUESTION_MESSAGE,
73 | null,
74 | null,
75 | oldName
76 | ) ?: return
77 |
78 | val newName = input.toString().trim()
79 | if (!NAME.matches(newName) || newName in keywords || newName in builtinCommands) return
80 |
81 | replace("""\b$oldName(\s*\(\s*\))""", "$newName$1")
82 | }
83 |
84 | private val lines = ArrayList()
85 |
86 | private val frontHeight = FreditorUI.frontHeight
87 | private val frontWidth = FreditorUI.frontWidth
88 | private val thickness = frontWidth - 2.0f
89 |
90 | private val stroke = BasicStroke(thickness, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND)
91 | private val color = Color(0x40ff0000, true)
92 |
93 | fun push(callInstruction: Instruction, returnInstruction: Instruction) {
94 | val x = 0.5 * thickness + lines.size * frontWidth
95 | val y1 = (lineOfPosition(callInstruction.position) + 0.5) * frontHeight
96 | val y2 = (lineOfPosition(returnInstruction.position) + 0.5) * frontHeight
97 | lines.add(Line2D.Double(x, y1, x, y2))
98 | repaint()
99 | }
100 |
101 | fun pop() {
102 | lines.removeAt(lines.lastIndex)
103 | repaint()
104 | }
105 |
106 | fun clearStack() {
107 | lines.clear()
108 | repaint()
109 | }
110 |
111 | override fun paint(graphics: Graphics) {
112 | super.paint(graphics)
113 | paintCallStack(graphics as Graphics2D)
114 | }
115 |
116 | private fun paintCallStack(graphics: Graphics2D) {
117 | graphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON)
118 | graphics.stroke = stroke
119 | graphics.color = color
120 | graphics.translate(0, -firstVisibleLine() * frontHeight)
121 | if (lines.isNotEmpty()) {
122 | lines.forEach(graphics::draw)
123 | graphics.draw(lines.last())
124 | }
125 | }
126 | }
127 |
--------------------------------------------------------------------------------
/src/main/kotlin/gui/Flexer.kt:
--------------------------------------------------------------------------------
1 | package gui
2 |
3 | import freditor.FlexerState
4 | import freditor.FlexerState.EMPTY
5 | import freditor.FlexerState.THIS
6 | import freditor.FlexerStateBuilder
7 | import freditor.persistent.ChampMap
8 |
9 | object Flexer : freditor.Flexer() {
10 | private val SLASH_SLASH = FlexerState('\n', null).setDefault(THIS)
11 | private val SLASH_ASTERISK___ASTERISK_SLASH = EMPTY.tail()
12 | private val SLASH_ASTERISK___ASTERISK = FlexerState('*', THIS, '/', SLASH_ASTERISK___ASTERISK_SLASH)
13 | private val SLASH_ASTERISK = FlexerState('*', SLASH_ASTERISK___ASTERISK).setDefault(THIS)
14 |
15 | init {
16 | SLASH_ASTERISK___ASTERISK.setDefault(SLASH_ASTERISK)
17 | }
18 |
19 | private val NUMBER_TAIL = FlexerState("09", THIS)
20 | private val NUMBER_HEAD = NUMBER_TAIL.head()
21 |
22 | val IDENTIFIER_TAIL = FlexerState("09AZ__az", THIS)
23 | private val IDENTIFIER_HEAD = IDENTIFIER_TAIL.head()
24 |
25 | private val START = FlexerStateBuilder()
26 | .set('(', OPENING_PAREN)
27 | .set(')', CLOSING_PAREN)
28 | .set('{', OPENING_BRACE)
29 | .set('}', CLOSING_BRACE)
30 | .set('\n', NEWLINE)
31 | .set(' ', SPACE_HEAD)
32 | .set('/', FlexerState('*', SLASH_ASTERISK, '/', SLASH_SLASH).head())
33 | .set("09", NUMBER_HEAD)
34 | .set("AZ__az", IDENTIFIER_HEAD)
35 | .build()
36 | .verbatim(IDENTIFIER_TAIL, "else", "if", "repeat", "void", "while")
37 | .verbatim(EMPTY, "!", "&&", ";", "||")
38 | .setDefault(ERROR)
39 |
40 | override fun start(): FlexerState = START
41 |
42 | override fun pickColorForLexeme(previousState: FlexerState, endState: FlexerState): Int {
43 | return lexemeColors[endState] ?: 0x000000
44 | }
45 |
46 | private val lexemeColors = ChampMap.of(ERROR, 0x808080)
47 | .put(START.read("/", "&", "|"), 0x808080)
48 | .put(SLASH_SLASH, SLASH_ASTERISK, SLASH_ASTERISK___ASTERISK, SLASH_ASTERISK___ASTERISK_SLASH, 0x008000)
49 | .put(NUMBER_HEAD, NUMBER_TAIL, 0x6400c8)
50 | .put(START.read("else", "if", "repeat", "while"), 0x0000ff)
51 | .put(START.read("void"), 0x008080)
52 | .put(START.read("(", ")", "{", "}"), 0xff0000)
53 | .put(START.read("!", "&&", "||"), 0x804040)
54 | }
55 |
--------------------------------------------------------------------------------
/src/main/kotlin/gui/MainDesign.kt:
--------------------------------------------------------------------------------
1 | package gui
2 |
3 | import freditor.*
4 | import logic.Problem
5 | import logic.World
6 | import java.awt.BorderLayout
7 | import javax.swing.Box
8 | import javax.swing.JFrame
9 | import javax.swing.border.EmptyBorder
10 |
11 | abstract class MainDesign(world: World) : JFrame() {
12 |
13 | val controlPanel = ControlPanel(Problem.problems)
14 |
15 | val worldPanel = WorldPanel(world)
16 |
17 | val story = FreditorUI(Flexer, JavaIndenter.instance, 33, 5)
18 |
19 | protected abstract fun createEditor(freditor: Freditor): Editor
20 |
21 | val tabbedEditors = TabbedEditors("karel", Flexer, JavaIndenter.instance, ::createEditor)
22 |
23 | val editor
24 | get() = tabbedEditors.selectedEditor as Editor
25 |
26 | val virtualMachinePanel = VirtualMachinePanel()
27 |
28 | init {
29 | title = "karel version ${Release.compilationDate(MainDesign::class.java)} @ ${editor.file.parent}"
30 | val left = verticalBoxPanel(
31 | controlPanel,
32 | Box.createVerticalStrut(16),
33 | worldPanel,
34 | Box.createVerticalStrut(16),
35 | story,
36 | )
37 | left.border = EmptyBorder(16, 16, 16, 16)
38 | super.add(left, BorderLayout.WEST)
39 | super.add(tabbedEditors.tabs, BorderLayout.CENTER)
40 | super.add(virtualMachinePanel, BorderLayout.EAST)
41 | super.pack()
42 | isVisible = true
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/src/main/kotlin/gui/MainFlow.kt:
--------------------------------------------------------------------------------
1 | package gui
2 |
3 | import common.Diagnostic
4 | import logic.*
5 | import syntax.lexer.Lexer
6 | import syntax.parser.Parser
7 | import syntax.parser.program
8 | import vm.*
9 | import javax.swing.Timer
10 |
11 | const val CHECK_TOTAL_NS = 2_000_000_000L
12 | const val CHECK_REPAINT_NS = 100_000_000L
13 |
14 | const val COMPARE = "mouse enter/exit (or click) world to compare"
15 |
16 | abstract class MainFlow : MainDesign(Problem.karelsFirstProgram.randomWorld()) {
17 |
18 | val currentProblem: Problem
19 | get() = controlPanel.problemPicker.selectedItem as Problem
20 |
21 | fun delay(): Int {
22 | val logarithm = controlPanel.delayLogarithm()
23 | return if (logarithm < 0) logarithm else 1.shl(logarithm)
24 | }
25 |
26 | var initialWorld: World = worldPanel.world
27 |
28 | var virtualMachine = VirtualMachine(emptyArray(), initialWorld)
29 |
30 | val timer = Timer(delay()) {
31 | tryStep(::stepInto)
32 | }
33 |
34 | fun executeGoal(goal: String) {
35 | start(createGoalInstructions(goal))
36 | }
37 |
38 | fun checkAgainst(goal: String) {
39 | editor.isolateBraces()
40 | editor.indent()
41 | editor.saveWithBackup()
42 | editor.clearDiagnostics()
43 | try {
44 | val lexer = Lexer(editor.text)
45 | val parser = Parser(lexer)
46 | parser.program()
47 | val main = parser.sema.command(currentProblem.name)
48 | if (main != null) {
49 | val instructions = Emitter(parser.sema, true).emit(main)
50 | virtualMachinePanel.setProgram(instructions)
51 | virtualMachinePanel.update(null, ENTRY_POINT)
52 |
53 | tryCheck(instructions.toTypedArray(), createGoalInstructions(goal).toTypedArray())
54 | } else {
55 | editor.setCursorTo(editor.length())
56 | showDiagnostic("void ${currentProblem.name}() not found")
57 | }
58 | } catch (diagnostic: Diagnostic) {
59 | showDiagnostic(diagnostic)
60 | }
61 | }
62 |
63 | private fun tryCheck(instructions: Array, goalInstructions: Array) {
64 | try {
65 | val successMessage = check(instructions, goalInstructions)
66 | reportFirstRedundantCondition(instructions)
67 | update()
68 | showDiagnostic(successMessage)
69 | } catch (diagnostic: Diagnostic) {
70 | update()
71 | showDiagnostic(diagnostic)
72 | }
73 | }
74 |
75 | private fun check(instructions: Array, goalInstructions: Array): String {
76 |
77 | val start = System.nanoTime()
78 | var nextRepaint = CHECK_REPAINT_NS
79 |
80 | val worlds = currentProblem.randomWorlds().iterator()
81 | var worldCounter = 0
82 |
83 | while (true) {
84 | initialWorld = worlds.next()
85 | checkOneWorld(instructions, goalInstructions)
86 | ++worldCounter
87 |
88 | if (!worlds.hasNext()) {
89 | return if (currentProblem.numWorlds == ONE) {
90 | "OK: every ${currentProblem.check.singular} matches the goal :-)"
91 | } else {
92 | "OK: checked all ${currentProblem.numWorlds} possible worlds :-)"
93 | }
94 | }
95 |
96 | val elapsed = System.nanoTime() - start
97 |
98 | if (elapsed >= CHECK_TOTAL_NS) {
99 | return if (currentProblem.numWorlds == UNKNOWN) {
100 | "OK: checked $worldCounter random worlds :-)"
101 | } else {
102 | "OK: checked $worldCounter random worlds :-)\n from ${currentProblem.numWorlds} possible worlds"
103 | }
104 | }
105 |
106 | if (elapsed >= nextRepaint) {
107 | worldPanel.world = initialWorld
108 | worldPanel.paintImmediately(0, 0, worldPanel.width, worldPanel.height)
109 | nextRepaint += CHECK_REPAINT_NS
110 | }
111 | }
112 | }
113 |
114 | private fun checkOneWorld(instructions: Array, goalInstructions: Array) {
115 | val goalWorlds = ArrayList(200)
116 | createVirtualMachine(goalInstructions, goalWorlds::add)
117 | try {
118 | virtualMachine.executeGoalProgram()
119 | } catch (_: VirtualMachine.Finished) {
120 | }
121 | val finalGoalWorld = virtualMachine.world
122 | var index = 0
123 | val size = goalWorlds.size
124 |
125 | createVirtualMachine(instructions) { world ->
126 | if (index == size) {
127 | worldPanel.antWorld = finalGoalWorld
128 | virtualMachine.error("extra ${currentProblem.check.singular}\n\n$COMPARE")
129 | }
130 | val goalWorld = goalWorlds[index++]
131 | if (!goalWorld.equalsIgnoringDirection(world)) {
132 | worldPanel.antWorld = goalWorld
133 | virtualMachine.error("wrong ${currentProblem.check.singular}\n\n$COMPARE")
134 | }
135 | }
136 |
137 | try {
138 | virtualMachine.executeUserProgram()
139 | } catch (_: VirtualMachine.Finished) {
140 | } catch (error: KarelError) {
141 | virtualMachine.error(error.message)
142 | }
143 | if (index < size && !finalGoalWorld.equalsIgnoringDirection(virtualMachine.world)) {
144 | worldPanel.antWorld = finalGoalWorld
145 | if (currentProblem.numWorlds == ONE) {
146 | val missing = size - index
147 | virtualMachine.error("missing $missing ${currentProblem.check.numerus(missing)}\n\n$COMPARE")
148 | } else {
149 | virtualMachine.error("missing ${currentProblem.check.plural}\n\n$COMPARE")
150 | }
151 | }
152 | }
153 |
154 | private fun createVirtualMachine(instructions: Array, callback: (World) -> Unit) {
155 | virtualMachine = VirtualMachine(
156 | instructions, initialWorld,
157 | onPickDrop = callback,
158 | onMove = callback.takeIf { Check.EVERY_PICK_DROP_MOVE == currentProblem.check },
159 | )
160 | }
161 |
162 | private fun reportFirstRedundantCondition(instructions: Array) {
163 | for (index in ENTRY_POINT until instructions.size) {
164 | val instruction = instructions[index]
165 | when (instruction.bytecode) {
166 | ON_BEEPER_FALSE, BEEPER_AHEAD_FALSE, LEFT_IS_CLEAR_FALSE, FRONT_IS_CLEAR_FALSE, RIGHT_IS_CLEAR_FALSE -> {
167 | instruction.error("condition was always false")
168 | }
169 |
170 | ON_BEEPER_TRUE, BEEPER_AHEAD_TRUE, LEFT_IS_CLEAR_TRUE, FRONT_IS_CLEAR_TRUE, RIGHT_IS_CLEAR_TRUE -> {
171 | instruction.error("condition was always true")
172 | }
173 | }
174 | }
175 | }
176 |
177 | fun parseAndExecute() {
178 | editor.isolateBraces()
179 | editor.indent()
180 | editor.saveWithBackup()
181 | editor.clearDiagnostics()
182 | try {
183 | val lexer = Lexer(editor.text)
184 | val parser = Parser(lexer)
185 | parser.program()
186 | val main = parser.sema.command(currentProblem.name)
187 | if (main != null) {
188 | val instructions = Emitter(parser.sema, false).emit(main)
189 | start(instructions)
190 | } else {
191 | editor.setCursorTo(editor.length())
192 | showDiagnostic("void ${currentProblem.name}() not found")
193 | }
194 | } catch (diagnostic: Diagnostic) {
195 | showDiagnostic(diagnostic)
196 | }
197 | }
198 |
199 | fun start(instructions: List) {
200 | val compiledFromSource = instructions[ENTRY_POINT].compiledFromSource
201 | tabbedEditors.tabs.isEnabled = false
202 | virtualMachinePanel.setProgram(instructions)
203 | virtualMachine = VirtualMachine(
204 | instructions.toTypedArray(), initialWorld,
205 | onCall = editor::push.takeIf { compiledFromSource },
206 | onReturn = editor::pop.takeIf { compiledFromSource },
207 | )
208 | controlPanel.executionStarted()
209 | update()
210 | if (delay() >= 0) {
211 | timer.start()
212 | }
213 | }
214 |
215 | fun stop() {
216 | timer.stop()
217 | controlPanel.executionFinished(currentProblem.isRandom)
218 | tabbedEditors.tabs.isEnabled = true
219 | editor.clearStack()
220 | editor.requestFocusInWindow()
221 | }
222 |
223 | fun update() {
224 | val instruction = virtualMachine.currentInstruction
225 | val position = instruction.position
226 | if (position > 0) {
227 | editor.setCursorTo(position)
228 | }
229 | virtualMachinePanel.update(virtualMachine.stack, virtualMachine.pc)
230 | worldPanel.world = virtualMachine.world
231 | worldPanel.repaint()
232 | }
233 |
234 | fun stepInto() {
235 | virtualMachine.stepInto(virtualMachinePanel.isVisible)
236 | }
237 |
238 | inline fun tryStep(step: () -> Unit) {
239 | try {
240 | step()
241 | update()
242 | } catch (_: VirtualMachine.Finished) {
243 | stop()
244 | update()
245 | } catch (error: KarelError) {
246 | stop()
247 | update()
248 | showDiagnostic(error.message)
249 | }
250 | }
251 |
252 | fun showDiagnostic(diagnostic: Diagnostic) {
253 | editor.setCursorTo(diagnostic.position)
254 | showDiagnostic(diagnostic.message)
255 | }
256 |
257 | fun showDiagnostic(message: String) {
258 | editor.requestFocusInWindow()
259 | editor.showDiagnostic(message)
260 | }
261 |
262 | init {
263 | story.load(currentProblem.story)
264 | if (editor.length() == 0) {
265 | editor.load(helloWorld)
266 | }
267 | editor.requestFocusInWindow()
268 | }
269 | }
270 |
271 | const val helloWorld = """/*
272 | F1 = moveForward();
273 | F2 = turnLeft();
274 | F3 = turnAround();
275 | F4 = turnRight();
276 | F5 = pickBeeper();
277 | F6 = dropBeeper();
278 | */
279 |
280 | void karelsFirstProgram()
281 | {
282 | // your code here
283 |
284 | }
285 | """
286 |
--------------------------------------------------------------------------------
/src/main/kotlin/gui/MainHandler.kt:
--------------------------------------------------------------------------------
1 | package gui
2 |
3 | import freditor.Freditor
4 | import logic.Problem
5 | import java.awt.event.KeyEvent
6 | import java.awt.event.MouseAdapter
7 | import java.awt.event.MouseEvent
8 | import java.util.function.Consumer
9 | import javax.swing.SwingUtilities
10 | import kotlin.streams.asSequence
11 |
12 | class MainHandler : MainFlow() {
13 | init {
14 | controlPanel.randomize.addActionListener {
15 | controlPanel.startStopReset.text = "start"
16 |
17 | initialWorld = currentProblem.randomWorld()
18 | virtualMachine.world = initialWorld
19 | worldPanel.world = initialWorld
20 | worldPanel.antWorld = null
21 | worldPanel.repaint()
22 |
23 | editor.requestFocusInWindow()
24 | }
25 |
26 | controlPanel.goal.addActionListener {
27 | worldPanel.antWorld = null
28 |
29 | executeGoal(currentProblem.goal)
30 | controlPanel.stepOver.isEnabled = false
31 | controlPanel.stepReturn.isEnabled = false
32 |
33 | editor.requestFocusInWindow()
34 | }
35 |
36 | controlPanel.problemPicker.addActionListener {
37 | controlPanel.startStopReset.text = "start"
38 | controlPanel.randomize.isEnabled = currentProblem.isRandom
39 | controlPanel.check.toolTipText = "check every ${currentProblem.check.singular}"
40 |
41 | initialWorld = currentProblem.randomWorld()
42 | virtualMachine.world = initialWorld
43 | worldPanel.world = initialWorld
44 | worldPanel.antWorld = null
45 | worldPanel.binaryLines = currentProblem.binaryLines
46 | worldPanel.repaint()
47 |
48 | story.load(currentProblem.story)
49 |
50 | val pattern = Regex("""\bvoid\s+(${currentProblem.name})\b""").toPattern()
51 |
52 | tabbedEditors.stream().asSequence()
53 | .minus(editor).plus(editor) // check current editor last
54 | .filter { it.setCursorTo(pattern, 1) }
55 | .lastOrNull()
56 | ?.let(tabbedEditors::selectEditor)
57 |
58 | editor.requestFocusInWindow()
59 | }
60 |
61 | controlPanel.startStopReset.addActionListener {
62 | when (controlPanel.startStopReset.text) {
63 | "start" -> parseAndExecute()
64 |
65 | "stop" -> stop()
66 |
67 | "reset" -> {
68 | controlPanel.startStopReset.text = "start"
69 |
70 | virtualMachine.world = initialWorld
71 | worldPanel.world = initialWorld
72 | worldPanel.antWorld = null
73 | worldPanel.repaint()
74 | }
75 | }
76 | editor.requestFocusInWindow()
77 | }
78 |
79 | controlPanel.check.addActionListener {
80 | controlPanel.startStopReset.text = "reset"
81 | worldPanel.antWorld = null
82 |
83 | checkAgainst(currentProblem.goal)
84 |
85 | editor.requestFocusInWindow()
86 | }
87 |
88 | controlPanel.stepInto.addActionListener {
89 | tryStep(::stepInto)
90 |
91 | editor.requestFocusInWindow()
92 | }
93 |
94 | controlPanel.stepOver.addActionListener {
95 | tryStep(virtualMachine::stepOver)
96 |
97 | editor.requestFocusInWindow()
98 | }
99 |
100 | controlPanel.stepReturn.addActionListener {
101 | tryStep(virtualMachine::stepReturn)
102 |
103 | editor.requestFocusInWindow()
104 | }
105 |
106 | controlPanel.slider.addChangeListener {
107 | val d = delay()
108 | if (d < 0) {
109 | timer.stop()
110 | } else {
111 | timer.initialDelay = d
112 | timer.delay = d
113 | if (controlPanel.isRunning()) {
114 | timer.restart()
115 | }
116 | }
117 | editor.requestFocusInWindow()
118 | }
119 |
120 | var previousValue = controlPanel.slider.value
121 |
122 | controlPanel.pause.addActionListener {
123 | with(controlPanel.slider) {
124 | if (value != minimum) {
125 | if (value != maximum) {
126 | previousValue = value
127 | }
128 | value = minimum
129 | } else {
130 | value = previousValue
131 | }
132 | }
133 | }
134 |
135 | controlPanel.fast.addActionListener {
136 | with(controlPanel.slider) {
137 | if (value != maximum) {
138 | if (value != minimum) {
139 | previousValue = value
140 | }
141 | value = maximum
142 | } else {
143 | value = previousValue
144 | }
145 | }
146 | }
147 |
148 | worldPanel.addMouseListener(object : MouseAdapter() {
149 | override fun mouseClicked(event: MouseEvent) {
150 | if (!event.component.isEnabled) return
151 |
152 | if (SwingUtilities.isLeftMouseButton(event)) {
153 | if (worldPanel.antWorld == null) {
154 | val x = event.x / worldPanel.tileSize
155 | val y = event.y / worldPanel.tileSize
156 | val world = virtualMachine.world.toggleBeeper(x, y)
157 | virtualMachine.world = world
158 |
159 | worldPanel.world = world
160 | worldPanel.repaint()
161 | }
162 | }
163 | }
164 | })
165 |
166 | defaultCloseOperation = EXIT_ON_CLOSE
167 | tabbedEditors.saveOnExit(this)
168 | }
169 |
170 | override fun createEditor(freditor: Freditor): Editor {
171 | val editor = Editor(freditor)
172 |
173 | editor.onKeyPressed { event ->
174 | when (event.keyCode) {
175 | KeyEvent.VK_M -> if (event.isControlDown && event.isShiftDown) {
176 | virtualMachinePanel.isVisible = !virtualMachinePanel.isVisible
177 | }
178 |
179 | KeyEvent.VK_F12 -> {
180 | if (controlPanel.isRunning()) {
181 | tryStep(::stepInto)
182 | } else {
183 | controlPanel.startStopReset.doClick()
184 | }
185 | }
186 | }
187 | }
188 |
189 | editor.onRightClick = Consumer { lexeme ->
190 | if (controlPanel.problemPicker.isEnabled) {
191 | Problem.problems.firstOrNull {
192 | it.name == lexeme
193 | }?.let {
194 | controlPanel.problemPicker.selectedItem = it
195 | }
196 | }
197 | }
198 |
199 | return editor
200 | }
201 | }
202 |
--------------------------------------------------------------------------------
/src/main/kotlin/gui/StackTable.kt:
--------------------------------------------------------------------------------
1 | package gui
2 |
3 | import freditor.Fronts
4 | import vm.Stack
5 | import vm.forEach
6 | import vm.size
7 | import java.awt.Color
8 | import java.awt.Dimension
9 | import java.awt.Graphics
10 | import javax.swing.JComponent
11 |
12 | class StackTable : JComponent() {
13 | private var stack: Stack? = null
14 |
15 | init {
16 | resize(0)
17 | }
18 |
19 | private fun resize(rows: Int) {
20 | val dimension = Dimension(Fronts.front.width * 5, Fronts.front.height * rows)
21 | minimumSize = dimension
22 | maximumSize = dimension
23 | preferredSize = dimension
24 | revalidate()
25 | }
26 |
27 | fun setStack(newStack: Stack?) {
28 | if (newStack !== stack) {
29 | if (newStack.size != stack.size) {
30 | resize(newStack.size)
31 | }
32 | stack = newStack
33 | repaint()
34 | }
35 | }
36 |
37 | override fun paint(graphics: Graphics) {
38 | graphics.color = Color.WHITE
39 | graphics.fillRect(0, 0, width, height)
40 |
41 | var y = 0
42 | val frontHeight = Fronts.front.height
43 | val frontRight = 4 * Fronts.front.width
44 |
45 | stack.forEach { stack ->
46 | when (stack) {
47 | is Stack.ReturnAddress -> Fronts.front.drawHexRight(graphics, frontRight, y, stack.head, 0x808080)
48 |
49 | is Stack.LoopCounter -> Fronts.front.drawIntRight(graphics, frontRight, y, stack.head, 0x6400c8)
50 | }
51 | y += frontHeight
52 | }
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/src/main/kotlin/gui/SwingBridge.kt:
--------------------------------------------------------------------------------
1 | package gui
2 |
3 | import java.awt.Component
4 | import java.awt.event.KeyAdapter
5 | import java.awt.event.KeyEvent
6 | import javax.swing.BoxLayout
7 | import javax.swing.JComponent
8 | import javax.swing.JPanel
9 |
10 | fun horizontalBoxPanel(vararg components: Component) = JPanel().apply {
11 | layout = BoxLayout(this, BoxLayout.X_AXIS)
12 | components.forEach(::add)
13 | }
14 |
15 | fun verticalBoxPanel(vararg components: Component) = JPanel().apply {
16 | layout = BoxLayout(this, BoxLayout.Y_AXIS)
17 | components.forEach(::add)
18 | }
19 |
20 | inline fun JComponent.onKeyPressed(crossinline handler: (KeyEvent) -> Unit) {
21 | addKeyListener(object : KeyAdapter() {
22 | override fun keyPressed(event: KeyEvent) {
23 | handler(event)
24 | }
25 | })
26 | }
27 |
--------------------------------------------------------------------------------
/src/main/kotlin/gui/Toolkits.kt:
--------------------------------------------------------------------------------
1 | package gui
2 |
3 | import java.awt.Toolkit.getDefaultToolkit
4 |
5 | val screenHeight: Int
6 | get() = getDefaultToolkit().screenSize.height
7 |
8 | fun flushGraphicsBuffers() {
9 | getDefaultToolkit().sync()
10 | }
11 |
--------------------------------------------------------------------------------
/src/main/kotlin/gui/VirtualMachinePanel.kt:
--------------------------------------------------------------------------------
1 | package gui
2 |
3 | import vm.Instruction
4 | import vm.Stack
5 | import javax.swing.Box
6 | import javax.swing.BoxLayout
7 | import javax.swing.JPanel
8 |
9 | class VirtualMachinePanel : JPanel() {
10 |
11 | private val stackTable = StackTable()
12 | private val bytecodeTable = BytecodeTable()
13 |
14 | init {
15 | layout = BoxLayout(this, BoxLayout.X_AXIS)
16 | add(verticalBoxPanel(Box.createVerticalGlue(), stackTable))
17 | add(bytecodeTable)
18 | isVisible = false
19 | }
20 |
21 | fun setProgram(program: List) {
22 | bytecodeTable.setProgram(program)
23 | }
24 |
25 | fun update(stack: Stack?, pc: Int) {
26 | stackTable.setStack(stack)
27 | bytecodeTable.highlightLine(pc)
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/src/main/kotlin/gui/WorldPanel.kt:
--------------------------------------------------------------------------------
1 | package gui
2 |
3 | import freditor.Fronts
4 | import logic.World
5 |
6 | import java.awt.Dimension
7 | import java.awt.Graphics
8 | import java.awt.event.MouseAdapter
9 | import java.awt.event.MouseEvent
10 | import java.awt.image.BufferedImage
11 |
12 | import javax.imageio.ImageIO
13 | import javax.swing.JPanel
14 | import javax.swing.SwingUtilities
15 |
16 | private const val FOLDER_40 = "40"
17 | private const val FOLDER_64 = "64"
18 |
19 | class WorldPanel(var world: World) : JPanel() {
20 |
21 | private var folder: String = if (screenHeight < 1000) FOLDER_40 else FOLDER_64
22 |
23 | private fun loadTile(name: String): BufferedImage {
24 | val image = ImageIO.read(WorldPanel::class.java.getResourceAsStream("/tiles/$folder/$name.png"))
25 | val scale: Int = screenHeight / 1000
26 | return if (scale <= 1) image else image.scaled(scale)
27 | }
28 |
29 | var tileSize = 0
30 | private set
31 |
32 | private var beeper = BufferedImage(1, 1, BufferedImage.TYPE_INT_ARGB)
33 | private var karels = emptyArray()
34 | private var walls = emptyArray()
35 |
36 | private fun loadTiles() {
37 | beeper = loadTile("beeper")
38 | tileSize = beeper.width
39 | loadKarels()
40 | loadWalls()
41 |
42 | val panelSize = Dimension(tileSize * 10, tileSize * 10)
43 | minimumSize = panelSize
44 | preferredSize = panelSize
45 | maximumSize = panelSize
46 | }
47 |
48 | private fun loadKarels() {
49 | val east = loadTile("karel")
50 | val north = east.rotatedCounterclockwise()
51 | val west = north.rotatedCounterclockwise()
52 | val south = west.rotatedCounterclockwise()
53 |
54 | karels = arrayOf(east, north, west, south)
55 | }
56 |
57 | private fun loadWalls() {
58 | walls = Array(16) { loadTile("cross") }
59 |
60 | val east = loadTile("wall")
61 | val north = east.rotatedCounterclockwise()
62 | val west = north.rotatedCounterclockwise()
63 | val south = west.rotatedCounterclockwise()
64 |
65 | intArrayOf(1, 3, 5, 7, 9, 11, 13, 15).forEach { drawWall(it, east) }
66 | intArrayOf(2, 3, 6, 7, 10, 11, 14, 15).forEach { drawWall(it, north) }
67 | intArrayOf(4, 5, 6, 7, 12, 13, 14, 15).forEach { drawWall(it, west) }
68 | intArrayOf(8, 9, 10, 11, 12, 13, 14, 15).forEach { drawWall(it, south) }
69 | }
70 |
71 | private fun drawWall(index: Int, wall: BufferedImage) {
72 | walls[index].graphics.drawImage(wall, 0, 0, null)
73 | }
74 |
75 | var antWorld: World? = null
76 | var showAntWorld: Boolean = false
77 |
78 | override fun paintComponent(graphics: Graphics) {
79 | var world = antWorld
80 | if (world == null || !showAntWorld) {
81 | world = this.world
82 | }
83 |
84 | graphics.drawWallsAndBeepers(world)
85 | graphics.drawKarel(world)
86 | graphics.drawNumbers(world)
87 |
88 | flushGraphicsBuffers()
89 | }
90 |
91 | private fun Graphics.drawWallsAndBeepers(world: World) {
92 | var position = 0
93 | for (y in 0 until 10) {
94 | for (x in 0 until 10) {
95 | drawTile(x, y, walls[world.floorPlan.wallsAt(position)])
96 | if (world.beeperAt(position)) {
97 | drawTile(x, y, beeper)
98 | }
99 | ++position
100 | }
101 | }
102 | }
103 |
104 | private fun Graphics.drawKarel(world: World) {
105 | drawTile(world.x, world.y, karels[world.direction])
106 | }
107 |
108 | private fun Graphics.drawTile(x: Int, y: Int, tile: BufferedImage) {
109 | drawImage(tile, x * tileSize, y * tileSize, null)
110 | }
111 |
112 | var binaryLines = 0
113 |
114 | private fun Graphics.drawNumbers(world: World) {
115 | val shift = if (world.beeperAt(0, 9)) 24 else 0
116 | var y = 0
117 | var lines = binaryLines
118 | while (lines != 0) {
119 | var totalValue = 0
120 | var beeperValue = 1
121 | for (x in 9 downTo 2) {
122 | if (world.beeperAt(x, y)) {
123 | drawNumber(x, y, beeperValue, 0x000000)
124 | totalValue += beeperValue
125 | }
126 | beeperValue = beeperValue.shl(shift + 1).shr(shift)
127 | }
128 | if (lines.and(1) != 0) {
129 | drawNumber(0, y, totalValue, 0x008000)
130 | }
131 | lines = lines.shr(1)
132 | ++y
133 | }
134 | }
135 |
136 | private fun Graphics.drawNumber(x: Int, y: Int, value: Int, color: Int) {
137 | val str = "%3d".format(value)
138 | val width = str.length * Fronts.front.width
139 | val leftPad = (tileSize - width).shr(1)
140 | Fronts.front.drawString(this, x * tileSize + leftPad, y * tileSize, str, color)
141 | }
142 |
143 | private fun listenToMouse() {
144 | addMouseListener(object : MouseAdapter() {
145 | override fun mouseClicked(event: MouseEvent) {
146 | if (!event.component.isEnabled) return
147 |
148 | if (SwingUtilities.isLeftMouseButton(event)) {
149 | if (antWorld != null) {
150 | showAntWorld = !showAntWorld
151 | }
152 | } else if (SwingUtilities.isRightMouseButton(event)) {
153 | switchTileSize()
154 | }
155 | repaint()
156 | }
157 |
158 | override fun mouseEntered(event: MouseEvent) {
159 | showAntWorld = true
160 | if (antWorld != null) {
161 | repaint()
162 | }
163 | }
164 |
165 | override fun mouseExited(event: MouseEvent) {
166 | showAntWorld = false
167 | if (antWorld != null) {
168 | repaint()
169 | }
170 | }
171 | })
172 | }
173 |
174 | private fun switchTileSize() {
175 | when (folder) {
176 | FOLDER_40 -> folder = FOLDER_64
177 | FOLDER_64 -> folder = FOLDER_40
178 | }
179 | loadTiles()
180 | revalidate()
181 | }
182 |
183 | init {
184 | loadTiles()
185 | listenToMouse()
186 | }
187 | }
188 |
--------------------------------------------------------------------------------
/src/main/kotlin/logic/Check.kt:
--------------------------------------------------------------------------------
1 | package logic
2 |
3 | enum class Check(val singular: String, val plural: String) {
4 | EVERY_PICK_DROP_MOVE("pick/drop/move", "picks/drops/moves"),
5 | EVERY_PICK_DROP("pick/drop", "picks/drops");
6 |
7 | fun numerus(n: Int): String = when (n) {
8 | 1 -> singular
9 | else -> plural
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/src/main/kotlin/logic/FloorPlan.kt:
--------------------------------------------------------------------------------
1 | package logic
2 |
3 | @JvmInline
4 | value class FloorPlan(private val walls: ByteArray) {
5 |
6 | fun isClear(position: Int, direction: Int): Boolean {
7 | return walls[position].toInt().and(1 shl direction) == 0
8 | }
9 |
10 | fun wallsAt(position: Int): Int {
11 | return walls[position].toInt().and(WALL_ALL)
12 | }
13 |
14 | fun numberOfWallsAt(position: Int): Int {
15 | val shift = wallsAt(position).shl(2)
16 | return 0x4332_3221_3221_2110L.ushr(shift).toInt().and(7)
17 | }
18 |
19 | fun builder(): FloorBuilder {
20 | return FloorBuilder(walls.clone())
21 | }
22 |
23 | fun world(): World {
24 | return World(0, 0, this)
25 | }
26 |
27 | companion object {
28 | const val WALL_NONE = 0
29 |
30 | const val WALL_EAST = 1
31 | const val WALL_NORTH = 2
32 | const val WALL_WEST = 4
33 | const val WALL_SOUTH = 8
34 |
35 | const val WALL_ALL = 15
36 |
37 | operator fun invoke(vararg walls: Byte): FloorPlan {
38 | require(walls.size == 100) { walls.size }
39 | return FloorPlan(walls)
40 | }
41 |
42 | private const val A: Byte = 10
43 | private const val B: Byte = 11
44 | private const val C: Byte = 12
45 | private const val D: Byte = 13
46 | private const val E: Byte = 14
47 | private const val F: Byte = 15
48 |
49 | val empty = FloorPlan(
50 | 6, 2, 2, 2, 2, 2, 2, 2, 2, 3,
51 | 4, 0, 0, 0, 0, 0, 0, 0, 0, 1,
52 | 4, 0, 0, 0, 0, 0, 0, 0, 0, 1,
53 | 4, 0, 0, 0, 0, 0, 0, 0, 0, 1,
54 | 4, 0, 0, 0, 0, 0, 0, 0, 0, 1,
55 | 4, 0, 0, 0, 0, 0, 0, 0, 0, 1,
56 | 4, 0, 0, 0, 0, 0, 0, 0, 0, 1,
57 | 4, 0, 0, 0, 0, 0, 0, 0, 0, 1,
58 | 4, 0, 0, 0, 0, 0, 0, 0, 0, 1,
59 | C, 8, 8, 8, 8, 8, 8, 8, 8, 9,
60 | )
61 |
62 | val first = FloorPlan(
63 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
64 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
65 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
66 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
67 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
68 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
69 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
70 | 6, 2, 2, 2, 3, 0, 0, 0, 0, 0,
71 | 4, 0, 0, 8, 9, 0, 0, 0, 0, 0,
72 | C, 8, 9, 0, 0, 0, 0, 0, 0, 0,
73 | )
74 |
75 | val holes = FloorPlan(
76 | 6, 2, 2, 2, 2, 2, 2, 2, 2, 3,
77 | 4, 0, 0, 0, 0, 0, 0, 0, 0, 1,
78 | 4, 0, 0, 0, 0, 0, 0, 0, 0, 1,
79 | 4, 0, 0, 0, 0, 0, 0, 0, 0, 1,
80 | 4, 0, 0, 0, 0, 0, 0, 0, 0, 1,
81 | 4, 0, 0, 0, 0, 0, 0, 0, 0, 1,
82 | 4, 0, 0, 0, 0, 0, 0, 0, 0, 1,
83 | C, 0, 0, 0, 0, 0, 0, 0, 0, 1,
84 | 3, C, 0, 8, 0, 8, 0, 8, 0, 9,
85 | 0, 3, D, 7, D, 7, D, 7, D, 6,
86 | )
87 |
88 | val stairs = FloorPlan(
89 | 6, 2, 2, 2, 2, 2, 2, 2, 2, 3,
90 | 4, 0, 0, 0, 0, 0, 0, 0, 0, 1,
91 | 4, 0, 0, 0, 0, 0, 0, 0, 0, 1,
92 | 4, 0, 0, 0, 0, 0, 0, 8, 0, 1,
93 | 4, 0, 0, 0, 0, 0, 9, 7, 4, 1,
94 | 4, 0, 0, 0, 0, 9, 6, 1, 4, 1,
95 | 4, 0, 0, 0, 9, 6, 0, 1, 4, 1,
96 | 4, 0, 0, 9, 6, 0, 0, 1, 4, 1,
97 | 4, 0, 9, 6, 0, 0, 0, 1, 4, 1,
98 | C, 9, E, 8, 8, 8, 8, 9, C, 9,
99 | )
100 |
101 | val mountain = FloorPlan(
102 | 6, 2, 2, 2, 2, 2, 2, 2, 2, 3,
103 | 4, 0, 0, 0, 0, 8, 0, 0, 0, 1,
104 | 4, 0, 0, 0, 1, 7, 4, 0, 0, 1,
105 | 4, 0, 0, 0, 9, 5, C, 0, 0, 1,
106 | 4, 0, 0, 1, 6, 0, 3, 4, 0, 1,
107 | 4, 0, 0, 9, 4, 0, 1, C, 0, 1,
108 | 4, 0, 1, 6, 0, 0, 0, 3, 4, 1,
109 | 4, 0, 9, 4, 0, 0, 0, 1, C, 1,
110 | 4, 1, 6, 0, 0, 0, 0, 0, 3, 5,
111 | C, 9, C, 8, 8, 8, 8, 8, 9, D,
112 | )
113 |
114 | val maze = FloorPlan(
115 | F, F, F, F, F, F, F, F, F, F,
116 | F, F, F, F, F, F, F, F, F, F,
117 | F, F, F, F, F, F, F, F, F, F,
118 | F, F, F, F, F, F, F, F, F, F,
119 | F, F, F, F, F, F, F, F, F, F,
120 | F, F, F, F, F, F, F, F, F, F,
121 | F, F, F, F, F, F, F, F, F, F,
122 | F, F, F, F, F, F, F, F, F, F,
123 | F, F, F, F, F, F, F, F, F, F,
124 | F, F, F, F, F, F, F, F, F, F,
125 | )
126 |
127 | val binary = FloorPlan(
128 | 0, 6, 2, 2, 2, 2, 2, 2, 2, 3,
129 | 0, 4, 0, 0, 0, 0, 0, 0, 0, 1,
130 | 0, 4, 0, 0, 0, 0, 0, 0, 0, 1,
131 | 0, 4, 0, 0, 0, 0, 0, 0, 0, 1,
132 | 0, 4, 0, 0, 0, 0, 0, 0, 0, 1,
133 | 0, 4, 0, 0, 0, 0, 0, 0, 0, 1,
134 | 0, 4, 0, 0, 0, 0, 0, 0, 0, 1,
135 | 0, 4, 0, 0, 0, 0, 0, 0, 0, 1,
136 | 0, 4, 0, 0, 0, 0, 0, 0, 0, 1,
137 | 0, C, 8, 8, 8, 8, 8, 8, 8, 9,
138 | )
139 |
140 | val trap = FloorPlan(
141 | 6, 2, 2, 2, 2, 2, 2, 2, 2, 3,
142 | 4, 0, 0, 0, 0, 0, 0, 0, 0, 1,
143 | 4, 0, 0, 0, 0, 0, 0, 0, 0, 1,
144 | 4, 0, 0, 0, 0, 0, 0, 0, 0, 1,
145 | 4, 0, 0, 0, 0, 0, 0, 0, 0, 1,
146 | 4, 0, 0, 0, 0, 0, 0, 0, 0, 1,
147 | 4, 0, 0, 0, 0, 0, 0, 0, 0, 1,
148 | 4, 0, 0, 0, 0, 0, 0, 0, 0, 1,
149 | 4, 0, 0, 0, 0, 0, 0, 0, 0, 1,
150 | E, A, A, A, A, A, A, A, A, B,
151 | )
152 | }
153 | }
154 |
155 | @JvmInline
156 | value class FloorBuilder(private val walls: ByteArray) {
157 |
158 | fun buildHorizontalWall(x: Int, y: Int): FloorBuilder {
159 | var position = y * 10 + x
160 | if (position < 100) {
161 | walls[position] = walls[position].toInt().or(FloorPlan.WALL_NORTH).toByte()
162 | }
163 | position -= 10
164 | if (position >= 0) {
165 | walls[position] = walls[position].toInt().or(FloorPlan.WALL_SOUTH).toByte()
166 | }
167 | return this
168 | }
169 |
170 | fun buildVerticalWall(x: Int, y: Int): FloorBuilder {
171 | val position = y * 10 + x
172 | if (x < 10) {
173 | walls[position] = walls[position].toInt().or(FloorPlan.WALL_WEST).toByte()
174 | }
175 | if (x > 0) {
176 | walls[position - 1] = walls[position - 1].toInt().or(FloorPlan.WALL_EAST).toByte()
177 | }
178 | return this
179 | }
180 |
181 | fun tearDownWall(position: Int, direction: Int): FloorBuilder {
182 | walls[position] = walls[position].toInt().and(1.shl(direction).inv()).toByte()
183 | return this
184 | }
185 |
186 | fun world(): World {
187 | return FloorPlan(walls).world()
188 | }
189 | }
190 |
--------------------------------------------------------------------------------
/src/main/kotlin/logic/KarelError.kt:
--------------------------------------------------------------------------------
1 | package logic
2 |
3 | abstract class KarelError(override val message: String) : Exception(message)
4 |
5 | class CellIsEmpty : KarelError("there is no beeper to pick")
6 |
7 | class CellIsFull : KarelError("cannot drop another beeper")
8 |
9 | class BlockedByWall : KarelError("cannot move through wall")
10 |
--------------------------------------------------------------------------------
/src/main/kotlin/logic/LabyrinthGenerator.kt:
--------------------------------------------------------------------------------
1 | package logic
2 |
3 | private const val CHARTED = '#'
4 | private const val WALL = '_'
5 | private const val FREE = ' '
6 |
7 | // The number on an uncharted cell denotes its uncharted neighbours
8 | private val labyrinth = """
9 | # # # # # # # # # # # #
10 | _ _ _ _ _ _ _ _ _ _ _ _
11 | #_2_3_3_3_3_3_3_3_3_2_#
12 | _ _ _ _ _ _ _ _ _ _ _ _
13 | #_3_4_4_4_4_4_4_4_4_3_#
14 | _ _ _ _ _ _ _ _ _ _ _ _
15 | #_3_4_4_4_4_4_4_4_4_3_#
16 | _ _ _ _ _ _ _ _ _ _ _ _
17 | #_3_4_4_4_4_4_4_4_4_3_#
18 | _ _ _ _ _ _ _ _ _ _ _ _
19 | #_3_4_4_4_4_4_4_4_4_3_#
20 | _ _ _ _ _ _ _ _ _ _ _ _
21 | #_3_4_4_4_4_4_4_4_4_3_#
22 | _ _ _ _ _ _ _ _ _ _ _ _
23 | #_3_4_4_4_4_4_4_4_4_3_#
24 | _ _ _ _ _ _ _ _ _ _ _ _
25 | #_3_4_4_4_4_4_4_4_4_3_#
26 | _ _ _ _ _ _ _ _ _ _ _ _
27 | #_3_4_4_4_4_4_4_4_4_3_#
28 | _ _ _ _ _ _ _ _ _ _ _ _
29 | #_2_3_3_3_3_3_3_3_3_2_#
30 | _ _ _ _ _ _ _ _ _ _ _ _
31 | # # # # # # # # # # # #
32 | """.toCharArray()
33 |
34 | private const val EAST = 1
35 | private const val NORTH = -24
36 | private const val WEST = -1
37 | private const val SOUTH = 24
38 |
39 | private const val NEIGHBOUR_X = 2 * EAST
40 | private const val NEIGHBOUR_Y = 2 * SOUTH
41 | private const val ORIGIN = 1 + NEIGHBOUR_Y + NEIGHBOUR_X
42 |
43 | private val turnLeft = IntArray(256).apply {
44 | this[EAST and 255] = NORTH
45 | this[NORTH and 255] = WEST
46 | this[WEST and 255] = SOUTH
47 | this[SOUTH and 255] = EAST
48 | }
49 |
50 | private val turnRight = IntArray(256).apply {
51 | this[EAST and 255] = SOUTH
52 | this[SOUTH and 255] = WEST
53 | this[WEST and 255] = NORTH
54 | this[NORTH and 255] = EAST
55 | }
56 |
57 | private val permutationsOfDirections = intArrayOf(
58 | 0x01e8ff18, // EAST, NORTH, WEST, SOUTH,
59 | 0x01e818ff, // EAST, NORTH, SOUTH, WEST,
60 | 0x01ffe818, // EAST, WEST, NORTH, SOUTH,
61 | 0x01ff18e8, // EAST, WEST, SOUTH, NORTH,
62 | 0x0118e8ff, // EAST, SOUTH, NORTH, WEST,
63 | 0x0118ffe8, // EAST, SOUTH, WEST, NORTH,
64 |
65 | 0xe801ff18.toInt(), // NORTH, EAST, WEST, SOUTH,
66 | 0xe80118ff.toInt(), // NORTH, EAST, SOUTH, WEST,
67 | 0xe8ff0118.toInt(), // NORTH, WEST, EAST, SOUTH,
68 | 0xe8ff1801.toInt(), // NORTH, WEST, SOUTH, EAST,
69 | 0xe81801ff.toInt(), // NORTH, SOUTH, EAST, WEST,
70 | 0xe818ff01.toInt(), // NORTH, SOUTH, WEST, EAST,
71 |
72 | 0xff01e818.toInt(), // WEST, EAST, NORTH, SOUTH,
73 | 0xff0118e8.toInt(), // WEST, EAST, SOUTH, NORTH,
74 | 0xffe80118.toInt(), // WEST, NORTH, EAST, SOUTH,
75 | 0xffe81801.toInt(), // WEST, NORTH, SOUTH, EAST,
76 | 0xff1801e8.toInt(), // WEST, SOUTH, EAST, NORTH,
77 | 0xff18e801.toInt(), // WEST, SOUTH, NORTH, EAST,
78 |
79 | 0x1801e8ff, // SOUTH, EAST, NORTH, WEST,
80 | 0x1801ffe8, // SOUTH, EAST, WEST, NORTH,
81 | 0x18e801ff, // SOUTH, NORTH, EAST, WEST,
82 | 0x18e8ff01, // SOUTH, NORTH, WEST, EAST,
83 | 0x18ff01e8, // SOUTH, WEST, EAST, NORTH,
84 | 0x18ffe801, // SOUTH, WEST, NORTH, EAST,
85 | )
86 |
87 | private inline fun forEachDirection(block: (Int) -> Unit) {
88 | val directions = permutationsOfDirections[kotlin.random.Random.nextInt(24)]
89 |
90 | block(directions shr 24)
91 | block(directions shl 8 shr 24)
92 | block(directions shl 16 shr 24)
93 | block(directions shl 24 shr 24)
94 | }
95 |
96 | private const val BACKTRACK = 0
97 | private const val BACKTRACK_BUDGET_EXHAUSTED = -1
98 |
99 | object LabyrinthGenerator {
100 | private var labyrinth = logic.labyrinth
101 | private var backtrackBudget = 0
102 |
103 | fun generateLabyrinth(): World {
104 | var destination: Int
105 | do {
106 | labyrinth = logic.labyrinth.clone()
107 | backtrackBudget = 1000
108 | destination = destinationOpen(ORIGIN, EAST, 99)
109 | } while (destination == BACKTRACK_BUDGET_EXHAUSTED)
110 |
111 | val walls = ByteArray(100)
112 | var i = 0
113 | var position = ORIGIN
114 |
115 | for (y in 0 until 10) {
116 | for (x in 0 until 10) {
117 |
118 | val east = labyrinth[position + EAST].code and 1
119 | val north = labyrinth[position + NORTH].code and 2
120 | val west = labyrinth[position + WEST].code and 4
121 | val south = labyrinth[position + SOUTH].code and 8
122 |
123 | walls[i++] = (south).or(west).or(north).or(east).toByte()
124 |
125 | position += NEIGHBOUR_X
126 | }
127 | position += NEIGHBOUR_Y - 10 * NEIGHBOUR_X
128 | }
129 |
130 | val y = destination / NEIGHBOUR_Y
131 | val x = destination % NEIGHBOUR_Y / NEIGHBOUR_X
132 |
133 | return FloorPlan(walls).world().dropBeeper(x - 1, y - 1)
134 | }
135 |
136 | private fun isUncharted(position: Int): Boolean {
137 | return labyrinth[position] >= '0'
138 | }
139 |
140 | private fun causesPartition(position: Int, direction: Int): Boolean {
141 | return !isUncharted(position + 2 * direction) &&
142 | isUncharted(position + 2 * turnLeft[direction and 255]) &&
143 | isUncharted(position + 2 * turnRight[direction and 255])
144 | }
145 |
146 | private fun destinationOpen(position: Int, direction: Int, uncharted: Int): Int {
147 | if (causesPartition(position, direction)) return BACKTRACK
148 |
149 | val unchartedNeighbours = labyrinth[position]
150 | labyrinth[position] = CHARTED
151 |
152 | var potentialDeadEnds = 0
153 | // A potential dead end is a neighbour that will turn into a dead end
154 | // unless we tear down the wall and pick it as our next position
155 | if (--labyrinth[position + NEIGHBOUR_X] == '1') ++potentialDeadEnds
156 | if (--labyrinth[position - NEIGHBOUR_Y] == '1') ++potentialDeadEnds
157 | if (--labyrinth[position - NEIGHBOUR_X] == '1') ++potentialDeadEnds
158 | if (--labyrinth[position + NEIGHBOUR_Y] == '1') ++potentialDeadEnds
159 |
160 | if (potentialDeadEnds == 0) {
161 | // We can roam freely without consequence
162 | forEachDirection { dir ->
163 | val neighbour = position + 2 * dir
164 | if (isUncharted(neighbour)) {
165 | val wall = position + dir
166 | labyrinth[wall] = FREE
167 | destinationOpen(neighbour, dir, uncharted - 1).let { if (it != BACKTRACK) return it }
168 | labyrinth[wall] = WALL
169 | }
170 | }
171 | } else if (potentialDeadEnds == 2) {
172 | // We must eliminate one of the potential dead ends by picking it
173 | forEachDirection { dir ->
174 | val neighbour = position + 2 * dir
175 | if (labyrinth[neighbour] == '1') {
176 | val wall = position + dir
177 | labyrinth[wall] = FREE
178 | destinationFound(neighbour, dir, uncharted - 1).let { if (it != BACKTRACK) return it }
179 | labyrinth[wall] = WALL
180 | }
181 | }
182 | } else if (potentialDeadEnds == 1) {
183 | // We can either eliminate the potential dead end by picking it,
184 | // or turn it into an actual dead end by picking another neighbour
185 | forEachDirection { dir ->
186 | val neighbour = position + 2 * dir
187 | if (isUncharted(neighbour)) {
188 | val wall = position + dir
189 | labyrinth[wall] = FREE
190 | if (labyrinth[neighbour] == '1') {
191 | destinationOpen(neighbour, dir, uncharted - 1).let { if (it != BACKTRACK) return it }
192 | } else {
193 | destinationFound(neighbour, dir, uncharted - 1).let { if (it != BACKTRACK) return it }
194 | }
195 | labyrinth[wall] = WALL
196 | }
197 | }
198 | }
199 |
200 | if (--backtrackBudget < 0) return BACKTRACK_BUDGET_EXHAUSTED
201 |
202 | labyrinth[position] = unchartedNeighbours
203 | labyrinth[position + NEIGHBOUR_X]++
204 | labyrinth[position - NEIGHBOUR_Y]++
205 | labyrinth[position - NEIGHBOUR_X]++
206 | labyrinth[position + NEIGHBOUR_Y]++
207 |
208 | return BACKTRACK
209 | }
210 |
211 | private fun destinationFound(position: Int, direction: Int, uncharted: Int): Int {
212 | if (causesPartition(position, direction)) return BACKTRACK
213 | if (uncharted == 0) return position
214 |
215 | val unchartedNeighbours = labyrinth[position]
216 | labyrinth[position] = CHARTED
217 |
218 | var potentialDeadEnds = 0
219 | // A potential dead end is a neighbour that will turn into a dead end
220 | // unless we tear down the wall and pick it as our next position
221 | if (--labyrinth[position + NEIGHBOUR_X] == '1') ++potentialDeadEnds
222 | if (--labyrinth[position - NEIGHBOUR_Y] == '1') ++potentialDeadEnds
223 | if (--labyrinth[position - NEIGHBOUR_X] == '1') ++potentialDeadEnds
224 | if (--labyrinth[position + NEIGHBOUR_Y] == '1') ++potentialDeadEnds
225 |
226 | if (potentialDeadEnds == 0) {
227 | // We can roam freely without consequence
228 | forEachDirection { dir ->
229 | val neighbour = position + 2 * dir
230 | if (isUncharted(neighbour)) {
231 | val wall = position + dir
232 | labyrinth[wall] = FREE
233 | destinationFound(neighbour, dir, uncharted - 1).let { if (it != BACKTRACK) return it }
234 | labyrinth[wall] = WALL
235 | }
236 | }
237 | } else if (potentialDeadEnds == 1) {
238 | // We must eliminate the potential dead end by picking it,
239 | // because we already found our destination earlier
240 | forEachDirection { dir ->
241 | val neighbour = position + 2 * dir
242 | if (labyrinth[neighbour] == '1') {
243 | val wall = position + dir
244 | labyrinth[wall] = FREE
245 | destinationFound(neighbour, dir, uncharted - 1).let { if (it != BACKTRACK) return it }
246 | labyrinth[wall] = WALL
247 | }
248 | }
249 | }
250 |
251 | if (--backtrackBudget < 0) return BACKTRACK_BUDGET_EXHAUSTED
252 |
253 | labyrinth[position] = unchartedNeighbours
254 | labyrinth[position + NEIGHBOUR_X]++
255 | labyrinth[position - NEIGHBOUR_Y]++
256 | labyrinth[position - NEIGHBOUR_X]++
257 | labyrinth[position + NEIGHBOUR_Y]++
258 |
259 | return BACKTRACK
260 | }
261 | }
262 |
--------------------------------------------------------------------------------
/src/main/kotlin/logic/Problem.kt:
--------------------------------------------------------------------------------
1 | package logic
2 |
3 | import java.math.BigInteger
4 | import kotlin.random.Random
5 |
6 | val UNKNOWN = 0.toBigInteger()
7 | val ONE = 1.toBigInteger()
8 | val TWO = 2.toBigInteger()
9 | val SHUFFLE = TWO..65536.toBigInteger()
10 |
11 | class Problem(
12 | val index: String,
13 | val name: String,
14 | val story: String,
15 | val goal: String,
16 | val check: Check,
17 | val binaryLines: Int,
18 | val numWorlds: BigInteger,
19 | val createWorld: (id: Int) -> World
20 | ) {
21 | override fun toString(): String = "$index $name"
22 |
23 | val isRandom: Boolean
24 | get() = numWorlds != ONE
25 |
26 | fun randomWorld(): World {
27 | return createWorld(Random.nextInt().ushr(1))
28 | }
29 |
30 | fun randomWorlds(): Sequence = when (numWorlds) {
31 | ONE -> sequenceOf(createWorld(0))
32 |
33 | in SHUFFLE -> (0 until numWorlds.toInt()).asSequence().shuffled().map(createWorld)
34 |
35 | else -> generateSequence { createWorld(0) }
36 | }
37 |
38 | companion object {
39 | const val EAST = 0
40 | const val NORTH = 1
41 | const val WEST = 2
42 | const val SOUTH = 3
43 |
44 | val emptyWorld: World = FloorPlan.empty.world()
45 |
46 | private fun pillars(): World {
47 | var world = emptyWorld
48 |
49 | for (x in 0..9) {
50 | for (y in Random.nextInt(11)..9) {
51 | world = world.dropBeeper(x, y)
52 | }
53 | }
54 | return world
55 | }
56 |
57 | private fun randomByte(rng: WorldEntropy): World {
58 | var world = FloorPlan.binary.world()
59 |
60 | world = world.withBeepers(0, rng.nextInt(256).shl(2).toLong())
61 |
62 | return world.withKarelAt(9, 0, WEST)
63 | }
64 |
65 | private fun randomBytes(rng: WorldEntropy, direction: Int): World {
66 | var world = FloorPlan.binary.world()
67 |
68 | world = world.withBeepers(0, (rng.nextInt(256).shl(2) + rng.nextInt(256).shl(12)).toLong())
69 |
70 | return world.withKarelAt(9, 0, direction)
71 | }
72 |
73 | val karelsFirstProgram = Problem(
74 | "0.0.1",
75 | "karelsFirstProgram",
76 | "Click the GOAL button (top left)\nand watch Karel go. Drag slider\nto adjust animation speed.\nCan you program Karel to perform\nthe same steps? Test with START!",
77 | "\u0001\u0005\u0001\u0002\u0001\u0004\u0001\u0006\u0001\u0000",
78 | Check.EVERY_PICK_DROP_MOVE,
79 | 0,
80 | ONE,
81 | ) {
82 | val world = FloorPlan.first.world()
83 |
84 | world.dropBeeper(1, 9).withKarelAt(0, 9, EAST)
85 | }
86 |
87 | val obtainArtifact = Problem(
88 | "1.1.1 \uD83E\uDD20",
89 | "obtainArtifact",
90 | "Karel auditions for the new Indy\nmovie. To demonstrate talent,\nKarel re-enacts the classic scene\nwhere Indy saves some valuable\nartifact from an ancient temple.",
91 | "\u0004\ua106\u0005\ua106\u0006\u0000\u0001\u0002\u0001\u0001\u0001\u0002\u0001\u0000",
92 | Check.EVERY_PICK_DROP_MOVE,
93 | 0,
94 | ONE,
95 | ) {
96 | val world = FloorPlan.empty.builder().buildVerticalWall(5, 5).world()
97 |
98 | world.dropBeeper(6, 5).withKarelAt(3, 5, EAST)
99 | }
100 |
101 | val defuseOneBomb = Problem(
102 | "1.1.2",
103 | "defuseOneBomb",
104 | "Karel the demolition expert\ndefuses a bomb at the other end\nof the room and returns filled\nwith pride and self-confidence.\nHave you learned repeat (n) yet?",
105 | "\ua106\u0005\u0003\ua106\u0003\u0000\u8009\u0001\u9107\u0000",
106 | Check.EVERY_PICK_DROP_MOVE,
107 | 0,
108 | ONE,
109 | ) {
110 | val world = emptyWorld.dropBeeper(9, 9)
111 |
112 | world.withKarelAt(0, 9, EAST)
113 | }
114 |
115 | val defuseTwoBombs = Problem(
116 | "1.1.3",
117 | "defuseTwoBombs",
118 | "One bomb is no problem for Karel.\nLet's spice up the challenge!\nShouldn't this be rather simple,\ngiven that Karel already knows\nhow to defuse one single bomb?",
119 | "\ua102\u0002\ua108\u0005\u0003\ua108\u0003\u0000\u8009\u0001\u9109\u0000",
120 | Check.EVERY_PICK_DROP_MOVE,
121 | 0,
122 | ONE,
123 | ) {
124 | val world = emptyWorld.dropBeeper(0, 0).dropBeeper(9, 9)
125 |
126 | world.withKarelAt(0, 9, EAST)
127 | }
128 |
129 | val practiceHomeRun = Problem(
130 | "1.1.4",
131 | "practiceHomeRun",
132 | "Karel's heart burns for baseball,\nbut merely watching does not cut\nit anymore. Tonight, let's sneak\ninto the stadium and perform our\nfirst home run. Adrenaline rush!",
133 | "\u8004\u8009\u0001\u9102\u0005\u0002\u9101\u0000",
134 | Check.EVERY_PICK_DROP_MOVE,
135 | 0,
136 | ONE,
137 | ) {
138 | val world = emptyWorld.dropBeeper(0, 0).dropBeeper(9, 0).dropBeeper(0, 9).dropBeeper(9, 9)
139 |
140 | world.withKarelAt(0, 9, EAST)
141 | }
142 |
143 | val climbTheStairs = Problem(
144 | "1.2.1",
145 | "climbTheStairs",
146 | "The elevator seems to be\nout of service as of late...\nBut Karel is still pumped from\nthat home run and full of energy!",
147 | "\u0001\u8006\u0002\u0001\u0004\u0001\u9102\u0000",
148 | Check.EVERY_PICK_DROP_MOVE,
149 | 0,
150 | ONE,
151 | ) {
152 | val world = FloorPlan.stairs.world()
153 |
154 | world.withKarelAt(0, 9, EAST)
155 | }
156 |
157 | val fillTheHoles = Problem(
158 | "1.2.2",
159 | "fillTheHoles",
160 | "Karel considers a career in den-\ndistry. The local dental school\nhas Open House day. Coincidence?\nKarel gets to fill 4 carious\nteeth with dental amalgam. Ouch!",
161 | "\u8004\u0001\u0004\u0001\u0006\u0003\u0001\u0004\u0001\u9101\u0000",
162 | Check.EVERY_PICK_DROP_MOVE,
163 | 0,
164 | ONE,
165 | ) {
166 | val world = FloorPlan.holes.world()
167 |
168 | world.withKarelAt(1, 8, EAST)
169 | }
170 |
171 | val saveTheFlower = Problem(
172 | "1.2.3",
173 | "saveTheFlower",
174 | "During a vacation in the alps,\nKarel discovers a rare flower\nwhich has trouble blooming\nat such low altitude...\nIt's a long way to the top!",
175 | "\u0001\u0005\u8004\u0002\u0001\u0001\u0004\u0001\u9103\u0006\u8004\u0001\u0004\u0001\u0001\u0002\u910b\u0000",
176 | Check.EVERY_PICK_DROP_MOVE,
177 | 0,
178 | ONE,
179 | ) {
180 | val world = FloorPlan.mountain.world().dropBeeper(1, 9)
181 |
182 | world.withKarelAt(0, 9, EAST)
183 | }
184 |
185 | val mowTheLawn = Problem(
186 | "1.2.4",
187 | "mowTheLawn",
188 | "Karel promised Granger to help in\nthe garden. Granger has already\npulled up the weeds, so Karel\ncan focus on mowing the lawn.",
189 | "\u8002\ua106\u0004\u0001\u0004\u9101\ua10a\u0002\u0001\u0002\u8006\u0001\u0005\u910b\u0001\u0000",
190 | Check.EVERY_PICK_DROP,
191 | 0,
192 | ONE,
193 | ) {
194 | val world = emptyWorld
195 |
196 | world.withBeepers(0x3f0fL, 0xc3f0fc3f0fcL.shl(20)).withKarelAt(1, 7, EAST)
197 | }
198 |
199 | val harvestTheField = Problem(
200 | "1.3.1",
201 | "harvestTheField",
202 | "Granger is an agricult -- erm...\nfarmer. After mowing the lawn,\nKarel can't reject the desperate\nplea for help on the farm.\nThe wheat is already overripe!",
203 | "\ua105\u0004\u0001\u0004\u0001\ua10a\u0001\u0002\u0001\u0002\u8003\u0005\u0004\u0001\u0002\u0001\u910b\u0005\u0000",
204 | Check.EVERY_PICK_DROP,
205 | 0,
206 | ONE,
207 | ) {
208 | val world = emptyWorld
209 |
210 | world.withBeepers(0x805L, 0x2a1542a05008000L).withKarelAt(5, 7, NORTH)
211 | }
212 |
213 | val repairTheStreet = Problem(
214 | "1.3.2",
215 | "repairTheStreet",
216 | "Click the DICE button. Notice\nsomething? Not all streets are\ncreated equal! Have you learned\nabout the if/else statement yet?\nF7..F11 are Karel's conditions.",
217 | "\u8009\ua104\u0001\u9101\u000b\uc10c\u0004\u0001\u0006\u0003\u0001\u0004\u0000",
218 | Check.EVERY_PICK_DROP_MOVE,
219 | 0,
220 | TWO.pow(10),
221 | ) { id ->
222 | val rng = WorldEntropy(id)
223 | val builder = FloorPlan.empty.builder()
224 |
225 | for (x in 0..9) {
226 | if (rng.nextBoolean()) {
227 | builder.buildHorizontalWall(x, 9)
228 | } else {
229 | builder.buildVerticalWall(x, 9)
230 | builder.buildVerticalWall(x + 1, 9)
231 | }
232 | }
233 | builder.world().withKarelAt(0, 8, EAST)
234 | }
235 |
236 | val cleanTheRoom = Problem(
237 | "1.3.3",
238 | "cleanTheRoom",
239 | "Granger is paying Karel a surprise\nvisit. But Karel's apartment\nis *really* out of shape :(\nThe chaos is almost overwhelming.\nCan Karel clean up in time?",
240 | "\u8004\ua106\u0004\u0001\u0004\u9101\ua10a\u0002\u0001\u0002\u8009\u0007\uc10e\u0005\u0001\u910b\u0007\uc113\u0005\u0000",
241 | Check.EVERY_PICK_DROP_MOVE,
242 | 0,
243 | TWO.pow(100),
244 | ) {
245 | var world = emptyWorld
246 |
247 | world = world.withBeepers(Random.nextLong(), Random.nextLong())
248 |
249 | world.withKarelAt(0, 9, EAST)
250 | }
251 |
252 | val tileTheFloor = Problem(
253 | "1.3.4",
254 | "tileTheFloor",
255 | "During a routine visit to the\nhardware store, Karel can't\nresist buying some flagstones.\nThey seem to be a perfect fit\nfor the luxurious bathroom!",
256 | "\u8064\u0006\u0008\ud106\u000a\ud107\u0002\u0001\u9101\u0000",
257 | Check.EVERY_PICK_DROP,
258 | 0,
259 | ONE,
260 | ) {
261 | emptyWorld.withKarelAt(0, 9, EAST)
262 | }
263 |
264 | val stealOlympicFire = Problem(
265 | "1.4.1",
266 | "stealOlympicFire",
267 | "Karel is mad with olympic fever\nand somehow comes to believe\nit would be a good idea to\nsteal the olympic fire O_o\nLet's hope nobody will notice...",
268 | "\u0001\u8006\u0002\u0001\u0004\u0001\u9102\u0005\u0001\u0004\u8006\u0001\u910b\u0002\u0001\u0000",
269 | Check.EVERY_PICK_DROP_MOVE,
270 | 0,
271 | ONE,
272 | ) {
273 | val world = FloorPlan.stairs.world().dropBeeper(7, 3)
274 |
275 | world.withKarelAt(0, 9, EAST)
276 | }
277 |
278 | val removeTheTiles = Problem(
279 | "1.4.2",
280 | "removeTheTiles",
281 | "The flagstones were supposed to\nbe a surprise for Karel's new\nsweetheart, Taylor. Too bad green\nis not Taylor's favourite color.\nOh well, back to square one...",
282 | "\u8064\u0005\u0008\ud105\u0002\u0001\u9101\u0000",
283 | Check.EVERY_PICK_DROP,
284 | 0,
285 | ONE,
286 | ) {
287 | val world = emptyWorld.fillWithBeepers()
288 |
289 | world.withKarelAt(0, 9, EAST)
290 | }
291 |
292 | val walkTheLabyrinth = Problem(
293 | "1.4.3",
294 | "walkTheLabyrinth",
295 | "Click DICE several times.\nNote how the generated labyrinths\nare rather simple? They contain\nneither crossroads nor dead ends.\nExactly one path to the beeper!",
296 | "\u8063\u000a\ud10a\u0009\uc109\u0002\u0001\u9101\u0000\u0004\u0001\u9101\u0000",
297 | Check.EVERY_PICK_DROP_MOVE,
298 | 0,
299 | UNKNOWN,
300 | ) {
301 | LabyrinthGenerator.generateLabyrinth()
302 | }
303 |
304 | val hangTheLampions = Problem(
305 | "2.1.1 \uD83C\uDFEE",
306 | "hangTheLampions",
307 | "Karel was assembled 10 years ago!\nTo celebrate this anniversary,\nKarel bought 10 lampions. Now all\nthat's left to do is hang them\nfrom the (irregular) ceiling.",
308 | "\u8009\ua104\u0001\u9101\u0002\u0005\u0001\u000a\ud106\u0006\u0003\u0001\u000a\ud10b\u0002\u0000",
309 | Check.EVERY_PICK_DROP_MOVE,
310 | 0,
311 | 3.toBigInteger().pow(10),
312 | ) { id ->
313 | val rng = WorldEntropy(id)
314 | val builder = FloorPlan.empty.builder()
315 |
316 | for (x in 0..9) {
317 | builder.buildHorizontalWall(x, 1 + rng.nextInt(3))
318 | }
319 | val world = builder.world().withBeepers(1023L.shl(90 - 64), 0L)
320 | world.withKarelAt(0, 9, EAST)
321 | }
322 |
323 | val followTheSeeds = Problem(
324 | "2.1.2",
325 | "followTheSeeds",
326 | "Karel had insomnia and decided\nto take a walk in the forest.\nFortunately, Karel was smart\nenough to leave a trail of seeds\nto find the way back...",
327 | "\u0001\u0005\u0008\ud100\u0002\u0008\ud100\u0000",
328 | Check.EVERY_PICK_DROP_MOVE,
329 | 0,
330 | ONE,
331 | ) {
332 | val world = emptyWorld.withBeepers(0xffc017f50L, 0x55d5555157d405ffL)
333 | world.withKarelAt(5, 4, WEST)
334 | }
335 |
336 | val cleanTheTunnels = Problem(
337 | "2.1.3",
338 | "cleanTheTunnels",
339 | "Karel the coal miner discovers\nten tunnels of varying lengths\nfilled with valuable coal.\n(Does your solution work for\ntunnels of length 0 and 10?)",
340 | "\u8009\ua104\u0001\u9101\u0007\uc113\u0002\u0005\u0008\uc114\u0001\u0005\u0008\ud10a\u0003\u0001\u000a\ud10f\u0002\u0000\u0004\u0000",
341 | Check.EVERY_PICK_DROP,
342 | 0,
343 | 11.toBigInteger().pow(10),
344 | ) {
345 | pillars().withKarelAt(0, 9, EAST)
346 | }
347 |
348 | val increment = Problem(
349 | "2.2.1",
350 | "increment",
351 | "Do you know binary numbers?\nen.wikipedia.org/wiki/Binary_number\nde.wikipedia.org/wiki/Dualsystem\nKarel wants to add 1 to a byte.\nThis is almost trivial in binary.",
352 | "\u0007\uc106\u0005\u0001\u0007\ud102\u0006\u0000",
353 | Check.EVERY_PICK_DROP_MOVE,
354 | 0b1,
355 | TWO.pow(8),
356 | ) { id ->
357 | randomByte(WorldEntropy(id))
358 | }
359 |
360 | val decrement = Problem(
361 | "2.2.2",
362 | "decrement",
363 | "Karel wants to subtract 1 from\na byte. Notice any similarity\nto increment? (What happens if\nKarel decrements the byte zero?\nYou can click in Karel's world!)",
364 | "\u0007\ud108\u0006\u000a\uc108\u0001\u0007\uc102\u0005\u0000",
365 | Check.EVERY_PICK_DROP_MOVE,
366 | 0b1,
367 | TWO.pow(8),
368 | ) { id ->
369 | randomByte(WorldEntropy(id))
370 | }
371 |
372 | val addSlow = Problem(
373 | "2.2.3",
374 | "addSlow",
375 | "Welcome to the slowest adding\nmachine in the world! Karel just\ndecrements the first byte\nand increments the second byte\nuntil the first byte is zero.",
376 | "\u0007\ud120\u0006\u000a\uc126\u0001\u0007\uc102\u0005\u0003\u0001\u000a\ud10a\u0004\u0001\u0004\u0007\uc123\u0005\u0001\u0007\ud112\u0006\u0003\u0001\u000a\ud118\u0002\u0001\u0002\u0007\uc102\u0005\u0003\ub10d\u0006\u0003\ub11b\u0005\u0000",
377 | Check.EVERY_PICK_DROP,
378 | 0b11,
379 | TWO.pow(16),
380 | ) { id ->
381 | randomBytes(WorldEntropy(id), WEST)
382 | }
383 |
384 | val saveTheFlowers = Problem(
385 | "2.3.1",
386 | "saveTheFlowers",
387 | "Karel climbs Mt. Everest. On the\nway up, Karel collects 4 flowers\nthat do not get enough sunlight\non the west side of the mountain.\nEast is where the sun comes up!",
388 | "\u8005\ub103\u0005\u0002\u0001\u000b\uc104\u0004\u0001\u9102\u8004\u0006\u0001\u0004\u0001\u000a\ud10e\u0002\u910b\u0000",
389 | Check.EVERY_PICK_DROP_MOVE,
390 | 0,
391 | 3920.toBigInteger(),
392 | ) { id ->
393 | val rng = WorldEntropy(id)
394 | val builder = FloorPlan.empty.builder()
395 |
396 | val upPermutations =
397 | "5432643265326542654374327532754275437632764276437652765376548432853285428543863286428643865286538654873287428743875287538754876287638764876594329532954295439632964296439652965396549732974297439752975397549762976397649765983298429843985298539854986298639864986598729873987498759876"
398 | val up = rng.nextInt(upPermutations.length / 4) * 4
399 | val y1 = upPermutations[up] - '0'
400 | val y2 = upPermutations[up + 1] - '0'
401 | val y3 = upPermutations[up + 2] - '0'
402 | val y4 = upPermutations[up + 3] - '0'
403 |
404 | for (y in y1 until 10) builder.buildVerticalWall(1, y)
405 | builder.buildHorizontalWall(1, y1)
406 | for (y in y2 until y1) builder.buildVerticalWall(2, y)
407 | builder.buildHorizontalWall(2, y2)
408 | for (y in y3 until y2) builder.buildVerticalWall(3, y)
409 | builder.buildHorizontalWall(3, y3)
410 | for (y in y4 until y3) builder.buildVerticalWall(4, y)
411 | builder.buildHorizontalWall(4, y4)
412 | for (y in 1 until y4) builder.buildVerticalWall(5, y)
413 |
414 | builder.buildHorizontalWall(5, 1)
415 |
416 | val downPermutations =
417 | "234235236237238239245246247248249256257258259267268269278279289345346347348349356357358359367368369378379389456457458459467468469478479489567568569578579589678679689789"
418 | val down = rng.nextInt(downPermutations.length / 3) * 3
419 | val y5 = downPermutations[down] - '0'
420 | val y6 = downPermutations[down + 1] - '0'
421 | val y7 = downPermutations[down + 2] - '0'
422 |
423 | for (y in 1 until y5) builder.buildVerticalWall(6, y)
424 | builder.buildHorizontalWall(6, y5)
425 | for (y in y5 until y6) builder.buildVerticalWall(7, y)
426 | builder.buildHorizontalWall(7, y6)
427 | for (y in y6 until y7) builder.buildVerticalWall(8, y)
428 | builder.buildHorizontalWall(8, y7)
429 | for (y in y7 until 10) builder.buildVerticalWall(9, y)
430 |
431 | val world =
432 | builder.world().dropBeeper(1, y1 - 1).dropBeeper(2, y2 - 1).dropBeeper(3, y3 - 1).dropBeeper(4, y4 - 1)
433 | world.withKarelAt(0, 9, EAST)
434 | }
435 |
436 | val findTeddyBear = Problem(
437 | "2.3.2",
438 | "findTeddyBear",
439 | "In the middle of the night, Karel\nawakens from a terrible dream.\nThe teddy bear will provide\ncomfort. It should lay somewhere\nnear the edge of the bed...",
440 | "\u0007\ud108\u000a\uc106\u0001\ub100\u0002\ub100\u0000",
441 | Check.EVERY_PICK_DROP_MOVE,
442 | 0,
443 | 14400.toBigInteger(),
444 | ) { id ->
445 | val rng = WorldEntropy(id)
446 | var world = emptyWorld
447 |
448 | val zeroToEight = rng.nextInt(9)
449 | when (rng.nextInt(4)) {
450 | NORTH -> world = world.dropBeeper(zeroToEight, 0)
451 | EAST -> world = world.dropBeeper(9, zeroToEight)
452 | SOUTH -> world = world.dropBeeper(9 - zeroToEight, 9)
453 | WEST -> world = world.dropBeeper(0, 9 - zeroToEight)
454 | }
455 | val x = rng.nextInt(10)
456 | val y = rng.nextInt(10)
457 | val dir = rng.nextInt(4)
458 | world.withKarelAt(x, y, dir)
459 | }
460 |
461 | val jumpTheHurdles = Problem(
462 | "2.3.3",
463 | "jumpTheHurdles",
464 | "Karel signs up for the Olympics\nand is allowed to participate\nin the hurdle runs. After jumping\nall the hurdles, Karel receives a\nspecial medal made of copper!",
465 | "\u0007\ud113\u000a\uc106\u0001\ub100\u0002\u0001\u000b\uc107\u0004\u0001\u0004\u0001\u000a\ud10d\u0002\u0007\uc102\u0000",
466 | Check.EVERY_PICK_DROP_MOVE,
467 | 0,
468 | 1111100000.toBigInteger(),
469 | ) {
470 | val xBeeper = 5 + Random.nextInt(5)
471 | val builder = FloorPlan.empty.builder()
472 |
473 | for (x in 1..xBeeper) {
474 | for (y in 0 until Random.nextInt(10)) {
475 | builder.buildVerticalWall(x, 9 - y)
476 | }
477 | }
478 | builder.world().dropBeeper(xBeeper, 9).withKarelAt(0, 9, EAST)
479 | }
480 |
481 | val solveTheMaze = Problem(
482 | "2.4.1",
483 | "solveTheMaze",
484 | "Study the random mazes carefully.\nThey contain both crossroads and\ndead ends, but no loops. Maintain\ncontact with Karel's left wall\nand you should find the beeper!",
485 | "\u0007\ud11a\u0009\uc109\u0002\u0001\u0007\uc102\u0000\u000a\uc10f\u0001\u0007\uc102\u0000\u000b\uc116\u0004\u0001\u0007\uc102\u0000\u0003\u0001\u0007\uc102\u0000",
486 | Check.EVERY_PICK_DROP_MOVE,
487 | 0,
488 | UNKNOWN,
489 | ) {
490 | val builder = FloorPlan.maze.builder()
491 | var world = builder.world().fillWithBeepers()
492 |
493 | fun generateMaze() {
494 | val angle = Random.nextInt(4)
495 | world = world.pickBeeper().turn(angle)
496 | repeat(4) {
497 | if (world.beeperAhead()) {
498 | builder.tearDownWall(world.position, world.direction)
499 | world = world.moveForward()
500 | generateMaze()
501 | world = world.turnAround()
502 | builder.tearDownWall(world.position, world.direction)
503 | world = world.moveForward().turnAround()
504 | }
505 | world = world.turnLeft()
506 | }
507 | world = world.turn(4 - angle)
508 | }
509 |
510 | generateMaze()
511 | val x = Random.nextInt(10)
512 | val y = Random.nextInt(10)
513 | world.dropBeeper(x, y).withKarelAt(0, 0, EAST)
514 | }
515 |
516 | val quantizeBits = Problem(
517 | "2.4.2",
518 | "quantizeBits",
519 | "Karel the hacker is eavesdropping\non an analog communications line\nand writes down 10 bits encoded\nas 0..5 (0) or 6..10 (1). Convert\nto always 0 (0) or always 10 (1).",
520 | "\u8009\ua104\u0001\u9101\u0007\uc12a\u0002\u0001\u0001\u0001\u0001\u0001\u0007\uc11f\u0008\uc115\u0001\u0008\ud110\u000a\uc119\u0001\u0006\u000a\ud115\u0003\u0001\u000a\ud11a\u0002\u0000\u0003\u0008\ud125\u0001\u0008\uc122\u0001\u0005\u0008\ud125\u0002\u0000",
521 | Check.EVERY_PICK_DROP,
522 | 0,
523 | 11.toBigInteger().pow(10),
524 | ) {
525 | pillars().withKarelAt(0, 9, EAST)
526 | }
527 |
528 | val addFast = Problem(
529 | "2.4.3",
530 | "addFast",
531 | "Karel adds two bytes from the\n1st and 2nd row and stores the\nsum in the 4th row. The 3rd row\nis reserved for the carry bits.\n(Does \"carry the 1\" ring a bell?)",
532 | "\u8008\u0007\ud113\u0001\u0007\ud116\u0001\u0007\ud119\u0001\u0004\u0001\u0004\u0001\u0001\u0001\u0003\u9101\u0000\u0001\u0007\ud124\u0001\u0007\ud127\u0001\u0006\u0004\u0001\u0004\u0001\u0001\u0001\u0003\u9101\u0000\u0001\u0007\ud132\u0001\u0004\u0001\u0004\u0001\u0006\u0001\u0001\u0003\u9101\u0000\u0001\u0006\u0004\u0001\u0004\u0001\u0006\u0001\u0001\u0003\u9101\u0000",
533 | Check.EVERY_PICK_DROP,
534 | 0b1011,
535 | TWO.pow(16),
536 | ) { id ->
537 | randomBytes(WorldEntropy(id), SOUTH)
538 | }
539 |
540 | val partyAgain = Problem(
541 | "3.1.1 \uD83C\uDF88",
542 | "partyAgain",
543 | "Karel is preparing the next big\nparty. Unfortunately, the floor\nis so soaked from the last party\nthat care must be taken not to\nbreak through into the cellar!",
544 | "\u8009\ua104\u0001\u9101\u0002\u0005\ua109\u0002\u0000\u000a\ud10e\u0006\u0003\u0000\u0001\ua109\u0001\u0000",
545 | Check.EVERY_PICK_DROP_MOVE,
546 | 0,
547 | 3.toBigInteger().pow(10),
548 | ) { id ->
549 | val rng = WorldEntropy(id)
550 | val builder = FloorPlan.trap.builder()
551 |
552 | for (x in 0..9) {
553 | builder.buildHorizontalWall(x, 1 + rng.nextInt(3))
554 | }
555 | val world = builder.world().withBeepers(1023L.shl(80 - 64), 0L)
556 | world.withKarelAt(0, 8, EAST)
557 | }
558 |
559 | val fetchTheStars = Problem(
560 | "3.1.2",
561 | "fetchTheStars",
562 | "Karel arranges a romantic date\nwith Taylor on a frozen lake to\n\"fetch the stars from the sky\",\nwhich is German for \"goes to\nthe ends of the world and back\".",
563 | "\u8009\ua104\u0001\u9101\u0002\ua109\u0006\u0002\u0000\u000a\ud10e\u0005\u0003\u0000\u0001\ua109\u0001\u0000",
564 | Check.EVERY_PICK_DROP_MOVE,
565 | 0,
566 | 3.toBigInteger().pow(10),
567 | ) { id ->
568 | val rng = WorldEntropy(id)
569 | val builder = FloorPlan.trap.builder()
570 | var world = builder.world()
571 |
572 | for (x in 0..9) {
573 | val y = 1 + rng.nextInt(3)
574 | builder.buildHorizontalWall(x, y)
575 | world = world.dropBeeper(x, y)
576 | }
577 | world.withKarelAt(0, 8, EAST)
578 | }
579 |
580 | val secureTheCave = Problem(
581 | "3.2.1",
582 | "secureTheCave",
583 | "Karel the cave explorer earns a\nliving as a tourist guide. For\nsafety measures, Karel breaks all\nstalactites from the ceiling and\nre-erects them as stalagmites.",
584 | "\u8009\ua104\u0001\u9101\u0002\ua109\ua10e\u0004\u0000\u0001\u000a\ud109\u0003\u0000\u0007\uc109\u0005\u0001\ua10e\u0006\u0001\u0000",
585 | Check.EVERY_PICK_DROP,
586 | 0,
587 | 9.toBigInteger().pow(10),
588 | ) {
589 | val builder = FloorPlan.empty.builder()
590 | var world = builder.world()
591 |
592 | for (x in 0..9) {
593 | val y = 1 + Random.nextInt(3)
594 | builder.buildHorizontalWall(x, y)
595 | for (a in y..y + Random.nextInt(3)) {
596 | world = world.dropBeeper(x, a)
597 | }
598 | }
599 | world.withKarelAt(0, 9, EAST)
600 | }
601 |
602 | val layAndRemoveTiles = Problem(
603 | "3.2.2",
604 | "layAndRemoveTiles",
605 | "Karel tries a different set of\nflagstones. But again, Taylor\nis not enamored with the result.\nHence Karel immediately removes\nthe flagstones, in reverse order.",
606 | "\u0007\uc104\u0003\u0000\u0006\u0008\ud10e\u000a\uc10e\u0001\ua100\u0001\u0005\u0000\u0002\u0001\ua100\u0001\u0004\u0005\u0000",
607 | Check.EVERY_PICK_DROP,
608 | 0,
609 | ONE,
610 | ) {
611 | emptyWorld.withKarelAt(0, 9, EAST)
612 | }
613 |
614 | val findShelters = Problem(
615 | "3.3.1",
616 | "findShelters",
617 | "Karel is part of an expedition to\nthe north pole. The first task is\nfinding storm-proof shelters.\nMark Karel's path with beepers,\nbut leave the shelters empty!",
618 | "\u8004\u0008\ud111\u000a\uc111\u0001\u0009\ud10c\u000a\ud10c\u000b\uc10e\u0006\ua100\u0003\u0001\u0003\u0002\u9101\u0000",
619 | Check.EVERY_PICK_DROP_MOVE,
620 | 0,
621 | UNKNOWN,
622 | ) {
623 | val builder = FloorPlan.empty.builder()
624 |
625 | repeat(25) {
626 | builder.buildHorizontalWall(Random.nextInt(10), 1 + Random.nextInt(9))
627 | builder.buildVerticalWall(1 + Random.nextInt(9), Random.nextInt(10))
628 | }
629 | val x = Random.nextInt(10)
630 | val y = Random.nextInt(10)
631 | val dir = Random.nextInt(4)
632 | builder.world().withKarelAt(x, y, dir)
633 | }
634 |
635 | val addSmart = Problem(
636 | "3.3.2",
637 | "addSmart",
638 | "Karel adds two bytes from the\n1st and 2nd row and stores the\nsum in the 3rd row. Dropping and\nchecking carry bits is no longer\nnecessary. What a smart robot!",
639 | "\ub10c\u0001\u0001\u0006\u0004\u0001\u0004\u0001\u0001\u0003\u000b\uc129\u0007\ud114\u0008\ud101\u0004\u0001\u0002\ub10a\u0008\uc101\u0004\u0001\u0002\u000b\uc129\u0007\uc114\u0008\uc116\u0001\u0001\u0006\u0004\u0001\u0004\u0001\u0001\u0003\ub119\u0000",
640 | Check.EVERY_PICK_DROP,
641 | 0b111,
642 | TWO.pow(16),
643 | ) { id ->
644 | randomBytes(WorldEntropy(id), SOUTH)
645 | }
646 |
647 | val computeFibonacci = Problem(
648 | "3.3.3",
649 | "computeFibonacci",
650 | "Given 2 Fibonacci numbers,\nKarel computes the next 8.\n\nen.wikipedia.org/wiki/Fibonacci_number\nde.wikipedia.org/wiki/Fibonacci-Folge",
651 | "\u8008\ub10d\u0001\u0001\u0006\u0004\u0001\u0004\u0001\u0001\u0003\u000b\uc12a\u0007\ud115\u0008\ud102\u0004\u0001\u0002\ub10b\u0008\uc102\u0004\u0001\u0002\u000b\uc12a\u0007\uc115\u0008\uc117\u0001\u0001\u0006\u0004\u0001\u0004\u0001\u0001\u0003\ub11a\u0001\u0002\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0004\u910d\u0000",
652 | Check.EVERY_PICK_DROP,
653 | 0b1111111111,
654 | 5.toBigInteger(),
655 | ) { id ->
656 | val world = FloorPlan.binary.world().withKarelAt(9, 0, SOUTH)
657 | when (id % 5) {
658 | 0 -> world.dropBeeper(9, 1) // 0 1
659 | 1 -> world.dropBeeper(9, 0).dropBeeper(9, 1) // 1 1
660 | 2 -> world.dropBeeper(9, 0).dropBeeper(8, 1) // 1 2
661 | 3 -> world.dropBeeper(8, 0).dropBeeper(8, 1).dropBeeper(9, 1) // 2 3
662 | 4 -> world.dropBeeper(8, 0).dropBeeper(9, 0).dropBeeper(7, 1).dropBeeper(9, 1) // 3 5
663 |
664 | else -> error(id)
665 | }
666 | }
667 |
668 | val problems: List = listOf(
669 | karelsFirstProgram,
670 |
671 | obtainArtifact,
672 | defuseOneBomb,
673 | defuseTwoBombs,
674 | practiceHomeRun,
675 | climbTheStairs,
676 | fillTheHoles,
677 | saveTheFlower,
678 | mowTheLawn,
679 | harvestTheField,
680 | repairTheStreet,
681 | cleanTheRoom,
682 | tileTheFloor,
683 | stealOlympicFire,
684 | removeTheTiles,
685 | walkTheLabyrinth,
686 |
687 | hangTheLampions,
688 | followTheSeeds,
689 | cleanTheTunnels,
690 | increment,
691 | decrement,
692 | addSlow,
693 | saveTheFlowers,
694 | findTeddyBear,
695 | jumpTheHurdles,
696 | solveTheMaze,
697 | quantizeBits,
698 | addFast,
699 |
700 | partyAgain,
701 | fetchTheStars,
702 | secureTheCave,
703 | layAndRemoveTiles,
704 | findShelters,
705 | addSmart,
706 | computeFibonacci,
707 | )
708 | }
709 | }
710 |
--------------------------------------------------------------------------------
/src/main/kotlin/logic/World.kt:
--------------------------------------------------------------------------------
1 | package logic
2 |
3 | // The state of the world is stored in just two 64-bit longs:
4 | //
5 | // 56 48 40 32 24 16 8 0
6 | // cppppppp ...cyyyy ...cxxxx .cddbbbb bbbbbbbb bbbbbbbb bbbbbbbb bbbbbbbb hi
7 | // bbbbbbbb bbbbbbbb bbbbbbbb bbbbbbbb bbbbbbbb bbbbbbbb bbbbbbbb bbbbbbbb lo
8 | //
9 | // c : carry
10 | // p : position (0..99)
11 | // y : y position
12 | // x : x position
13 | // d : direction
14 | // b : beepers
15 |
16 | private const val D_SHIFT = 36
17 | private const val X_SHIFT = 40
18 | private const val Y_SHIFT = 48
19 | private const val P_SHIFT = 56
20 |
21 | private const val BEEPERS_HI = 0x0000000fffffffffL
22 | private const val CLEAR_CARRY = 0x7f0f0f3fffffffffL
23 | private const val IGNORING_DIRECTION = 3L.shl(D_SHIFT).inv()
24 |
25 | // E N W S
26 | private val deltaX = intArrayOf(1, 0, -1, 0)
27 | private val deltaY = intArrayOf(0, -1, 0, 1)
28 | private val deltaXYP = longArrayOf(
29 | 1L.shl(X_SHIFT) + 1L.shl(P_SHIFT),
30 | 15L.shl(Y_SHIFT) + 118L.shl(P_SHIFT),
31 | 15L.shl(X_SHIFT) + 127L.shl(P_SHIFT),
32 | 1L.shl(Y_SHIFT) + 10L.shl(P_SHIFT),
33 | )
34 |
35 | class World(private val hi: Long, private val lo: Long, val floorPlan: FloorPlan) {
36 | val beepersLo: Long
37 | get() = lo
38 |
39 | val beepersHi: Long
40 | get() = hi.and(BEEPERS_HI)
41 |
42 | val direction: Int
43 | get() = hi.ushr(D_SHIFT).toInt().and(3)
44 |
45 | val x: Int
46 | get() = hi.ushr(X_SHIFT).toInt().and(15)
47 |
48 | val y: Int
49 | get() = hi.ushr(Y_SHIFT).toInt().and(15)
50 |
51 | val position: Int
52 | get() = hi.ushr(P_SHIFT).toInt()
53 |
54 | fun equalsIgnoringDirection(that: World): Boolean {
55 | return this.lo == that.lo && this.hi.and(IGNORING_DIRECTION) == that.hi.and(IGNORING_DIRECTION)
56 | }
57 |
58 | fun withBeepers(hi: Long, lo: Long): World {
59 | return World(hi, lo, floorPlan)
60 | }
61 |
62 | fun withKarelAt(x: Int, y: Int, direction: Int): World {
63 | val coordinates = (y * 10 + x).toLong().shl(P_SHIFT) or
64 | y.toLong().shl(Y_SHIFT) or
65 | x.toLong().shl(X_SHIFT) or
66 | direction.toLong().shl(D_SHIFT)
67 | return World(hi.and(BEEPERS_HI).or(coordinates), lo, floorPlan)
68 | }
69 |
70 | // BEEPERS
71 |
72 | private fun beeperAt(input: Long, shift: Int): Boolean {
73 | return input.ushr(shift).and(1L) != 0L
74 | }
75 |
76 | private fun pickBeeper(input: Long, shift: Int): Long {
77 | val output = input.and(1L.shl(shift).inv())
78 | if (output == input) throw CellIsEmpty()
79 | return output
80 | }
81 |
82 | private fun dropBeeper(input: Long, shift: Int): Long {
83 | val output = input.or(1L.shl(shift))
84 | if (output == input) throw CellIsFull()
85 | return output
86 | }
87 |
88 | private fun toggleBeeper(input: Long, shift: Int): Long {
89 | return input.xor(1L.shl(shift))
90 | }
91 |
92 | fun beeperAt(x: Int, y: Int): Boolean {
93 | return beeperAt(y * 10 + x)
94 | }
95 |
96 | fun beeperAt(shift: Int): Boolean {
97 | return if (shift >= 64) {
98 | beeperAt(hi, shift)
99 | } else {
100 | beeperAt(lo, shift)
101 | }
102 | }
103 |
104 | fun pickBeeper(x: Int, y: Int): World {
105 | return pickBeeper(y * 10 + x)
106 | }
107 |
108 | fun pickBeeper(shift: Int): World {
109 | return if (shift >= 64) {
110 | World(pickBeeper(hi, shift), lo, floorPlan)
111 | } else {
112 | World(hi, pickBeeper(lo, shift), floorPlan)
113 | }
114 | }
115 |
116 | fun dropBeeper(x: Int, y: Int): World {
117 | return dropBeeper(y * 10 + x)
118 | }
119 |
120 | fun dropBeeper(shift: Int): World {
121 | return if (shift >= 64) {
122 | World(dropBeeper(hi, shift), lo, floorPlan)
123 | } else {
124 | World(hi, dropBeeper(lo, shift), floorPlan)
125 | }
126 | }
127 |
128 | fun toggleBeeper(x: Int, y: Int): World {
129 | return toggleBeeper(y * 10 + x)
130 | }
131 |
132 | fun toggleBeeper(shift: Int): World {
133 | return if (shift >= 64) {
134 | World(toggleBeeper(hi, shift), lo, floorPlan)
135 | } else {
136 | World(hi, toggleBeeper(lo, shift), floorPlan)
137 | }
138 | }
139 |
140 | fun fillWithBeepers(): World {
141 | return World(hi.or(BEEPERS_HI), -1, floorPlan)
142 | }
143 |
144 | fun countBeepers(): Int {
145 | return beepersHi.countOneBits() + beepersLo.countOneBits()
146 | }
147 |
148 | fun firstByte(): Int {
149 | return lo.reverseByte(2)
150 | }
151 |
152 | fun secondByte(): Int {
153 | return lo.reverseByte(12)
154 | }
155 |
156 | fun thirdByte(): Int {
157 | return lo.reverseByte(22)
158 | }
159 |
160 | fun fourthByte(): Int {
161 | return lo.reverseByte(32)
162 | }
163 |
164 | fun allBytes(): IntArray {
165 | return intArrayOf(
166 | lo.reverseByte(2),
167 | lo.reverseByte(12),
168 | lo.reverseByte(22),
169 | lo.reverseByte(32),
170 | lo.reverseByte(42),
171 | lo.reverseByte(52),
172 |
173 | (lo.ushr(62) or hi.shl(2)).reverseByte(0),
174 |
175 | hi.reverseByte(8),
176 | hi.reverseByte(18),
177 | hi.reverseByte(28),
178 | )
179 | }
180 |
181 | fun countBeepersInColumn(x: Int): Int {
182 | return (0..9).count { y -> beeperAt(x, y) }
183 | }
184 |
185 | // KAREL
186 |
187 | fun leftIsClear(): Boolean {
188 | return floorPlan.isClear(position, (direction + 1).and(3))
189 | }
190 |
191 | fun frontIsClear(): Boolean {
192 | return floorPlan.isClear(position, direction)
193 | }
194 |
195 | fun rightIsClear(): Boolean {
196 | return floorPlan.isClear(position, (direction + 3).and(3))
197 | }
198 |
199 | fun moveForward(): World {
200 | if (!frontIsClear()) throw BlockedByWall()
201 | return World((hi + deltaXYP[direction]).and(CLEAR_CARRY), lo, floorPlan)
202 | }
203 |
204 | fun turn(delta: Int): World {
205 | return World((hi + delta.toLong().shl(D_SHIFT)).and(CLEAR_CARRY), lo, floorPlan)
206 | }
207 |
208 | fun turnLeft(): World {
209 | return turn(1)
210 | }
211 |
212 | fun turnAround(): World {
213 | return turn(2)
214 | }
215 |
216 | fun turnRight(): World {
217 | return turn(3)
218 | }
219 |
220 | fun onBeeper(): Boolean {
221 | return beeperAt(position)
222 | }
223 |
224 | fun beeperAhead(): Boolean {
225 | val x = this.x + deltaX[direction]
226 | val y = this.y + deltaY[direction]
227 | return isInsideWorld(x, y) && beeperAt(x, y)
228 | }
229 |
230 | fun isInsideWorld(x: Int, y: Int): Boolean {
231 | return (0 <= x && x < 10) && (0 <= y && y < 10)
232 | }
233 |
234 | fun pickBeeper(): World {
235 | return pickBeeper(position)
236 | }
237 |
238 | fun dropBeeper(): World {
239 | return dropBeeper(position)
240 | }
241 | }
242 |
243 | private fun Long.reverseByte(shift: Int): Int {
244 | val byte = this.ushr(shift).toInt().and(255)
245 | val reverse =
246 | "\u0000\u0080\u0040\u00c0\u0020\u00a0\u0060\u00e0\u0010\u0090\u0050\u00d0\u0030\u00b0\u0070\u00f0\u0008\u0088\u0048\u00c8\u0028\u00a8\u0068\u00e8\u0018\u0098\u0058\u00d8\u0038\u00b8\u0078\u00f8\u0004\u0084\u0044\u00c4\u0024\u00a4\u0064\u00e4\u0014\u0094\u0054\u00d4\u0034\u00b4\u0074\u00f4\u000c\u008c\u004c\u00cc\u002c\u00ac\u006c\u00ec\u001c\u009c\u005c\u00dc\u003c\u00bc\u007c\u00fc\u0002\u0082\u0042\u00c2\u0022\u00a2\u0062\u00e2\u0012\u0092\u0052\u00d2\u0032\u00b2\u0072\u00f2\u000a\u008a\u004a\u00ca\u002a\u00aa\u006a\u00ea\u001a\u009a\u005a\u00da\u003a\u00ba\u007a\u00fa\u0006\u0086\u0046\u00c6\u0026\u00a6\u0066\u00e6\u0016\u0096\u0056\u00d6\u0036\u00b6\u0076\u00f6\u000e\u008e\u004e\u00ce\u002e\u00ae\u006e\u00ee\u001e\u009e\u005e\u00de\u003e\u00be\u007e\u00fe\u0001\u0081\u0041\u00c1\u0021\u00a1\u0061\u00e1\u0011\u0091\u0051\u00d1\u0031\u00b1\u0071\u00f1\u0009\u0089\u0049\u00c9\u0029\u00a9\u0069\u00e9\u0019\u0099\u0059\u00d9\u0039\u00b9\u0079\u00f9\u0005\u0085\u0045\u00c5\u0025\u00a5\u0065\u00e5\u0015\u0095\u0055\u00d5\u0035\u00b5\u0075\u00f5\u000d\u008d\u004d\u00cd\u002d\u00ad\u006d\u00ed\u001d\u009d\u005d\u00dd\u003d\u00bd\u007d\u00fd\u0003\u0083\u0043\u00c3\u0023\u00a3\u0063\u00e3\u0013\u0093\u0053\u00d3\u0033\u00b3\u0073\u00f3\u000b\u008b\u004b\u00cb\u002b\u00ab\u006b\u00eb\u001b\u009b\u005b\u00db\u003b\u00bb\u007b\u00fb\u0007\u0087\u0047\u00c7\u0027\u00a7\u0067\u00e7\u0017\u0097\u0057\u00d7\u0037\u00b7\u0077\u00f7\u000f\u008f\u004f\u00cf\u002f\u00af\u006f\u00ef\u001f\u009f\u005f\u00df\u003f\u00bf\u007f\u00ff"
247 | return reverse[byte].code
248 | }
249 |
--------------------------------------------------------------------------------
/src/main/kotlin/logic/WorldEntropy.kt:
--------------------------------------------------------------------------------
1 | package logic
2 |
3 | class WorldEntropy(private var entropy: Int) {
4 | fun nextBoolean(): Boolean {
5 | val lsb = entropy.and(1)
6 | entropy = entropy.ushr(1)
7 | return lsb != 0
8 | }
9 |
10 | fun nextInt(bound: Int): Int {
11 | val result = entropy % bound
12 | entropy /= bound
13 | return result
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/src/main/kotlin/syntax/lexer/Lexer.kt:
--------------------------------------------------------------------------------
1 | package syntax.lexer
2 |
3 | import freditor.persistent.StringedValueMap
4 | import syntax.lexer.TokenKind.*
5 |
6 | class Lexer(input: String) : LexerBase(input) {
7 |
8 | tailrec fun nextToken(): Token {
9 | startAtIndex()
10 | return when (current) {
11 | ' ', '\u0009', '\u000a', '\u000b', '\u000c', '\u000d' -> {
12 | next()
13 | nextToken()
14 | }
15 |
16 | '/' -> when (next()) {
17 | '/' -> {
18 | skipSingleLineComment()
19 | nextToken()
20 | }
21 |
22 | '*' -> {
23 | skipMultiLineComment()
24 | nextToken()
25 | }
26 |
27 | else -> error("comments start with // or /*")
28 | }
29 |
30 | '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' -> number()
31 |
32 | '(' -> nextVerbatim(OPENING_PAREN)
33 | ')' -> nextVerbatim(CLOSING_PAREN)
34 | ';' -> nextVerbatim(SEMICOLON)
35 | '{' -> nextVerbatim(OPENING_BRACE)
36 | '}' -> nextVerbatim(CLOSING_BRACE)
37 |
38 | '!' -> nextVerbatim(BANG)
39 |
40 | '&' -> {
41 | if (next() != '&') error("logical and is &&")
42 | nextVerbatim(AMPERSAND_AMPERSAND)
43 | }
44 |
45 | '|' -> {
46 | if (next() != '|') error("logical or is ||")
47 | nextVerbatim(BAR_BAR)
48 | }
49 |
50 | 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z',
51 | 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z',
52 | '_' -> identifierOrKeyword()
53 |
54 | EOF -> verbatim(END_OF_INPUT)
55 |
56 | else -> error("illegal character $current")
57 | }
58 | }
59 |
60 | private fun skipSingleLineComment() {
61 | while (next() != '\n') {
62 | if (current == EOF) return
63 | }
64 | next() // skip '\n'
65 | }
66 |
67 | private fun skipMultiLineComment() {
68 | next() // skip '*'
69 | val afterAsterisk = index
70 | do {
71 | if (current == EOF) {
72 | val voidIndex = input.indexOf("void", afterAsterisk)
73 | if (voidIndex != -1) {
74 | index = voidIndex
75 | }
76 | error("/* starts a green multi-line comment, but no */ was found to end it.\nPlease insert */ where you intend to end the comment.")
77 | }
78 | } while ((current != '*') or (next() != '/'))
79 | next() // skip '/'
80 | }
81 |
82 | private tailrec fun number(): Token = when (next()) {
83 | '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' -> number()
84 |
85 | else -> token(NUMBER)
86 | }
87 |
88 | private tailrec fun identifierOrKeyword(): Token = when (next()) {
89 | 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z',
90 | 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z',
91 | '_', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' -> identifierOrKeyword()
92 |
93 | else -> {
94 | val lexeme = lexeme()
95 | when (val value: Any? = identifiersOrKeywords[lexeme]) {
96 | is TokenKind -> verbatim(value)
97 |
98 | is String -> token(IDENTIFIER, value)
99 |
100 | else -> {
101 | identifiersOrKeywords = identifiersOrKeywords.put(lexeme)
102 | token(IDENTIFIER, lexeme)
103 | }
104 | }
105 | }
106 | }
107 |
108 | @Suppress("UNCHECKED_CAST")
109 | private var identifiersOrKeywords = keywords as StringedValueMap
110 | }
111 |
--------------------------------------------------------------------------------
/src/main/kotlin/syntax/lexer/LexerBase.kt:
--------------------------------------------------------------------------------
1 | package syntax.lexer
2 |
3 | import common.Diagnostic
4 |
5 | const val EOF = '\u0000'
6 |
7 | abstract class LexerBase(protected val input: String) {
8 |
9 | var start: Int = -1
10 | private set
11 |
12 | var index: Int = -1
13 | protected set
14 |
15 | fun startAtIndex() {
16 | start = index
17 | }
18 |
19 | var current: Char = next()
20 | private set
21 |
22 | fun next(): Char {
23 | current = if (++index < input.length) input[index] else EOF
24 | return current
25 | }
26 |
27 | protected fun lexeme(): String {
28 | return input.substring(start, index)
29 | }
30 |
31 | protected fun token(kind: TokenKind): Token {
32 | return token(kind, lexeme())
33 | }
34 |
35 | protected fun token(kind: TokenKind, lexeme: String): Token {
36 | return Token(kind, start, lexeme)
37 | }
38 |
39 | protected fun verbatim(kind: TokenKind): Token {
40 | return token(kind, kind.lexeme)
41 | }
42 |
43 | protected fun nextVerbatim(kind: TokenKind): Token {
44 | next()
45 | return verbatim(kind)
46 | }
47 |
48 | protected fun error(message: String): Nothing {
49 | throw Diagnostic(index, message)
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/src/main/kotlin/syntax/lexer/Token.kt:
--------------------------------------------------------------------------------
1 | package syntax.lexer
2 |
3 | import common.Diagnostic
4 |
5 | class Token(val kind: TokenKind, val start: Int, val lexeme: String) {
6 |
7 | val end: Int
8 | get() = start + lexeme.length
9 |
10 | fun error(message: String): Nothing {
11 | throw Diagnostic(start, message)
12 | }
13 |
14 | fun error(message: String, startDelta: Int): Nothing {
15 | throw Diagnostic(start + startDelta, message)
16 | }
17 |
18 | override fun toString(): String = kind.toString()
19 |
20 | fun toInt(range: IntRange): Int {
21 | try {
22 | val n = lexeme.toInt()
23 | if (n in range) return n
24 | } catch (_: NumberFormatException) {
25 | // intentional fallthrough
26 | }
27 | error("$lexeme out of range $range")
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/src/main/kotlin/syntax/lexer/TokenKind.kt:
--------------------------------------------------------------------------------
1 | package syntax.lexer
2 |
3 | import freditor.persistent.StringedValueMap
4 |
5 | enum class TokenKind(val lexeme: String) {
6 | VOID("void"),
7 | REPEAT("repeat"),
8 | IF("if"),
9 | ELSE("else"),
10 | WHILE("while"),
11 |
12 | OPENING_PAREN("("),
13 | CLOSING_PAREN(")"),
14 | OPENING_BRACE("{"),
15 | CLOSING_BRACE("}"),
16 | SEMICOLON(";"),
17 | BANG("!"),
18 | AMPERSAND_AMPERSAND("&&"),
19 | BAR_BAR("||"),
20 |
21 | NUMBER("NUMBER"),
22 | IDENTIFIER("IDENTIFIER"),
23 | END_OF_INPUT("END OF INPUT");
24 |
25 | override fun toString(): String = lexeme
26 |
27 | val isKeyword: Boolean
28 | get() = lexeme.first().isLowerCase()
29 | }
30 |
31 | val keywords: StringedValueMap = TokenKind.entries.filter(TokenKind::isKeyword)
32 | .fold(StringedValueMap.empty(), StringedValueMap::put)
33 |
--------------------------------------------------------------------------------
/src/main/kotlin/syntax/parser/Conditions.kt:
--------------------------------------------------------------------------------
1 | package syntax.parser
2 |
3 | import freditor.Levenshtein
4 | import syntax.lexer.TokenKind.*
5 | import syntax.tree.*
6 |
7 | fun Parser.disjunction(): Condition {
8 | val left = conjunction()
9 | return if (current != BAR_BAR) {
10 | left
11 | } else {
12 | Disjunction(left, accept(), disjunction())
13 | }
14 | }
15 |
16 | fun Parser.conjunction(): Condition {
17 | val left = primaryCondition()
18 | return if (current != AMPERSAND_AMPERSAND) {
19 | left
20 | } else {
21 | Conjunction(left, accept(), conjunction())
22 | }
23 | }
24 |
25 | val PREDICATES = listOf("onBeeper", "beeperAhead", "leftIsClear", "frontIsClear", "rightIsClear")
26 |
27 | fun Parser.primaryCondition(): Condition = when (current) {
28 | IDENTIFIER -> when (token.lexeme) {
29 |
30 | "onBeeper" -> OnBeeper(accept().emptyParens())
31 | "beeperAhead" -> BeeperAhead(accept().emptyParens())
32 | "leftIsClear" -> LeftIsClear(accept().emptyParens())
33 | "frontIsClear" -> FrontIsClear(accept().emptyParens())
34 | "rightIsClear" -> RightIsClear(accept().emptyParens())
35 |
36 | else -> {
37 | val bestMatches = Levenshtein.bestMatches(token.lexeme, PREDICATES)
38 | if (bestMatches.size == 1) {
39 | val bestMatch = bestMatches.first()
40 | val prefix = bestMatch.commonPrefixWith(token.lexeme)
41 | token.error("Did you mean $bestMatch?", prefix.length)
42 | } else {
43 | val commaSeparated = bestMatches.joinToString(", ")
44 | token.error("Did you mean $commaSeparated?")
45 | }
46 | }
47 | }
48 |
49 | BANG -> Not(accept(), primaryCondition())
50 |
51 | OPENING_PAREN -> parenthesized(::disjunction)
52 |
53 | else -> illegalStartOf("condition")
54 | }
55 |
--------------------------------------------------------------------------------
/src/main/kotlin/syntax/parser/Parser.kt:
--------------------------------------------------------------------------------
1 | package syntax.parser
2 |
3 | import common.Diagnostic
4 | import syntax.lexer.Lexer
5 | import syntax.lexer.Token
6 | import syntax.lexer.TokenKind
7 | import syntax.lexer.TokenKind.*
8 |
9 | class Parser(private val lexer: Lexer) {
10 | var previous: Token = lexer.nextToken()
11 | private set
12 |
13 | var token: Token = previous
14 | private set
15 |
16 | var current: TokenKind = token.kind
17 | private set
18 |
19 | var lookahead: Token = lexer.nextToken()
20 | private set
21 |
22 | fun next(): TokenKind {
23 | previous = token
24 | token = lookahead
25 | current = token.kind
26 | lookahead = lexer.nextToken()
27 | return current
28 | }
29 |
30 | fun accept(): Token {
31 | val result = token
32 | next()
33 | return result
34 | }
35 |
36 | fun expect(expected: TokenKind): Token {
37 | if (current != expected) throw Diagnostic(previous.end, "missing $expected")
38 | return accept()
39 | }
40 |
41 | fun T.emptyParens(): T {
42 | expect(OPENING_PAREN)
43 | expect(CLOSING_PAREN)
44 | return this
45 | }
46 |
47 | fun T.semicolon(): T {
48 | expect(SEMICOLON)
49 | return this
50 | }
51 |
52 | fun illegalStartOf(rule: String): Nothing {
53 | token.error("illegal start of $rule")
54 | }
55 |
56 | inline fun list1While(proceed: () -> Boolean, parse: () -> T): List {
57 | val list = mutableListOf(parse())
58 | while (proceed()) {
59 | list.add(parse())
60 | }
61 | return list
62 | }
63 |
64 | inline fun list0While(proceed: () -> Boolean, parse: () -> T): List {
65 | return if (!proceed()) {
66 | emptyList()
67 | } else {
68 | list1While(proceed, parse)
69 | }
70 | }
71 |
72 | inline fun list1Until(terminator: TokenKind, parse: () -> T): List {
73 | return list1While({ current != terminator }, parse)
74 | }
75 |
76 | inline fun list0Until(terminator: TokenKind, parse: () -> T): List {
77 | return list0While({ current != terminator }, parse)
78 | }
79 |
80 | inline fun parenthesized(parse: () -> T): T {
81 | expect(OPENING_PAREN)
82 | val result = parse()
83 | expect(CLOSING_PAREN)
84 | return result
85 | }
86 |
87 | inline fun optional(indicator: TokenKind, parse: () -> T): T? {
88 | return if (current != indicator) {
89 | null
90 | } else {
91 | next()
92 | parse()
93 | }
94 | }
95 |
96 | val sema = Sema()
97 | }
98 |
--------------------------------------------------------------------------------
/src/main/kotlin/syntax/parser/Sema.kt:
--------------------------------------------------------------------------------
1 | package syntax.parser
2 |
3 | import freditor.Levenshtein
4 | import syntax.tree.Call
5 | import syntax.tree.Command
6 | import syntax.tree.Program
7 |
8 | val BUILTIN_COMMANDS = setOf("moveForward", "turnLeft", "turnAround", "turnRight", "pickBeeper", "dropBeeper")
9 |
10 | class Sema {
11 | private val commands = LinkedHashMap()
12 | private val calls = ArrayList()
13 |
14 | fun previousCommandName(): String = commands.keys.last()
15 |
16 | fun command(name: String): Command? = commands[name]
17 |
18 | operator fun invoke(command: Command): Command {
19 | if (commands.containsKey(command.identifier.lexeme)) {
20 | command.identifier.error("duplicate command ${command.identifier.lexeme}")
21 | }
22 | if (BUILTIN_COMMANDS.contains(command.identifier.lexeme)) {
23 | command.identifier.error("cannot redefine builtin command ${command.identifier.lexeme}")
24 | }
25 | commands[command.identifier.lexeme] = command
26 | return command
27 | }
28 |
29 | operator fun invoke(call: Call): Call {
30 | if (!BUILTIN_COMMANDS.contains(call.target.lexeme)) {
31 | calls.add(call)
32 | }
33 | return call
34 | }
35 |
36 | operator fun invoke(program: Program): Program {
37 | for (call in calls) {
38 | if (!commands.containsKey(call.target.lexeme)) {
39 | val bestMatches = Levenshtein.bestMatches(call.target.lexeme, commands.keys + BUILTIN_COMMANDS)
40 | if (bestMatches.size == 1) {
41 | val bestMatch = bestMatches.first()
42 | val prefix = bestMatch.commonPrefixWith(call.target.lexeme)
43 | call.target.error("Did you mean $bestMatch?", prefix.length)
44 | } else {
45 | val commaSeparated = bestMatches.joinToString(", ")
46 | call.target.error("Did you mean $commaSeparated?")
47 | }
48 | }
49 | }
50 | return program
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/src/main/kotlin/syntax/parser/Statements.kt:
--------------------------------------------------------------------------------
1 | package syntax.parser
2 |
3 | import syntax.lexer.TokenKind.*
4 | import syntax.tree.*
5 |
6 | const val EXPECTED_VOID = """Command definitions look like this:
7 |
8 | void commandNameHere()
9 | {
10 | // your code here
11 |
12 | }"""
13 |
14 | private var currentCommandName = ""
15 |
16 | fun Parser.program(): Program {
17 | if (current != VOID) token.error(EXPECTED_VOID)
18 |
19 | return sema(Program(list1Until(END_OF_INPUT, ::command)))
20 | }
21 |
22 | fun Parser.command(): Command {
23 | val previousClosingBrace = previous
24 |
25 | return when (current) {
26 | VOID -> {
27 | val void = accept()
28 | val identifier = expect(IDENTIFIER).emptyParens()
29 | currentCommandName = identifier.lexeme
30 | sema(Command(void, identifier, block()))
31 | }
32 |
33 | CLOSING_BRACE -> token.error("remove }\n\nThis closing brace has no opening partner")
34 |
35 | REPEAT, WHILE, IF -> previousClosingBrace.error("|\n|\n|\nremove }\n\nThe command ${sema.previousCommandName()}() ends here,\nbut the following $current-statement\nstill belongs inside a command")
36 |
37 | IDENTIFIER -> {
38 | val identifier = accept().emptyParens()
39 | when (current) {
40 | SEMICOLON -> previousClosingBrace.error("|\n|\n|\nremove }\n\nThe command ${sema.previousCommandName()}() ends here,\nbut the following call ${identifier.lexeme}();\nstill belongs inside a command")
41 |
42 | else -> identifier.error(EXPECTED_VOID)
43 | }
44 | }
45 |
46 | else -> token.error(EXPECTED_VOID)
47 | }
48 | }
49 |
50 | fun Parser.block(): Block {
51 | return Block(expect(OPENING_BRACE), list0Until(CLOSING_BRACE, ::statement), accept())
52 | }
53 |
54 | fun Parser.statement(): Statement = when (current) {
55 | IDENTIFIER -> sema(Call(accept().emptyParens()).semicolon())
56 |
57 | REPEAT -> Repeat(accept(), parenthesized { expect(NUMBER).toInt(2..4095) }, block())
58 |
59 | WHILE -> While(accept(), parenthesized(::disjunction), block())
60 |
61 | IF -> ifThenElse()
62 |
63 | VOID -> {
64 | val void = accept()
65 | val identifier = expect(IDENTIFIER).emptyParens()
66 | when (current) {
67 | OPENING_BRACE -> void.error("missing }\n\nCannot define command ${identifier.lexeme}()\nINSIDE command $currentCommandName()\n\nCommand definitions do not nest")
68 |
69 | else -> void.error("remove void\n\nYou want to CALL ${identifier.lexeme}(), not DEFINE it, right?")
70 | }
71 | }
72 |
73 | END_OF_INPUT -> token.error("missing }")
74 |
75 | else -> illegalStartOf("statement")
76 | }
77 |
78 | fun Parser.ifThenElse(): IfThenElse {
79 | return IfThenElse(expect(IF), parenthesized(::disjunction), block(), optional(ELSE) {
80 | when (current) {
81 | OPENING_BRACE -> block()
82 |
83 | IF -> ifThenElse()
84 |
85 | else -> previous.error("else must be followed by { or if")
86 | }
87 | })
88 | }
89 |
--------------------------------------------------------------------------------
/src/main/kotlin/syntax/tree/Nodes.kt:
--------------------------------------------------------------------------------
1 | package syntax.tree
2 |
3 | import syntax.lexer.Token
4 |
5 |
6 | sealed interface ElseBranch
7 |
8 |
9 | class Program(val commands: List)
10 |
11 | class Command(val void: Token, val identifier: Token, val body: Block)
12 |
13 | class Block(val openingBrace: Token, val statements: List, val closingBrace: Token) :
14 | ElseBranch
15 |
16 |
17 | sealed class Statement
18 |
19 | class Call(val target: Token) : Statement()
20 |
21 | class Repeat(val repeat: Token, val times: Int, val body: Block) : Statement()
22 |
23 | class IfThenElse(val iF: Token, val condition: Condition, val th3n: Block, val e1se: ElseBranch?) : Statement(),
24 | ElseBranch
25 |
26 | class While(val whi1e: Token, val condition: Condition, val body: Block) : Statement()
27 |
28 |
29 | sealed class Condition
30 |
31 | class OnBeeper(val onBeeper: Token) : Condition()
32 |
33 | class BeeperAhead(val beeperAhead: Token) : Condition()
34 |
35 | class LeftIsClear(val leftIsClear: Token) : Condition()
36 |
37 | class FrontIsClear(val frontIsClear: Token) : Condition()
38 |
39 | class RightIsClear(val rightIsClear: Token) : Condition()
40 |
41 | class Not(val not: Token, val p: Condition) : Condition()
42 |
43 | class Conjunction(val p: Condition, val and: Token, val q: Condition) : Condition()
44 |
45 | class Disjunction(val p: Condition, val or: Token, val q: Condition) : Condition()
46 |
--------------------------------------------------------------------------------
/src/main/kotlin/vm/Emitter.kt:
--------------------------------------------------------------------------------
1 | package vm
2 |
3 | import common.subList
4 | import syntax.lexer.Token
5 | import syntax.parser.Sema
6 | import syntax.tree.*
7 |
8 | class Emitter(private val sema: Sema, instrument: Boolean) {
9 |
10 | private val instrumentOffset: Int = if (instrument) ON_BEEPER_INSTRUMENT - ON_BEEPER else 0
11 |
12 | private val program: MutableList = createInstructionBuffer()
13 |
14 | private val pc: Int
15 | get() = program.size
16 |
17 | private fun emitInstruction(bytecode: Int, token: Token): Instruction {
18 | val instruction = Instruction(bytecode, token.start)
19 | program.add(instruction)
20 | return instruction
21 | }
22 |
23 | private fun emitBranch(predicate: Int, token: Token, branch: Int, label: Label, control: Token) {
24 | emitInstruction(predicate + instrumentOffset, token)
25 | emitInstruction(branch, control).label = label
26 | }
27 |
28 | private fun resolveLabels() {
29 | program.subList(ENTRY_POINT).forEach(Instruction::resolveLabel)
30 | }
31 |
32 | fun emit(main: Command): List {
33 | commandLabels[main] = Label()
34 | main.emit()
35 | while (toEmit.isNotEmpty()) {
36 | toEmit.removeFirst().emit()
37 | }
38 | resolveLabels()
39 | return program
40 | }
41 |
42 | private val commandLabels = HashMap()
43 |
44 | private val toEmit = ArrayDeque()
45 |
46 | private fun Command.emit() {
47 | commandLabels[this]!!.address = pc
48 | body.emit()
49 | emitInstruction(RETURN, body.closingBrace)
50 | }
51 |
52 | private fun Block.emit() {
53 | for (statement in statements) {
54 | statement.emit()
55 | }
56 | }
57 |
58 | private fun Statement.emit() {
59 | when (this) {
60 | is Call -> {
61 | val builtin = builtinCommands[target.lexeme]
62 | if (builtin != null) {
63 | emitInstruction(builtin, target)
64 | } else {
65 | val command = sema.command(target.lexeme)!!
66 | emitInstruction(CALL, target).label = commandLabels.getOrPut(command) {
67 | toEmit.addLast(command)
68 | Label()
69 | }
70 | }
71 | }
72 |
73 | is Repeat -> {
74 | emitInstruction(PUSH + times, repeat)
75 | val back = pc
76 | body.emit()
77 | emitInstruction(LOOP + back, body.closingBrace)
78 | }
79 |
80 | is While -> {
81 | val thenLabel = Label()
82 | val elseLabel = Label()
83 | val back = pc
84 | condition.emitPositive(thenLabel, elseLabel, ELSE, elseLabel, whi1e)
85 | thenLabel.address = pc
86 | body.emit()
87 | emitInstruction(JUMP + back, body.closingBrace)
88 | elseLabel.address = pc
89 | }
90 |
91 | is IfThenElse -> emit()
92 | }
93 | }
94 |
95 | private fun IfThenElse.emit() {
96 | if (e1se == null) {
97 | val thenLabel = Label()
98 | val elseLabel = Label()
99 | condition.emitPositive(thenLabel, elseLabel, ELSE, elseLabel, iF)
100 | thenLabel.address = pc
101 | th3n.emit()
102 | elseLabel.address = pc
103 | } else {
104 | val thenLabel = Label()
105 | val elseLabel = Label()
106 | val doneLabel = Label()
107 | condition.emitPositive(thenLabel, elseLabel, ELSE, elseLabel, iF)
108 | thenLabel.address = pc
109 | th3n.emit()
110 | emitInstruction(JUMP, th3n.closingBrace).label = doneLabel
111 | elseLabel.address = pc
112 | e1se.emit()
113 | doneLabel.address = pc
114 | }
115 | }
116 |
117 | private fun ElseBranch.emit() {
118 | when (this) {
119 | is Block -> emit()
120 |
121 | is IfThenElse -> emit()
122 | }
123 | }
124 |
125 | private fun Condition.emitPositive(thenLabel: Label, elseLabel: Label, branch: Int, label: Label, control: Token) {
126 | when (this) {
127 | is OnBeeper -> emitBranch(ON_BEEPER, onBeeper, branch, label, control)
128 | is BeeperAhead -> emitBranch(BEEPER_AHEAD, beeperAhead, branch, label, control)
129 | is LeftIsClear -> emitBranch(LEFT_IS_CLEAR, leftIsClear, branch, label, control)
130 | is FrontIsClear -> emitBranch(FRONT_IS_CLEAR, frontIsClear, branch, label, control)
131 | is RightIsClear -> emitBranch(RIGHT_IS_CLEAR, rightIsClear, branch, label, control)
132 |
133 | is Not -> p.emitNegative(thenLabel, elseLabel, branch xor (ELSE xor THEN), label, control)
134 |
135 | is Conjunction -> { // left && right
136 | val right = Label()
137 | p.emitPositive(right, elseLabel, ELSE, elseLabel, and)
138 | right.address = pc
139 | q.emitPositive(thenLabel, elseLabel, branch, label, control)
140 | }
141 |
142 | is Disjunction -> { // left || right
143 | val right = Label()
144 | p.emitPositive(thenLabel, right, THEN, thenLabel, or)
145 | right.address = pc
146 | q.emitPositive(thenLabel, elseLabel, branch, label, control)
147 | }
148 | }
149 | }
150 |
151 | private fun Condition.emitNegative(thenLabel: Label, elseLabel: Label, branch: Int, label: Label, control: Token) {
152 | when (this) {
153 | is Disjunction -> { // !(left || right) = !left && !right
154 | val right = Label()
155 | p.emitNegative(right, elseLabel, THEN, elseLabel, or)
156 | right.address = pc
157 | q.emitNegative(thenLabel, elseLabel, branch, label, control)
158 | }
159 |
160 | is Conjunction -> { // !(left && right) = !left || !right
161 | val right = Label()
162 | p.emitNegative(thenLabel, right, ELSE, thenLabel, and)
163 | right.address = pc
164 | q.emitNegative(thenLabel, elseLabel, branch, label, control)
165 | }
166 |
167 | is Not -> p.emitPositive(thenLabel, elseLabel, branch xor (ELSE xor THEN), label, control)
168 |
169 | is OnBeeper -> emitBranch(ON_BEEPER, onBeeper, branch, label, control)
170 | is BeeperAhead -> emitBranch(BEEPER_AHEAD, beeperAhead, branch, label, control)
171 | is LeftIsClear -> emitBranch(LEFT_IS_CLEAR, leftIsClear, branch, label, control)
172 | is FrontIsClear -> emitBranch(FRONT_IS_CLEAR, frontIsClear, branch, label, control)
173 | is RightIsClear -> emitBranch(RIGHT_IS_CLEAR, rightIsClear, branch, label, control)
174 | }
175 | }
176 | }
177 |
--------------------------------------------------------------------------------
/src/main/kotlin/vm/IllegalBytecode.kt:
--------------------------------------------------------------------------------
1 | package vm
2 |
3 | class IllegalBytecode(bytecode: Int) : Exception("%04x".format(bytecode))
4 |
--------------------------------------------------------------------------------
/src/main/kotlin/vm/Instruction.kt:
--------------------------------------------------------------------------------
1 | package vm
2 |
3 | import common.Diagnostic
4 | import freditor.persistent.ChampMap
5 |
6 | class Instruction(var bytecode: Int, val position: Int) {
7 |
8 | val category: Int
9 | get() = bytecode.and(0xf000)
10 |
11 | val target: Int
12 | get() = bytecode.and(0x0fff)
13 |
14 | var label: Label? = null
15 |
16 | fun resolveLabel() {
17 | label?.apply {
18 | bytecode = category + address
19 | label = null
20 | }
21 | }
22 |
23 | val compiledFromSource: Boolean
24 | get() = position > 0
25 |
26 | fun shouldPause(): Boolean {
27 | return when (bytecode) {
28 | RETURN -> compiledFromSource
29 |
30 | MOVE_FORWARD, TURN_LEFT, TURN_AROUND, TURN_RIGHT, PICK_BEEPER, DROP_BEEPER -> true
31 |
32 | ON_BEEPER, BEEPER_AHEAD, LEFT_IS_CLEAR, FRONT_IS_CLEAR, RIGHT_IS_CLEAR -> compiledFromSource
33 |
34 | else -> compiledFromSource && (category < JUMP)
35 | }
36 | }
37 |
38 | fun mnemonic(): String {
39 | return when (bytecode) {
40 | RETURN -> "RET"
41 |
42 | MOVE_FORWARD -> "MOVE"
43 | TURN_LEFT -> "TRNL"
44 | TURN_AROUND -> "TRNA"
45 | TURN_RIGHT -> "TRNR"
46 | PICK_BEEPER -> "PICK"
47 | DROP_BEEPER -> "DROP"
48 |
49 | ON_BEEPER, ON_BEEPER_INSTRUMENT, ON_BEEPER_FALSE, ON_BEEPER_TRUE -> "BEEP"
50 | BEEPER_AHEAD, BEEPER_AHEAD_INSTRUMENT, BEEPER_AHEAD_FALSE, BEEPER_AHEAD_TRUE -> "HEAD"
51 | LEFT_IS_CLEAR, LEFT_IS_CLEAR_INSTRUMENT, LEFT_IS_CLEAR_FALSE, LEFT_IS_CLEAR_TRUE -> "LCLR"
52 | FRONT_IS_CLEAR, FRONT_IS_CLEAR_INSTRUMENT, FRONT_IS_CLEAR_FALSE, FRONT_IS_CLEAR_TRUE -> "FCLR"
53 | RIGHT_IS_CLEAR, RIGHT_IS_CLEAR_INSTRUMENT, RIGHT_IS_CLEAR_FALSE, RIGHT_IS_CLEAR_TRUE -> "RCLR"
54 |
55 | else -> when (category) {
56 | PUSH -> "PUSH %03x".format(target)
57 | LOOP -> "LOOP %03x".format(target)
58 | CALL -> "CALL %03x".format(target)
59 |
60 | JUMP -> "JUMP %03x".format(target)
61 | ELSE -> "ELSE %03x".format(target)
62 | THEN -> "THEN %03x".format(target)
63 |
64 | else -> throw IllegalBytecode(bytecode)
65 | }
66 | }
67 | }
68 | }
69 |
70 | const val RETURN = 0x0000
71 |
72 | const val MOVE_FORWARD = 0x0001
73 | const val TURN_LEFT = 0x0002
74 | const val TURN_AROUND = 0x0003
75 | const val TURN_RIGHT = 0x0004
76 | const val PICK_BEEPER = 0x0005
77 | const val DROP_BEEPER = 0x0006
78 |
79 | const val ON_BEEPER = 0x0007
80 | const val BEEPER_AHEAD = 0x0008
81 | const val LEFT_IS_CLEAR = 0x0009
82 | const val FRONT_IS_CLEAR = 0x000a
83 | const val RIGHT_IS_CLEAR = 0x000b
84 |
85 | const val ON_BEEPER_INSTRUMENT = 0x0010
86 | const val BEEPER_AHEAD_INSTRUMENT = 0x0011
87 | const val LEFT_IS_CLEAR_INSTRUMENT = 0x0012
88 | const val FRONT_IS_CLEAR_INSTRUMENT = 0x0013
89 | const val RIGHT_IS_CLEAR_INSTRUMENT = 0x0014
90 |
91 | const val ON_BEEPER_FALSE = 0x0015
92 | const val BEEPER_AHEAD_FALSE = 0x0016
93 | const val LEFT_IS_CLEAR_FALSE = 0x0017
94 | const val FRONT_IS_CLEAR_FALSE = 0x0018
95 | const val RIGHT_IS_CLEAR_FALSE = 0x0019
96 |
97 | const val ON_BEEPER_TRUE = 0x001a
98 | const val BEEPER_AHEAD_TRUE = 0x001b
99 | const val LEFT_IS_CLEAR_TRUE = 0x001c
100 | const val FRONT_IS_CLEAR_TRUE = 0x001d
101 | const val RIGHT_IS_CLEAR_TRUE = 0x001e
102 |
103 | const val NORM = 0x0000
104 |
105 | const val PUSH = 0x8000
106 | const val LOOP = 0x9000
107 | const val CALL = 0xa000
108 |
109 | const val JUMP = 0xb000
110 | const val ELSE = 0xc000
111 | const val THEN = 0xd000
112 |
113 | val builtinCommands: ChampMap = ChampMap.of(
114 | "moveForward", MOVE_FORWARD,
115 | "turnLeft", TURN_LEFT,
116 | "turnAround", TURN_AROUND,
117 | "turnRight", TURN_RIGHT,
118 | "pickBeeper", PICK_BEEPER,
119 | "dropBeeper", DROP_BEEPER,
120 | )
121 |
122 | private val basicGoalInstructions = Array(RIGHT_IS_CLEAR + 1) { Instruction(it, 0) }
123 |
124 | fun createInstructionBuffer(): MutableList {
125 | return MutableList(ENTRY_POINT) { basicGoalInstructions[RETURN] }
126 | }
127 |
128 | fun createGoalInstructions(goal: String): List {
129 | return goal.mapTo(createInstructionBuffer()) { char ->
130 | basicGoalInstructions.getOrElse(char.code) { code ->
131 | Instruction(code, 0)
132 | }
133 | }
134 | }
135 |
136 | fun Instruction.error(message: String) {
137 | throw Diagnostic(position, message)
138 | }
139 |
--------------------------------------------------------------------------------
/src/main/kotlin/vm/Label.kt:
--------------------------------------------------------------------------------
1 | package vm
2 |
3 | class Label {
4 | var address = 0
5 | }
6 |
--------------------------------------------------------------------------------
/src/main/kotlin/vm/Stack.kt:
--------------------------------------------------------------------------------
1 | package vm
2 |
3 | sealed class Stack(@JvmField val head: Int, @JvmField val tail: Stack?) {
4 |
5 | class ReturnAddress(head: Int, tail: Stack?) : Stack(head, tail)
6 |
7 | class LoopCounter(head: Int, tail: Stack?) : Stack(head, tail)
8 |
9 | @JvmField
10 | val size: Int = tail.size + 1
11 | }
12 |
13 | val Stack?.size: Int
14 | get() = this?.size ?: 0
15 |
16 | inline fun Stack?.forEach(action: (Stack) -> Unit) {
17 | var stack = this
18 | while (stack != null) {
19 | action(stack)
20 | stack = stack.tail
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/src/main/kotlin/vm/VirtualMachine.kt:
--------------------------------------------------------------------------------
1 | package vm
2 |
3 | import logic.World
4 |
5 | // If "step over" or "step return" do not finish within 1 second,
6 | // we assume the code contains an infinite loop.
7 | const val TIMEOUT = 1_000_000_000L
8 |
9 | // The first instruction starts at address 256.
10 | // This makes it easier to distinguish addresses
11 | // from truth values and loop counters on the stack.
12 | const val ENTRY_POINT = 256
13 |
14 | class VirtualMachine(
15 | private val program: Array,
16 | var world: World,
17 | // callbacks
18 | private val onCall: ((Instruction, Instruction) -> Unit)? = null,
19 | private val onReturn: (() -> Unit)? = null,
20 | private val onPickDrop: ((World) -> Unit)? = null,
21 | private val onMove: ((World) -> Unit)? = null,
22 | ) {
23 |
24 | var pc: Int = ENTRY_POINT
25 | private set
26 |
27 | val currentInstruction: Instruction
28 | get() = program[pc]
29 |
30 | var stack: Stack? = null
31 | private set
32 |
33 | private var callDepth: Int = 0
34 |
35 | private fun pop(): Int {
36 | val stack = this.stack!!
37 | this.stack = stack.tail
38 | return stack.head
39 | }
40 |
41 | fun stepInto(virtualMachineVisible: Boolean) {
42 | executeUnpausedInstructions(virtualMachineVisible)
43 | executeOneInstruction()
44 | executeUnpausedInstructions(virtualMachineVisible)
45 | }
46 |
47 | private fun executeUnpausedInstructions(virtualMachineVisible: Boolean) {
48 | if (!virtualMachineVisible) {
49 | while (!currentInstruction.shouldPause()) {
50 | executeOneInstruction()
51 | }
52 | }
53 | }
54 |
55 | fun stepOver() {
56 | stepUntil(callDepth)
57 | }
58 |
59 | fun stepReturn() {
60 | stepUntil(callDepth - 1)
61 | }
62 |
63 | private fun stepUntil(targetDepth: Int) {
64 | val start = System.nanoTime()
65 | stepInto(false)
66 | while ((callDepth > targetDepth) && (System.nanoTime() - start < TIMEOUT)) {
67 | executeOneInstruction()
68 | }
69 | if (callDepth > targetDepth) {
70 | error("infinite loop detected")
71 | }
72 | }
73 |
74 | fun executeUserProgram() {
75 | val start = System.nanoTime()
76 | while (System.nanoTime() - start < TIMEOUT) {
77 | repeat(1000) {
78 | executeOneInstruction()
79 | }
80 | }
81 | error("infinite loop detected")
82 | }
83 |
84 | fun executeGoalProgram() {
85 | while (true) {
86 | executeOneInstruction()
87 | }
88 | }
89 |
90 | private fun executeOneInstruction() {
91 | with(currentInstruction) {
92 | when (category shr 12) {
93 | NORM shr 12 -> executeBasicInstruction(bytecode)
94 |
95 | PUSH shr 12 -> executePush()
96 | LOOP shr 12 -> executeLoop()
97 | CALL shr 12 -> executeCall()
98 | JUMP shr 12 -> pc = target
99 |
100 | else -> throw IllegalBytecode(bytecode)
101 | }
102 | }
103 | }
104 |
105 | private fun Instruction.executePush() {
106 | stack = Stack.LoopCounter(target, stack)
107 | ++pc
108 | }
109 |
110 | private fun Instruction.executeLoop() {
111 | val remaining = pop() - 1
112 | if (remaining > 0) {
113 | stack = Stack.LoopCounter(remaining, stack)
114 | pc = target
115 | } else {
116 | ++pc
117 | }
118 | }
119 |
120 | private fun Instruction.executeCall() {
121 | onCall?.invoke(this, findReturnInstructionAfter(target))
122 | stack = Stack.ReturnAddress(pc, stack)
123 | ++callDepth
124 | pc = target
125 | }
126 |
127 | private fun findReturnInstructionAfter(start: Int): Instruction {
128 | var index = start
129 | while (program[index].bytecode != RETURN) ++index
130 | return program[index]
131 | }
132 |
133 | object Finished : Exception() {
134 | private fun readResolve(): Any = Finished
135 | }
136 |
137 | private fun executeReturn() {
138 | if (stack == null) throw Finished
139 | onReturn?.invoke()
140 | pc = pop() + 1
141 | --callDepth
142 | }
143 |
144 | private fun executeBasicInstruction(bytecode: Int) {
145 | when (bytecode) {
146 | RETURN -> executeReturn()
147 |
148 | MOVE_FORWARD -> world.moveForward().let { world = it; onMove?.invoke(it); ++pc }
149 | TURN_LEFT -> world.turnLeft().let { world = it; ++pc }
150 | TURN_AROUND -> world.turnAround().let { world = it; ++pc }
151 | TURN_RIGHT -> world.turnRight().let { world = it; ++pc }
152 | PICK_BEEPER -> world.pickBeeper().let { world = it; onPickDrop?.invoke(it); ++pc }
153 | DROP_BEEPER -> world.dropBeeper().let { world = it; onPickDrop?.invoke(it); ++pc }
154 |
155 | ON_BEEPER -> {
156 | val status = world.onBeeper()
157 | with(program[pc + 1]) {
158 | pc = if (status == (category == THEN)) target else pc + 2
159 | }
160 | }
161 |
162 | BEEPER_AHEAD -> {
163 | val status = world.beeperAhead()
164 | with(program[pc + 1]) {
165 | pc = if (status == (category == THEN)) target else pc + 2
166 | }
167 | }
168 |
169 | LEFT_IS_CLEAR -> {
170 | val status = world.leftIsClear()
171 | with(program[pc + 1]) {
172 | pc = if (status == (category == THEN)) target else pc + 2
173 | }
174 | }
175 |
176 | FRONT_IS_CLEAR -> {
177 | val status = world.frontIsClear()
178 | with(program[pc + 1]) {
179 | pc = if (status == (category == THEN)) target else pc + 2
180 | }
181 | }
182 |
183 | RIGHT_IS_CLEAR -> {
184 | val status = world.rightIsClear()
185 | with(program[pc + 1]) {
186 | pc = if (status == (category == THEN)) target else pc + 2
187 | }
188 | }
189 |
190 | // INSTRUMENT
191 |
192 | ON_BEEPER_INSTRUMENT -> {
193 | val status = world.onBeeper()
194 | currentInstruction.bytecode = if (status) ON_BEEPER_TRUE else ON_BEEPER_FALSE
195 | with(program[pc + 1]) {
196 | pc = if (status == (category == THEN)) target else pc + 2
197 | }
198 | }
199 |
200 | BEEPER_AHEAD_INSTRUMENT -> {
201 | val status = world.beeperAhead()
202 | currentInstruction.bytecode = if (status) BEEPER_AHEAD_TRUE else BEEPER_AHEAD_FALSE
203 | with(program[pc + 1]) {
204 | pc = if (status == (category == THEN)) target else pc + 2
205 | }
206 | }
207 |
208 | LEFT_IS_CLEAR_INSTRUMENT -> {
209 | val status = world.leftIsClear()
210 | currentInstruction.bytecode = if (status) LEFT_IS_CLEAR_TRUE else LEFT_IS_CLEAR_FALSE
211 | with(program[pc + 1]) {
212 | pc = if (status == (category == THEN)) target else pc + 2
213 | }
214 | }
215 |
216 | FRONT_IS_CLEAR_INSTRUMENT -> {
217 | val status = world.frontIsClear()
218 | currentInstruction.bytecode = if (status) FRONT_IS_CLEAR_TRUE else FRONT_IS_CLEAR_FALSE
219 | with(program[pc + 1]) {
220 | pc = if (status == (category == THEN)) target else pc + 2
221 | }
222 | }
223 |
224 | RIGHT_IS_CLEAR_INSTRUMENT -> {
225 | val status = world.rightIsClear()
226 | currentInstruction.bytecode = if (status) RIGHT_IS_CLEAR_TRUE else RIGHT_IS_CLEAR_FALSE
227 | with(program[pc + 1]) {
228 | pc = if (status == (category == THEN)) target else pc + 2
229 | }
230 | }
231 |
232 | // FALSE
233 |
234 | ON_BEEPER_FALSE -> {
235 | val status = world.onBeeper()
236 | if (status) currentInstruction.bytecode = ON_BEEPER
237 | with(program[pc + 1]) {
238 | pc = if (status == (category == THEN)) target else pc + 2
239 | }
240 | }
241 |
242 | BEEPER_AHEAD_FALSE -> {
243 | val status = world.beeperAhead()
244 | if (status) currentInstruction.bytecode = BEEPER_AHEAD
245 | with(program[pc + 1]) {
246 | pc = if (status == (category == THEN)) target else pc + 2
247 | }
248 | }
249 |
250 | LEFT_IS_CLEAR_FALSE -> {
251 | val status = world.leftIsClear()
252 | if (status) currentInstruction.bytecode = LEFT_IS_CLEAR
253 | with(program[pc + 1]) {
254 | pc = if (status == (category == THEN)) target else pc + 2
255 | }
256 | }
257 |
258 | FRONT_IS_CLEAR_FALSE -> {
259 | val status = world.frontIsClear()
260 | if (status) currentInstruction.bytecode = FRONT_IS_CLEAR
261 | with(program[pc + 1]) {
262 | pc = if (status == (category == THEN)) target else pc + 2
263 | }
264 | }
265 |
266 | RIGHT_IS_CLEAR_FALSE -> {
267 | val status = world.rightIsClear()
268 | if (status) currentInstruction.bytecode = RIGHT_IS_CLEAR
269 | with(program[pc + 1]) {
270 | pc = if (status == (category == THEN)) target else pc + 2
271 | }
272 | }
273 |
274 | // TRUE
275 |
276 | ON_BEEPER_TRUE -> {
277 | val status = world.onBeeper()
278 | if (!status) currentInstruction.bytecode = ON_BEEPER
279 | with(program[pc + 1]) {
280 | pc = if (status == (category == THEN)) target else pc + 2
281 | }
282 | }
283 |
284 | BEEPER_AHEAD_TRUE -> {
285 | val status = world.beeperAhead()
286 | if (!status) currentInstruction.bytecode = BEEPER_AHEAD
287 | with(program[pc + 1]) {
288 | pc = if (status == (category == THEN)) target else pc + 2
289 | }
290 | }
291 |
292 | LEFT_IS_CLEAR_TRUE -> {
293 | val status = world.leftIsClear()
294 | if (!status) currentInstruction.bytecode = LEFT_IS_CLEAR
295 | with(program[pc + 1]) {
296 | pc = if (status == (category == THEN)) target else pc + 2
297 | }
298 | }
299 |
300 | FRONT_IS_CLEAR_TRUE -> {
301 | val status = world.frontIsClear()
302 | if (!status) currentInstruction.bytecode = FRONT_IS_CLEAR
303 | with(program[pc + 1]) {
304 | pc = if (status == (category == THEN)) target else pc + 2
305 | }
306 | }
307 |
308 | RIGHT_IS_CLEAR_TRUE -> {
309 | val status = world.rightIsClear()
310 | if (!status) currentInstruction.bytecode = RIGHT_IS_CLEAR
311 | with(program[pc + 1]) {
312 | pc = if (status == (category == THEN)) target else pc + 2
313 | }
314 | }
315 |
316 | else -> throw IllegalBytecode(bytecode)
317 | }
318 | }
319 | }
320 |
321 | fun VirtualMachine.error(message: String) {
322 | currentInstruction.error(message)
323 | }
324 |
--------------------------------------------------------------------------------
/src/main/resources/tiles/40/beeper.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fredoverflow/karel/dda1391dc4025ef1023b79787d34dca7534ed828/src/main/resources/tiles/40/beeper.png
--------------------------------------------------------------------------------
/src/main/resources/tiles/40/cross.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fredoverflow/karel/dda1391dc4025ef1023b79787d34dca7534ed828/src/main/resources/tiles/40/cross.png
--------------------------------------------------------------------------------
/src/main/resources/tiles/40/karel.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fredoverflow/karel/dda1391dc4025ef1023b79787d34dca7534ed828/src/main/resources/tiles/40/karel.png
--------------------------------------------------------------------------------
/src/main/resources/tiles/40/wall.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fredoverflow/karel/dda1391dc4025ef1023b79787d34dca7534ed828/src/main/resources/tiles/40/wall.png
--------------------------------------------------------------------------------
/src/main/resources/tiles/64/beeper.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fredoverflow/karel/dda1391dc4025ef1023b79787d34dca7534ed828/src/main/resources/tiles/64/beeper.png
--------------------------------------------------------------------------------
/src/main/resources/tiles/64/cross.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fredoverflow/karel/dda1391dc4025ef1023b79787d34dca7534ed828/src/main/resources/tiles/64/cross.png
--------------------------------------------------------------------------------
/src/main/resources/tiles/64/karel.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fredoverflow/karel/dda1391dc4025ef1023b79787d34dca7534ed828/src/main/resources/tiles/64/karel.png
--------------------------------------------------------------------------------
/src/main/resources/tiles/64/wall.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fredoverflow/karel/dda1391dc4025ef1023b79787d34dca7534ed828/src/main/resources/tiles/64/wall.png
--------------------------------------------------------------------------------
/src/test/kotlin/gui/AutocompletionTest.kt:
--------------------------------------------------------------------------------
1 | package gui
2 |
3 | import org.junit.Assert.assertEquals
4 | import org.junit.Test
5 |
6 | class AutocompletionTest {
7 | @Test
8 | fun fullSuffix() {
9 | val actual = autocompleteCall("void foo() void bar() void baz()", "f")
10 | assertEquals(listOf("oo();"), actual)
11 | }
12 |
13 | @Test
14 | fun partialSuffix() {
15 | val actual = autocompleteCall("void foo() void bar() void baz()", "b")
16 | assertEquals(listOf("a"), actual)
17 | }
18 |
19 | @Test
20 | fun ambiguous() {
21 | val actual = autocompleteCall("void foo() void bar() void baz()", "ba")
22 | assertEquals(listOf("r();", "z();"), actual)
23 | }
24 |
25 | @Test
26 | fun alreadyComplete() {
27 | val actual = autocompleteCall("void foo() void bar() void baz()", "foo();")
28 | assertEquals(emptyList(), actual)
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/src/test/kotlin/logic/BeeperTest.kt:
--------------------------------------------------------------------------------
1 | package logic
2 |
3 | import org.junit.Assert.*
4 | import org.junit.Test
5 |
6 | class BeeperTest {
7 | @Test
8 | fun dropOneBeeper() {
9 | val beforeDrop = Problem.emptyWorld
10 | val afterDrop = beforeDrop.dropBeeper(1, 2)
11 |
12 | assertFalse(beforeDrop.beeperAt(1, 2))
13 | assertTrue(afterDrop.beeperAt(1, 2))
14 | }
15 |
16 | @Test
17 | fun dropAnotherBeeper() {
18 | val one = Problem.emptyWorld.dropBeeper(1, 2)
19 | assertThrows(CellIsFull::class.java) {
20 | one.dropBeeper(1, 2)
21 | }
22 | }
23 |
24 | @Test
25 | fun dropFourCornerBeepers() {
26 | val beforeDrop = Problem.emptyWorld
27 | val afterDrop = beforeDrop.dropBeeper(0, 0).dropBeeper(9, 0).dropBeeper(0, 9).dropBeeper(9, 9)
28 |
29 | assertEquals(0, beforeDrop.countBeepers())
30 | assertEquals(4, afterDrop.countBeepers())
31 | }
32 |
33 | @Test
34 | fun pickOneBeeper() {
35 | val beforePick = World(0, 1, FloorPlan.empty)
36 | val afterPick = beforePick.pickBeeper(0, 0)
37 |
38 | assertTrue(beforePick.beeperAt(0, 0))
39 | assertFalse(afterPick.beeperAt(0, 0))
40 | }
41 |
42 | @Test
43 | fun pickImaginaryBeeper() {
44 | assertThrows(CellIsEmpty::class.java) {
45 | Problem.emptyWorld.pickBeeper(0, 0)
46 | }
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/src/test/kotlin/logic/Week1Test.kt:
--------------------------------------------------------------------------------
1 | package logic
2 |
3 | import logic.Problem.Companion.EAST
4 | import logic.Problem.Companion.NORTH
5 | import logic.Problem.Companion.SOUTH
6 | import logic.Problem.Companion.WEST
7 | import org.junit.Assert.assertEquals
8 | import org.junit.Assert.assertTrue
9 | import org.junit.Test
10 |
11 | class Week1Test : WorldTestBase() {
12 | @Test
13 | fun karelsFirstProgram() {
14 | executeGoal(Problem.karelsFirstProgram)
15 | assertKarelAt(4, 8, EAST)
16 | assertSoleBeeperAt(3, 8)
17 | }
18 |
19 | @Test
20 | fun obtainArtifact() {
21 | executeGoal(Problem.obtainArtifact)
22 | assertKarelAt(3, 5, SOUTH)
23 | assertSoleBeeperAtKarel()
24 | }
25 |
26 | @Test
27 | fun defuseOneBomb() {
28 | executeGoal(Problem.defuseOneBomb)
29 | assertKarelAt(0, 9, EAST)
30 | assertNoBeepers()
31 | }
32 |
33 | @Test
34 | fun defuseTwoBombs() {
35 | executeGoal(Problem.defuseTwoBombs)
36 | assertKarelAt(0, 9, NORTH)
37 | assertNoBeepers()
38 | }
39 |
40 | @Test
41 | fun practiceHomeRun() {
42 | executeGoal(Problem.practiceHomeRun)
43 | assertKarelAt(0, 9, EAST)
44 | assertNoBeepers()
45 | }
46 |
47 | @Test
48 | fun climbTheStairs() {
49 | executeGoal(Problem.climbTheStairs)
50 | assertKarelAt(7, 3, EAST)
51 | assertNoBeepers()
52 | }
53 |
54 | @Test
55 | fun fillTheHoles() {
56 | executeGoal(Problem.fillTheHoles)
57 | assertKarelAt(9, 8, EAST)
58 | assertNumberOfBeepers(4)
59 | assertAllBeepersTouch(FloorPlan.WALL_ALL - FloorPlan.WALL_NORTH)
60 | }
61 |
62 | @Test
63 | fun saveTheFlower() {
64 | executeGoal(Problem.saveTheFlower)
65 | assertKarelAt(9, 9, EAST)
66 | assertSoleBeeperAt(5, 1)
67 | }
68 |
69 | @Test
70 | fun mowTheLawn() {
71 | executeGoal(Problem.mowTheLawn)
72 | assertKarelAt(1, 2, WEST)
73 | assertNoBeepers()
74 | }
75 |
76 | @Test
77 | fun harvestTheField() {
78 | executeGoal(Problem.harvestTheField)
79 | assertKarelAt(2, 4, SOUTH)
80 | assertNoBeepers()
81 | }
82 |
83 | @Test
84 | fun repairTheStreet() {
85 | executeGoal(Problem.repairTheStreet)
86 | assertKarelAt(9, 8, EAST)
87 | for (position in 90 until 100) {
88 | val isSolid = world.floorPlan.wallsAt(position).and(FloorPlan.WALL_NORTH) != 0
89 | val isRepaired = world.beeperAt(position)
90 | assertTrue(isSolid.xor(isRepaired))
91 | }
92 | }
93 |
94 | @Test
95 | fun cleanTheRoom() {
96 | executeGoal(Problem.cleanTheRoom)
97 | assertKarelAt(0, 0, WEST)
98 | assertNoBeepers()
99 | }
100 |
101 | @Test
102 | fun tileTheFloor() {
103 | executeGoal(Problem.tileTheFloor)
104 | assertKarelAt(4, 5, SOUTH)
105 | assertNumberOfBeepers(100)
106 | }
107 |
108 | @Test
109 | fun stealOlympicFire() {
110 | executeGoal(Problem.stealOlympicFire)
111 | assertKarelAt(9, 9, EAST)
112 | assertNoBeepers()
113 | }
114 |
115 | @Test
116 | fun removeTheTiles() {
117 | executeGoal(Problem.removeTheTiles)
118 | assertEquals(100, initialWorld.countBeepers())
119 | assertKarelAt(4, 5, SOUTH)
120 | assertNoBeepers()
121 | }
122 |
123 | @Test
124 | fun walkTheLabyrinth() {
125 | executeGoal(Problem.walkTheLabyrinth)
126 | assertSoleBeeperAtKarel()
127 | }
128 | }
129 |
--------------------------------------------------------------------------------
/src/test/kotlin/logic/Week2Test.kt:
--------------------------------------------------------------------------------
1 | package logic
2 |
3 | import logic.Problem.Companion.EAST
4 | import logic.Problem.Companion.NORTH
5 | import org.junit.Assert.assertEquals
6 | import org.junit.Test
7 |
8 | class Week2Test : WorldTestBase() {
9 | @Test
10 | fun hangTheLampions() {
11 | executeGoal(Problem.hangTheLampions)
12 | assertKarelAt(9, 9, EAST)
13 | assertNumberOfBeepers(10)
14 | assertAllBeepersTouch(FloorPlan.WALL_NORTH)
15 | }
16 |
17 | @Test
18 | fun followTheSeeds() {
19 | executeGoal(Problem.followTheSeeds)
20 | assertKarelAt(9, 9, NORTH)
21 | assertNoBeepers()
22 | }
23 |
24 | @Test
25 | fun cleanTheTunnels() {
26 | executeGoal(Problem.cleanTheTunnels)
27 | assertKarelAt(9, 9, EAST)
28 | assertNoBeepers()
29 | }
30 |
31 | @Test
32 | fun increment() {
33 | executeGoal(Problem.increment)
34 | val before = initialWorld.firstByte()
35 | val after = world.firstByte()
36 | assertEquals((before + 1).and(255), after)
37 | }
38 |
39 | @Test
40 | fun decrement() {
41 | executeGoal(Problem.decrement)
42 | val before = initialWorld.firstByte()
43 | val after = world.firstByte()
44 | assertEquals((before - 1).and(255), after)
45 | }
46 |
47 | @Test
48 | fun addSlow() {
49 | executeGoal(Problem.addSlow)
50 | val one = initialWorld.firstByte()
51 | val two = initialWorld.secondByte()
52 | val sum = world.secondByte()
53 | assertEquals((one + two).and(255), sum)
54 | }
55 |
56 | @Test
57 | fun saveTheFlowers() {
58 | executeGoal(Problem.saveTheFlowers)
59 | assertKarelAt(9, 9, EAST)
60 | assertNumberOfBeepers(4)
61 | assertAllBeepersTouch(FloorPlan.WALL_SOUTH)
62 | assertNoBeepersTouch(FloorPlan.WALL_EAST)
63 | }
64 |
65 | @Test
66 | fun findTeddyBear() {
67 | executeGoal(Problem.findTeddyBear)
68 | assertSoleBeeperAtKarel()
69 | }
70 |
71 | @Test
72 | fun jumpTheHurdles() {
73 | executeGoal(Problem.jumpTheHurdles)
74 | val x = Integer.numberOfTrailingZeros((initialWorld.beepersHi.ushr(9 * 10 - 64)).toInt())
75 | assertKarelAt(x, 9, EAST)
76 | assertSoleBeeperAtKarel()
77 | }
78 |
79 | @Test
80 | fun solveTheMaze() {
81 | executeGoal(Problem.solveTheMaze)
82 | assertSoleBeeperAtKarel()
83 | }
84 |
85 | @Test
86 | fun quantize() {
87 | executeGoal(Problem.quantizeBits)
88 | assertKarelAt(9, 9, EAST)
89 | for (x in 0..9) {
90 | val expected = initialWorld.beeperAt(x, 4)
91 | for (y in 0..9) {
92 | assertEquals(expected, world.beeperAt(x, y))
93 | }
94 | }
95 | }
96 |
97 | @Test
98 | fun addFast() {
99 | executeGoal(Problem.addFast)
100 | val one = initialWorld.firstByte()
101 | val two = initialWorld.secondByte()
102 | val sum = world.fourthByte()
103 | assertEquals((one + two).and(255), sum)
104 | }
105 | }
106 |
--------------------------------------------------------------------------------
/src/test/kotlin/logic/Week3Test.kt:
--------------------------------------------------------------------------------
1 | package logic
2 |
3 | import logic.Problem.Companion.EAST
4 | import logic.Problem.Companion.NORTH
5 | import logic.Problem.Companion.SOUTH
6 | import logic.Problem.Companion.WEST
7 | import org.junit.Assert.*
8 | import org.junit.Test
9 |
10 | class Week3Test : WorldTestBase() {
11 | @Test
12 | fun partyAgain() {
13 | executeGoal(Problem.partyAgain)
14 | assertKarelAt(9, 8, EAST)
15 | assertNumberOfBeepers(10)
16 | assertAllBeepersTouch(FloorPlan.WALL_NORTH)
17 | }
18 |
19 | @Test
20 | fun fetchTheStars() {
21 | executeGoal(Problem.fetchTheStars)
22 | assertKarelAt(9, 8, EAST)
23 | assertNumberOfBeepers(10)
24 | assertNoBeepersTouch(FloorPlan.WALL_NORTH)
25 | }
26 |
27 | @Test
28 | fun secureTheCave() {
29 | executeGoal(Problem.secureTheCave)
30 | for (x in 0..9) {
31 | val n = initialWorld.countBeepersInColumn(x)
32 | for (y in 0 until 10 - n) {
33 | assertFalse(world.beeperAt(x, y))
34 | }
35 | for (y in 10 - n until 10) {
36 | assertTrue(world.beeperAt(x, y))
37 | }
38 | }
39 | }
40 |
41 | @Test
42 | fun layAndRemoveTiles() {
43 | executeGoal(Problem.layAndRemoveTiles)
44 | assertKarelAt(0, 9, WEST)
45 | assertNoBeepers()
46 | }
47 |
48 | @Test
49 | fun findShelters() {
50 | executeGoal(Problem.findShelters)
51 | var floodWorld = initialWorld
52 | val floorPlan = floodWorld.floorPlan
53 |
54 | // mark reachable positions with beepers
55 | fun floodFill(position: Int) {
56 | if (floodWorld.beeperAt(position)) return
57 |
58 | floodWorld = floodWorld.dropBeeper(position)
59 |
60 | if (floorPlan.isClear(position, EAST)) {
61 | floodFill(position + 1)
62 | }
63 | if (floorPlan.isClear(position, NORTH)) {
64 | floodFill(position - 10)
65 | }
66 | if (floorPlan.isClear(position, WEST)) {
67 | floodFill(position - 1)
68 | }
69 | if (floorPlan.isClear(position, SOUTH)) {
70 | floodFill(position + 10)
71 | }
72 | }
73 | floodFill(world.position)
74 |
75 | // remove beepers from shelters
76 | for (position in 0 until 100) {
77 | if (floodWorld.beeperAt(position) && floorPlan.numberOfWallsAt(position) >= 3) {
78 | floodWorld = floodWorld.pickBeeper(position)
79 | }
80 | }
81 |
82 | assertEquals(floodWorld.beepersHi, world.beepersHi)
83 | assertEquals(floodWorld.beepersLo, world.beepersLo)
84 | }
85 |
86 | @Test
87 | fun addSmart() {
88 | executeGoal(Problem.addSmart)
89 | val one = initialWorld.firstByte()
90 | val two = initialWorld.secondByte()
91 | val sum = world.thirdByte()
92 | assertEquals((one + two).and(255), sum)
93 | }
94 |
95 | @Test
96 | fun computeFibonacci() {
97 | executeGoal(Problem.computeFibonacci)
98 | val bytes = world.allBytes()
99 | for (i in 2..9) {
100 | assertEquals(bytes[i - 2] + bytes[i - 1], bytes[i])
101 | }
102 | }
103 | }
104 |
--------------------------------------------------------------------------------
/src/test/kotlin/logic/WorldTestBase.kt:
--------------------------------------------------------------------------------
1 | package logic
2 |
3 | import org.junit.Assert.assertEquals
4 | import vm.VirtualMachine
5 |
6 | open class WorldTestBase {
7 | protected var initialWorld: World = Problem.emptyWorld
8 | protected var world: World = Problem.emptyWorld
9 |
10 | protected fun executeGoal(problem: Problem) {
11 | val instructions = vm.createGoalInstructions(problem.goal)
12 | initialWorld = problem.randomWorld()
13 | val virtualMachine = VirtualMachine(instructions.toTypedArray(), initialWorld)
14 | try {
15 | virtualMachine.executeGoalProgram()
16 | } catch (_: VirtualMachine.Finished) {
17 | }
18 | world = virtualMachine.world
19 | }
20 |
21 | protected fun assertKarelAt(x: Int, y: Int, direction: Int) {
22 | assertEquals(x, world.x)
23 | assertEquals(y, world.y)
24 | assertEquals(direction, world.direction)
25 | }
26 |
27 | protected fun assertSoleBeeperAt(x: Int, y: Int) {
28 | val expected = Problem.emptyWorld.dropBeeper(x, y)
29 | assertEquals(expected.beepersHi, world.beepersHi)
30 | assertEquals(expected.beepersLo, world.beepersLo)
31 | }
32 |
33 | protected fun assertSoleBeeperAtKarel() {
34 | assertSoleBeeperAt(world.x, world.y)
35 | }
36 |
37 | protected fun assertNoBeepers() {
38 | assertEquals(0, world.beepersHi)
39 | assertEquals(0, world.beepersLo)
40 | }
41 |
42 | protected fun assertNumberOfBeepers(expected: Int) {
43 | val actual = world.countBeepers()
44 | assertEquals(expected, actual)
45 | }
46 |
47 | protected fun assertAllBeepersTouch(walls: Int) {
48 | for (position in 0 until 100) {
49 | if (world.beeperAt(position)) {
50 | assertEquals(walls, world.floorPlan.wallsAt(position).and(walls))
51 | }
52 | }
53 | }
54 |
55 | protected fun assertNoBeepersTouch(walls: Int) {
56 | for (position in 0 until 100) {
57 | if (world.beeperAt(position)) {
58 | assertEquals(FloorPlan.WALL_NONE, world.floorPlan.wallsAt(position).and(walls))
59 | }
60 | }
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/src/test/kotlin/syntax/lexer/LexerNegativeTest.kt:
--------------------------------------------------------------------------------
1 | package syntax.lexer
2 |
3 | import common.Diagnostic
4 | import org.hamcrest.MatcherAssert.assertThat
5 | import org.hamcrest.core.StringContains.containsString
6 | import org.junit.Assert.assertThrows
7 | import org.junit.Test
8 |
9 | class LexerNegativeTest {
10 | private fun assertDiagnostic(messageSubstring: String, input: String) {
11 | val lexer = Lexer(input)
12 | val diagnostic = assertThrows(Diagnostic::class.java) {
13 | lexer.nextToken()
14 | }
15 | assertThat(diagnostic.message, containsString(messageSubstring))
16 | }
17 |
18 | @Test
19 | fun illegalCharacter() {
20 | assertDiagnostic(messageSubstring = "illegal character", input = "@")
21 | }
22 |
23 | @Test
24 | fun slashStartsComment() {
25 | assertDiagnostic(messageSubstring = "comments start", input = "/@")
26 | }
27 |
28 | @Test
29 | fun singleAmpersand() {
30 | assertDiagnostic(messageSubstring = "&&", input = "&")
31 | }
32 |
33 | @Test
34 | fun singleBar() {
35 | assertDiagnostic(messageSubstring = "||", input = "|")
36 | }
37 |
38 | @Test
39 | fun unclosedMultiLineComment() {
40 | assertDiagnostic(messageSubstring = "multi-line comment", input = "/*")
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/src/test/kotlin/syntax/lexer/LexerTest.kt:
--------------------------------------------------------------------------------
1 | package syntax.lexer
2 |
3 | import org.junit.Assert.assertEquals
4 | import org.junit.Test
5 | import syntax.lexer.TokenKind.*
6 |
7 | class LexerTest {
8 | private var lexer = Lexer("")
9 |
10 | private fun assertToken(expected: TokenKind) {
11 | val actualToken = lexer.nextToken()
12 | assertEquals(expected, actualToken.kind)
13 | }
14 |
15 | private fun assertIdentifier(expected: String) {
16 | val actualToken = lexer.nextToken()
17 | assertEquals(IDENTIFIER, actualToken.kind)
18 | assertEquals(expected, actualToken.lexeme)
19 | }
20 |
21 | private fun assertNumber(expected: String) {
22 | val actualToken = lexer.nextToken()
23 | assertEquals(NUMBER, actualToken.kind)
24 | assertEquals(expected, actualToken.lexeme)
25 | }
26 |
27 | @Test
28 | fun emptyString() {
29 | lexer = Lexer("")
30 | assertToken(END_OF_INPUT)
31 | }
32 |
33 | @Test
34 | fun singleLineComments() {
35 | lexer = Lexer(
36 | """
37 | // comment #1
38 | a
39 | // comment #2
40 | // comment #3
41 | b
42 | c // comment #4
43 | d// comment #5
44 | e//
45 | """
46 | )
47 |
48 | assertIdentifier("a")
49 | assertIdentifier("b")
50 | assertIdentifier("c")
51 | assertIdentifier("d")
52 | assertIdentifier("e")
53 | assertToken(END_OF_INPUT)
54 | }
55 |
56 | @Test
57 | fun openSingleLineComment() {
58 | lexer = Lexer("//")
59 | assertToken(END_OF_INPUT)
60 | }
61 |
62 | @Test
63 | fun multiLineComments() {
64 | lexer = Lexer(
65 | """
66 | /*
67 | comment #1
68 | */
69 | a /* comment #2 */
70 | b /*/ comment #3*/
71 | c /**/
72 | d/***/
73 | e /* / ** / *** /*/
74 | f
75 | """
76 | )
77 |
78 | assertIdentifier("a")
79 | assertIdentifier("b")
80 | assertIdentifier("c")
81 | assertIdentifier("d")
82 | assertIdentifier("e")
83 | assertIdentifier("f")
84 | assertToken(END_OF_INPUT)
85 | }
86 |
87 | @Test
88 | fun digits() {
89 | lexer = Lexer("0 1 2 3 4 5 6 7 8 9")
90 | assertNumber("0")
91 | assertNumber("1")
92 | assertNumber("2")
93 | assertNumber("3")
94 | assertNumber("4")
95 | assertNumber("5")
96 | assertNumber("6")
97 | assertNumber("7")
98 | assertNumber("8")
99 | assertNumber("9")
100 | }
101 |
102 | @Test
103 | fun numbers() {
104 | lexer = Lexer("10 42 97 1234567890")
105 | assertNumber("10")
106 | assertNumber("42")
107 | assertNumber("97")
108 | assertNumber("1234567890")
109 | }
110 |
111 | @Test
112 | fun separators() {
113 | lexer = Lexer("();{}")
114 | assertToken(OPENING_PAREN)
115 | assertToken(CLOSING_PAREN)
116 | assertToken(SEMICOLON)
117 | assertToken(OPENING_BRACE)
118 | assertToken(CLOSING_BRACE)
119 | }
120 |
121 | @Test
122 | fun operators() {
123 | lexer = Lexer("!&&||")
124 | assertToken(BANG)
125 | assertToken(AMPERSAND_AMPERSAND)
126 | assertToken(BAR_BAR)
127 | }
128 |
129 | @Test
130 | fun identifiers() {
131 | lexer =
132 | Lexer("a z a0 z9 a_z foo _bar the_quick_brown_fox_jumps_over_the_lazy_dog THE_QUICK_BROWN_FOX_JUMPS_OVER_THE_LAZY_DOG")
133 |
134 | assertIdentifier("a")
135 | assertIdentifier("z")
136 | assertIdentifier("a0")
137 | assertIdentifier("z9")
138 | assertIdentifier("a_z")
139 | assertIdentifier("foo")
140 | assertIdentifier("_bar")
141 | assertIdentifier("the_quick_brown_fox_jumps_over_the_lazy_dog")
142 | assertIdentifier("THE_QUICK_BROWN_FOX_JUMPS_OVER_THE_LAZY_DOG")
143 | }
144 |
145 | @Test
146 | fun keywords() {
147 | lexer = Lexer("if else repeat void while")
148 |
149 | assertToken(IF)
150 | assertToken(ELSE)
151 | assertToken(REPEAT)
152 | assertToken(VOID)
153 | assertToken(WHILE)
154 | }
155 | }
156 |
--------------------------------------------------------------------------------
/src/test/kotlin/syntax/parser/ParserNegativeTest.kt:
--------------------------------------------------------------------------------
1 | package syntax.parser
2 |
3 | import common.Diagnostic
4 | import org.hamcrest.MatcherAssert.assertThat
5 | import org.hamcrest.core.StringContains.containsString
6 | import org.junit.Assert.assertThrows
7 | import org.junit.Test
8 | import syntax.lexer.Lexer
9 |
10 | class ParserNegativeTest {
11 | private fun assertDiagnostic(messageSubstring: String, sourceCode: String) {
12 | val lexer = Lexer(sourceCode)
13 | val parser = Parser(lexer)
14 | val diagnostic = assertThrows(Diagnostic::class.java) {
15 | parser.program()
16 | }
17 | assertThat(diagnostic.message, containsString(messageSubstring))
18 | }
19 |
20 | @Test
21 | fun tooManyClosingBraces() {
22 | assertDiagnostic(
23 | "closing brace has no opening partner", """
24 | void main() {
25 | }
26 | }
27 | """
28 | )
29 | }
30 |
31 | @Test
32 | fun firstCommandMissingVoid() {
33 | assertDiagnostic(
34 | "Command definitions look like this", """
35 | main() {
36 | }
37 | """
38 | )
39 | }
40 |
41 | @Test
42 | fun secondCommandMissingVoid() {
43 | assertDiagnostic(
44 | "Command definitions look like this", """
45 | void first() {
46 | }
47 | second() {
48 | }
49 | """
50 | )
51 | }
52 |
53 | @Test
54 | fun repeatBelongsInsideCommand() {
55 | assertDiagnostic(
56 | "belongs inside a command", """
57 | void main() {
58 | }
59 | repeat
60 | """
61 | )
62 | }
63 |
64 | @Test
65 | fun whileBelongsInsideCommand() {
66 | assertDiagnostic(
67 | "belongs inside a command", """
68 | void main() {
69 | }
70 | while
71 | """
72 | )
73 | }
74 |
75 | @Test
76 | fun ifBelongsInsideCommand() {
77 | assertDiagnostic(
78 | "belongs inside a command", """
79 | void main() {
80 | }
81 | if
82 | """
83 | )
84 | }
85 |
86 | @Test
87 | fun commandCallsBelongInsideCommand() {
88 | assertDiagnostic(
89 | "belongs inside a command", """
90 | void main() {
91 | }
92 | moveForward();
93 | """
94 | )
95 | }
96 |
97 | @Test
98 | fun commandMissingName() {
99 | assertDiagnostic(
100 | "missing IDENTIFIER", """
101 | void () {
102 | }
103 | """
104 | )
105 | }
106 |
107 | @Test
108 | fun commandMissingParameters() {
109 | assertDiagnostic(
110 | "missing (", """
111 | void main {
112 | }
113 | """
114 | )
115 | }
116 |
117 | @Test
118 | fun commandMissingBody() {
119 | assertDiagnostic(
120 | "missing {", """
121 | void main()
122 | """
123 | )
124 | }
125 |
126 | @Test
127 | fun nestedCommands() {
128 | assertDiagnostic(
129 | "Command definitions do not nest", """
130 | void outer() {
131 | void inner() {
132 | }
133 | }
134 | """
135 | )
136 | }
137 |
138 | @Test
139 | fun unclosedBlock() {
140 | assertDiagnostic(
141 | "missing }", """
142 | void main() {
143 | if (onBeeper()) {
144 | pickBeeper();
145 | }
146 | """
147 | )
148 | }
149 |
150 | @Test
151 | fun strayElse() {
152 | assertDiagnostic(
153 | "{ or if", """
154 | void main() {
155 | if (onBeeper()) {
156 | } else
157 | }
158 | """
159 | )
160 | }
161 |
162 | @Test
163 | fun numbersAreNotStatements() {
164 | assertDiagnostic(
165 | "illegal start of statement", """
166 | void main() {
167 | 123
168 | }
169 | """
170 | )
171 | }
172 |
173 | @Test
174 | fun commandMissingArguments() {
175 | assertDiagnostic(
176 | "missing (", """
177 | void main() {
178 | other;
179 | }
180 | """
181 | )
182 | }
183 |
184 | @Test
185 | fun commandMissingSemicolon() {
186 | assertDiagnostic(
187 | "missing ;", """
188 | void main() {
189 | other()
190 | }
191 | """
192 | )
193 | }
194 |
195 | @Test
196 | fun commandSuperfluousVoid() {
197 | assertDiagnostic(
198 | "void", """
199 | void main() {
200 | void other();
201 | }
202 | """
203 | )
204 | }
205 |
206 | @Test
207 | fun repeatMissingParens() {
208 | assertDiagnostic(
209 | "missing (", """
210 | void main() {
211 | repeat 9 {
212 | moveForward();
213 | }
214 | }
215 | """
216 | )
217 | }
218 |
219 | @Test
220 | fun repeatMissingNumber() {
221 | assertDiagnostic(
222 | "missing NUMBER", """
223 | void main() {
224 | repeat () {
225 | moveForward();
226 | }
227 | }
228 | """
229 | )
230 | }
231 |
232 | @Test
233 | fun repeatMissingBlock() {
234 | assertDiagnostic(
235 | "missing {", """
236 | void main() {
237 | repeat (9)
238 | moveForward();
239 | }
240 | """
241 | )
242 | }
243 |
244 | @Test
245 | fun zeroRepetitions() {
246 | assertDiagnostic(
247 | "0 out of range", """
248 | void main() {
249 | repeat (0) {
250 | moveForward();
251 | }
252 | }
253 | """
254 | )
255 | }
256 |
257 | @Test
258 | fun oneRepetition() {
259 | assertDiagnostic(
260 | "1 out of range", """
261 | void main() {
262 | repeat (1) {
263 | moveForward();
264 | }
265 | }
266 | """
267 | )
268 | }
269 |
270 | @Test
271 | fun tooManyRepetitions() {
272 | assertDiagnostic(
273 | "4096 out of range", """
274 | void main() {
275 | repeat (4096) {
276 | moveForward();
277 | }
278 | }
279 | """
280 | )
281 | }
282 |
283 | @Test
284 | fun integerOverflow() {
285 | assertDiagnostic(
286 | "2147483648 out of range", """
287 | void main() {
288 | repeat (2147483648) {
289 | moveForward();
290 | }
291 | }
292 | """
293 | )
294 | }
295 |
296 | @Test
297 | fun ifMissingParens() {
298 | assertDiagnostic(
299 | "missing (", """
300 | void main() {
301 | if frontIsClear() {
302 | moveForward();
303 | }
304 | }
305 | """
306 | )
307 | }
308 |
309 | @Test
310 | fun ifMissingBlock() {
311 | assertDiagnostic(
312 | "missing {", """
313 | void main() {
314 | if (frontIsClear())
315 | moveForward();
316 | }
317 | """
318 | )
319 | }
320 |
321 | @Test
322 | fun elseRequiresBlockOrIf() {
323 | assertDiagnostic(
324 | "{ or if", """
325 | void main() {
326 | if (onBeeper()) {
327 | pickBeeper();
328 | }
329 | else dropBeeper();
330 | }
331 | """
332 | )
333 | }
334 |
335 | @Test
336 | fun whileMissingParens() {
337 | assertDiagnostic(
338 | "missing (", """
339 | void main() {
340 | while frontIsClear() {
341 | moveForward();
342 | }
343 | }
344 | """
345 | )
346 | }
347 |
348 | @Test
349 | fun whileMissingBlock() {
350 | assertDiagnostic(
351 | "missing {", """
352 | void main() {
353 | while (frontIsClear())
354 | moveForward();
355 | }
356 | """
357 | )
358 | }
359 |
360 | @Test
361 | fun statementAsCondition() {
362 | assertDiagnostic(
363 | "Did you mean", """
364 | void main() {
365 | if (turnAround()) {
366 | }
367 | }
368 | """
369 | )
370 | }
371 |
372 | @Test
373 | fun conditionMissingParens() {
374 | assertDiagnostic(
375 | "missing (", """
376 | void main() {
377 | while (frontIsClear) {
378 | moveForward();
379 | }
380 | }
381 | """
382 | )
383 | }
384 | }
385 |
--------------------------------------------------------------------------------
/src/test/kotlin/syntax/parser/SemaTest.kt:
--------------------------------------------------------------------------------
1 | package syntax.parser
2 |
3 | import common.Diagnostic
4 | import org.junit.Assert.fail
5 | import org.junit.Test
6 | import syntax.lexer.Lexer
7 |
8 | class SemaTest {
9 | private fun assertIllegal(messageSubstring: String, sourceCode: String) {
10 | try {
11 | val lexer = Lexer(sourceCode)
12 | val parser = Parser(lexer)
13 | parser.program()
14 | fail()
15 | } catch (diagnostic: Diagnostic) {
16 | if (!diagnostic.message.contains(messageSubstring)) {
17 | fail(diagnostic.message)
18 | }
19 | }
20 | }
21 |
22 | @Test
23 | fun duplicateCommand() {
24 | assertIllegal(
25 | "duplicate", """
26 | void main() {
27 | pickBeeper();
28 | }
29 |
30 | void main() {
31 | dropBeeper();
32 | }
33 | """
34 | )
35 | }
36 |
37 | @Test
38 | fun redefineBuiltin() {
39 | assertIllegal(
40 | "redefine builtin", """
41 | void turnRight() {
42 | turnLeft();
43 | turnLeft();
44 | turnLeft();
45 | }
46 | """
47 | )
48 | }
49 |
50 | @Test
51 | fun undefinedCommand() {
52 | assertIllegal(
53 | "Did you mean b?", """
54 | void main() {
55 | a();
56 | }
57 |
58 | void b() {
59 | }
60 | """
61 | )
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/src/test/kotlin/vm/EmitterTest.kt:
--------------------------------------------------------------------------------
1 | package vm
2 |
3 | import common.subList
4 | import org.junit.Assert.assertEquals
5 | import org.junit.Test
6 | import syntax.lexer.Lexer
7 | import syntax.parser.Parser
8 | import syntax.parser.program
9 |
10 | class EmitterTest {
11 | private fun compile(sourceCode: String): List {
12 | val lexer = Lexer(sourceCode)
13 | val parser = Parser(lexer)
14 | val program = parser.program()
15 | val main = program.commands.first()
16 | return Emitter(parser.sema, false).emit(main)
17 | }
18 |
19 | private fun assertBytecode(sourceCode: String, vararg bytecodes: Int) {
20 | val expected = bytecodes.map { bytecode -> "%x".format(bytecode) }
21 | val actual = compile(sourceCode).subList(ENTRY_POINT).map { instruction -> "%x".format(instruction.bytecode) }
22 | assertEquals(expected, actual)
23 | }
24 |
25 | @Test
26 | fun basicCommands() {
27 | assertBytecode(
28 | """
29 | void main() {
30 | moveForward();
31 | turnLeft();
32 | turnAround();
33 | turnRight();
34 | pickBeeper();
35 | dropBeeper();
36 | }
37 | """,
38 | MOVE_FORWARD,
39 | TURN_LEFT,
40 | TURN_AROUND,
41 | TURN_RIGHT,
42 | PICK_BEEPER,
43 | DROP_BEEPER,
44 | RETURN,
45 | )
46 | }
47 |
48 | @Test
49 | fun call() {
50 | assertBytecode(
51 | """
52 | void main() {
53 | moveForward();
54 | turns();
55 | moveForward();
56 | beepers();
57 | moveForward();
58 | }
59 |
60 | void turns() {
61 | turnLeft();
62 | turnAround();
63 | turnRight();
64 | }
65 |
66 | void beepers() {
67 | pickBeeper();
68 | dropBeeper();
69 | }
70 | """,
71 | MOVE_FORWARD,
72 | CALL + 0x106,
73 | MOVE_FORWARD,
74 | CALL + 0x10a,
75 | MOVE_FORWARD,
76 | RETURN,
77 |
78 | TURN_LEFT,
79 | TURN_AROUND,
80 | TURN_RIGHT,
81 | RETURN,
82 |
83 | PICK_BEEPER,
84 | DROP_BEEPER,
85 | RETURN,
86 | )
87 | }
88 |
89 | @Test
90 | fun repeat() {
91 | assertBytecode(
92 | """
93 | void main() {
94 | repeat (9) {
95 | moveForward();
96 | }
97 | }
98 | """,
99 | PUSH + 9,
100 | MOVE_FORWARD,
101 | LOOP + 0x101,
102 | RETURN,
103 | )
104 | }
105 |
106 | @Test
107 | fun nestedRepeat() {
108 | assertBytecode(
109 | """
110 | void main() {
111 | repeat (4) {
112 | repeat (9) {
113 | moveForward();
114 | }
115 | }
116 | }
117 | """,
118 | PUSH + 4,
119 | PUSH + 9,
120 | MOVE_FORWARD,
121 | LOOP + 0x102,
122 | LOOP + 0x101,
123 | RETURN
124 | )
125 | }
126 |
127 | @Test
128 | fun ifThenTrue() {
129 | assertBytecode(
130 | """
131 | void main() {
132 | if (onBeeper()) {
133 | pickBeeper();
134 | }
135 | }
136 | """,
137 | ON_BEEPER, ELSE + 0x103,
138 | PICK_BEEPER,
139 | RETURN,
140 | )
141 | }
142 |
143 | @Test
144 | fun ifThenFalse() {
145 | assertBytecode(
146 | """
147 | void main() {
148 | if (!onBeeper()) {
149 | dropBeeper();
150 | }
151 | }
152 | """,
153 | ON_BEEPER, THEN + 0x103,
154 | DROP_BEEPER,
155 | RETURN,
156 | )
157 | }
158 |
159 | @Test
160 | fun ifElseTrue() {
161 | assertBytecode(
162 | """
163 | void main() {
164 | if (onBeeper()) {
165 | pickBeeper();
166 | } else {
167 | dropBeeper();
168 | }
169 | }
170 | """,
171 | ON_BEEPER, ELSE + 0x104,
172 | PICK_BEEPER,
173 | JUMP + 0x105,
174 | DROP_BEEPER,
175 | RETURN,
176 | )
177 | }
178 |
179 | @Test
180 | fun ifElseFalse() {
181 | assertBytecode(
182 | """
183 | void main() {
184 | if (!onBeeper()) {
185 | dropBeeper();
186 | } else {
187 | pickBeeper();
188 | }
189 | }
190 | """,
191 | ON_BEEPER, THEN + 0x104,
192 | DROP_BEEPER,
193 | JUMP + 0x105,
194 | PICK_BEEPER,
195 | RETURN,
196 | )
197 | }
198 |
199 | @Test
200 | fun elseIf() {
201 | assertBytecode(
202 | """
203 | void main() {
204 | if (leftIsClear()) {
205 | turnLeft();
206 | } else if (frontIsClear()) {
207 | } else if (rightIsClear()) {
208 | turnRight();
209 | } else {
210 | turnAround();
211 | }
212 | }
213 | """,
214 | LEFT_IS_CLEAR, ELSE + 0x104,
215 | TURN_LEFT,
216 | JUMP + 0x10c,
217 | FRONT_IS_CLEAR, ELSE + 0x107,
218 | JUMP + 0x10c,
219 | RIGHT_IS_CLEAR, ELSE + 0x10b,
220 | TURN_RIGHT,
221 | JUMP + 0x10c,
222 | TURN_AROUND,
223 | RETURN,
224 | )
225 | }
226 |
227 | @Test
228 | fun obstacle1() {
229 | assertBytecode(
230 | """
231 | void main() {
232 | if (!frontIsClear() || beeperAhead()) {
233 | turnLeft();
234 | }
235 | }
236 | """,
237 | FRONT_IS_CLEAR, ELSE + 0x104,
238 | BEEPER_AHEAD, ELSE + 0x105,
239 | TURN_LEFT,
240 | RETURN,
241 | )
242 | }
243 |
244 | @Test
245 | fun obstacle2() {
246 | assertBytecode(
247 | """
248 | void main() {
249 | if (beeperAhead() || !frontIsClear()) {
250 | turnLeft();
251 | }
252 | }
253 | """,
254 | BEEPER_AHEAD, THEN + 0x104,
255 | FRONT_IS_CLEAR, THEN + 0x105,
256 | TURN_LEFT,
257 | RETURN,
258 | )
259 | }
260 |
261 | @Test
262 | fun shot1() {
263 | assertBytecode(
264 | """
265 | void main() {
266 | if (!onBeeper() && frontIsClear()) {
267 | moveForward();
268 | }
269 | }
270 | """,
271 | ON_BEEPER, THEN + 0x105,
272 | FRONT_IS_CLEAR, ELSE + 0x105,
273 | MOVE_FORWARD,
274 | RETURN,
275 | )
276 | }
277 |
278 | @Test
279 | fun shot2() {
280 | assertBytecode(
281 | """
282 | void main() {
283 | if (frontIsClear() && !onBeeper()) {
284 | moveForward();
285 | }
286 | }
287 | """,
288 | FRONT_IS_CLEAR, ELSE + 0x105,
289 | ON_BEEPER, THEN + 0x105,
290 | MOVE_FORWARD,
291 | RETURN,
292 | )
293 | }
294 |
295 | @Test
296 | fun deadEnd1() {
297 | assertBytecode(
298 | """
299 | void main() {
300 | if (!leftIsClear() && !frontIsClear() && !rightIsClear()) {
301 | turnAround();
302 | }
303 | }
304 | """,
305 | LEFT_IS_CLEAR, THEN + 0x107,
306 | FRONT_IS_CLEAR, THEN + 0x107,
307 | RIGHT_IS_CLEAR, THEN + 0x107,
308 | TURN_AROUND,
309 | RETURN,
310 | )
311 | }
312 |
313 | @Test
314 | fun deadEnd2() {
315 | assertBytecode(
316 | """
317 | void main() {
318 | if (!(leftIsClear() || frontIsClear() || rightIsClear())) {
319 | turnAround();
320 | }
321 | }
322 | """,
323 | LEFT_IS_CLEAR, THEN + 0x107,
324 | FRONT_IS_CLEAR, THEN + 0x107,
325 | RIGHT_IS_CLEAR, THEN + 0x107,
326 | TURN_AROUND,
327 | RETURN,
328 | )
329 | }
330 |
331 | @Test
332 | fun whi1e() {
333 | assertBytecode(
334 | """
335 | void hangTheLampions() {
336 | while (beeperAhead()) {
337 | moveForward();
338 | pickBeeper();
339 | }
340 | }
341 | """,
342 | BEEPER_AHEAD, ELSE + 0x105,
343 | MOVE_FORWARD,
344 | PICK_BEEPER,
345 | JUMP + 0x100,
346 | RETURN,
347 | )
348 | }
349 |
350 | @Test
351 | fun recursion() {
352 | assertBytecode(
353 | """
354 | void partyAgain() {
355 | if (!frontIsClear()) {
356 | turnAround();
357 | } else {
358 | moveForward();
359 | partyAgain();
360 | moveForward();
361 | }
362 | }
363 | """,
364 | FRONT_IS_CLEAR, THEN + 0x104,
365 | TURN_AROUND,
366 | JUMP + 0x107,
367 | MOVE_FORWARD,
368 | CALL + 0x100,
369 | MOVE_FORWARD,
370 | RETURN,
371 | )
372 | }
373 |
374 | @Test
375 | fun oddNumberOfNegations() {
376 | assertBytecode(
377 | """
378 | void main() {
379 | if (!!!onBeeper()) {
380 | dropBeeper();
381 | }
382 | }
383 | """,
384 | ON_BEEPER, THEN + 0x103,
385 | DROP_BEEPER,
386 | RETURN,
387 | )
388 | }
389 |
390 | @Test
391 | fun evenNumberOfNegations() {
392 | assertBytecode(
393 | """
394 | void main() {
395 | if (!!!!frontIsClear()) {
396 | moveForward();
397 | }
398 | }
399 | """,
400 | FRONT_IS_CLEAR, ELSE + 0x103,
401 | MOVE_FORWARD,
402 | RETURN,
403 | )
404 | }
405 |
406 | @Test
407 | fun infiniteRecursion2() {
408 | assertBytecode(
409 | """
410 | void f() { g(); }
411 | void g() { f(); }
412 | """,
413 | CALL + 0x102, RETURN,
414 | CALL + 0x100, RETURN,
415 | )
416 | }
417 |
418 | @Test
419 | fun infiniteRecursion3() {
420 | assertBytecode(
421 | """
422 | void f() { g(); }
423 | void g() { h(); }
424 | void h() { f(); }
425 | """,
426 | CALL + 0x102, RETURN,
427 | CALL + 0x104, RETURN,
428 | CALL + 0x100, RETURN,
429 | )
430 | }
431 | }
432 |
--------------------------------------------------------------------------------
/src/test/kotlin/vm/InstructionTest.kt:
--------------------------------------------------------------------------------
1 | package vm
2 |
3 | import org.junit.Assert.assertFalse
4 | import org.junit.Assert.assertTrue
5 | import org.junit.Test
6 |
7 | private const val GOAL = 0
8 | private const val HUMAN = 1
9 |
10 | class InstructionTest {
11 |
12 | private fun assertPause(bytecode: Int, address: Int) {
13 | assertTrue(Instruction(bytecode, address).shouldPause())
14 | }
15 |
16 | private fun assertNoPause(bytecode: Int, address: Int) {
17 | assertFalse(Instruction(bytecode, address).shouldPause())
18 | }
19 |
20 | @Test
21 | fun commandsAlwaysPause() {
22 | for (bytecode in MOVE_FORWARD..DROP_BEEPER) {
23 | assertPause(bytecode, GOAL)
24 | assertPause(bytecode, HUMAN)
25 | }
26 | }
27 |
28 | @Test
29 | fun goalConditionsNeverPause() {
30 | for (bytecode in ON_BEEPER..RIGHT_IS_CLEAR) {
31 | assertNoPause(bytecode, GOAL)
32 | }
33 | }
34 |
35 | @Test
36 | fun humanConditionsAlwaysPause() {
37 | for (bytecode in ON_BEEPER..RIGHT_IS_CLEAR) {
38 | assertPause(bytecode, HUMAN)
39 | }
40 | }
41 |
42 | @Test
43 | fun otherGoalInstructionsNeverPause() {
44 | assertNoPause(RETURN, GOAL)
45 | assertNoPause(PUSH, GOAL)
46 | assertNoPause(LOOP, GOAL)
47 | assertNoPause(CALL, GOAL)
48 | assertNoPause(JUMP, GOAL)
49 | assertNoPause(ELSE, GOAL)
50 | assertNoPause(THEN, GOAL)
51 | }
52 |
53 | @Test
54 | fun otherHumanInstructionsAlwaysPauseExceptBranches() {
55 | assertPause(RETURN, HUMAN)
56 | assertPause(PUSH, HUMAN)
57 | assertPause(LOOP, HUMAN)
58 | assertPause(CALL, HUMAN)
59 | assertNoPause(JUMP, HUMAN)
60 | assertNoPause(ELSE, HUMAN)
61 | assertNoPause(THEN, HUMAN)
62 | }
63 | }
64 |
--------------------------------------------------------------------------------