├── .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 | ![hangTheLampions](https://i.imgur.com/EMKIohi.png) 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 | ![Pattis](https://i.imgur.com/i3KWuae.jpg) 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 | --------------------------------------------------------------------------------