Atlas - NovaBasic.java
Home / lab / nova / src / main / java / com / github / _0x4248 / nova_examples Lines: 1 | Size: 20330 bytes [Download] [Show on GitHub] [Search similar files] [Raw] [Raw (proxy)][FILE BEGIN]1package com.github._0x4248.nova_examples; 2 3import com.github._0x4248.nova.Machine.core.Machine; 4import com.github._0x4248.nova.Machine.core.MachineProgram; 5import com.github._0x4248.nova.Machine.core.Keyboard; 6import com.github._0x4248.nova.Machine.core.Video; 7import com.github._0x4248.nova.Machine.gpu.GPU; 8 9import java.awt.event.KeyEvent; 10import java.util.ArrayList; 11import java.util.HashMap; 12import java.util.List; 13import java.util.Map; 14import java.util.Scanner; 15import java.util.TreeMap; 16 17public class NovaBasic implements MachineProgram { 18 19 private static final int VIDEO_COLUMNS = 40; 20 private static final int VIDEO_ROWS = 25; 21 22 private final TreeMap<Integer, String> program = new TreeMap<>(); 23 private final Map<String, Integer> variables = new HashMap<>(); 24 private final List<String> screenLines = new ArrayList<>(); 25 private GPU gpu; 26 private Keyboard keyboard; 27 private String currentInput = ""; 28 29 public static void main(String[] args) { 30 new NovaBasic().run(Machine.basic(), args); 31 } 32 33 @Override 34 public void run(Machine machine, String[] args) { 35 initVideoOut(machine); 36 initKeyboardIn(machine); 37 38 outLine("NOVA BASIC V0.1"); 39 outLine("READY."); 40 outLine("Type HELP for commands."); 41 42 if (gpu != null && keyboard != null) { 43 runKeyboardLoop(); 44 if (gpu != null) { 45 gpu.shutdown(); 46 } 47 return; 48 } 49 50 try (Scanner scanner = new Scanner(System.in)) { 51 while (true) { 52 System.out.print("> "); 53 if (!scanner.hasNextLine()) { 54 break; 55 } 56 57 String line = scanner.nextLine().trim(); 58 appendScreenLine("> " + line); 59 if (line.isEmpty()) { 60 continue; 61 } 62 63 if (processInputLine(line)) { 64 break; 65 } 66 } 67 } 68 69 if (gpu != null) { 70 gpu.shutdown(); 71 } 72 } 73 74 private void runKeyboardLoop() { 75 while (true) { 76 String line = readLineFromKeyboard(); 77 if (line == null) { 78 continue; 79 } 80 81 String trimmed = line.trim(); 82 if (trimmed.isEmpty()) { 83 continue; 84 } 85 86 if (processInputLine(trimmed)) { 87 break; 88 } 89 } 90 } 91 92 private boolean processInputLine(String line) { 93 if (startsWithLineNumber(line)) { 94 saveProgramLine(line); 95 return false; 96 } 97 98 return handleDirectCommand(line); 99 } 100 101 private boolean handleDirectCommand(String line) { 102 String upper = line.toUpperCase(); 103 104 if (upper.equals("RUN")) { 105 runProgram(); 106 return false; 107 } 108 if (upper.equals("LIST")) { 109 listProgram(); 110 return false; 111 } 112 if (upper.equals("NEW")) { 113 program.clear(); 114 variables.clear(); 115 outLine("OK"); 116 return false; 117 } 118 if (upper.equals("HELP")) { 119 printHelp(); 120 return false; 121 } 122 if (upper.equals("VARS")) { 123 printVars(); 124 return false; 125 } 126 if (upper.equals("EXIT") || upper.equals("QUIT") || upper.equals("BYE")) { 127 outLine("BYE"); 128 return true; 129 } 130 131 try { 132 executeStatement(line, null, null); 133 } catch (BasicRuntimeException e) { 134 outLine("? " + e.getMessage()); 135 } 136 return false; 137 } 138 139 private boolean startsWithLineNumber(String line) { 140 int idx = 0; 141 while (idx < line.length() && Character.isDigit(line.charAt(idx))) { 142 idx++; 143 } 144 return idx > 0 && (idx == line.length() || Character.isWhitespace(line.charAt(idx))); 145 } 146 147 private void saveProgramLine(String line) { 148 int idx = 0; 149 while (idx < line.length() && Character.isDigit(line.charAt(idx))) { 150 idx++; 151 } 152 153 int lineNumber = Integer.parseInt(line.substring(0, idx)); 154 String statement = line.substring(idx).trim(); 155 156 if (statement.isEmpty()) { 157 program.remove(lineNumber); 158 } else { 159 program.put(lineNumber, statement); 160 } 161 } 162 163 private void listProgram() { 164 if (program.isEmpty()) { 165 outLine("(empty)"); 166 return; 167 } 168 for (Map.Entry<Integer, String> entry : program.entrySet()) { 169 outLine(entry.getKey() + " " + entry.getValue()); 170 } 171 } 172 173 private void printHelp() { 174 outLine("DIRECT: RUN, LIST, NEW, VARS, HELP, QUIT"); 175 outLine("PROGRAM: PRINT, LET, GOTO, IF ... THEN <line>, END, REM"); 176 outLine("EXAMPLE:"); 177 outLine("10 LET X = 1"); 178 outLine("20 PRINT X"); 179 outLine("30 LET X = X + 1"); 180 outLine("40 IF X < 5 THEN 20"); 181 outLine("50 END"); 182 } 183 184 private void printVars() { 185 if (variables.isEmpty()) { 186 outLine("(no vars)"); 187 return; 188 } 189 190 for (Map.Entry<String, Integer> entry : variables.entrySet()) { 191 outLine(entry.getKey() + " = " + entry.getValue()); 192 } 193 } 194 195 private void runProgram() { 196 if (program.isEmpty()) { 197 outLine("NO PROGRAM"); 198 return; 199 } 200 201 List<Integer> lineNumbers = new ArrayList<>(program.keySet()); 202 int pc = 0; 203 int stepCounter = 0; 204 final int maxSteps = 100_000; 205 206 try { 207 while (pc >= 0 && pc < lineNumbers.size()) { 208 if (++stepCounter > maxSteps) { 209 throw new BasicRuntimeException("TOO MANY STEPS (possible infinite loop)"); 210 } 211 212 int currentLine = lineNumbers.get(pc); 213 String statement = program.get(currentLine); 214 215 ExecutionResult result = executeStatement(statement, lineNumbers, pc); 216 if (result.endProgram) { 217 break; 218 } 219 if (result.nextPc != null) { 220 pc = result.nextPc; 221 } else { 222 pc++; 223 } 224 } 225 226 outLine("READY."); 227 } catch (BasicRuntimeException e) { 228 outLine("? " + e.getMessage()); 229 } 230 } 231 232 private ExecutionResult executeStatement(String statement, List<Integer> lineNumbers, Integer currentPc) { 233 String trimmed = statement.trim(); 234 if (trimmed.isEmpty()) { 235 return ExecutionResult.continueNext(); 236 } 237 238 String upper = trimmed.toUpperCase(); 239 240 if (upper.startsWith("REM") || upper.startsWith("'")) { 241 return ExecutionResult.continueNext(); 242 } 243 244 if (upper.startsWith("PRINT")) { 245 String arg = trimmed.length() > 5 ? trimmed.substring(5).trim() : ""; 246 doPrint(arg); 247 return ExecutionResult.continueNext(); 248 } 249 250 if (upper.startsWith("LET")) { 251 String assign = trimmed.substring(3).trim(); 252 doLet(assign); 253 return ExecutionResult.continueNext(); 254 } 255 256 if (upper.startsWith("IF")) { 257 return doIf(trimmed, lineNumbers); 258 } 259 260 if (upper.startsWith("GOTO")) { 261 String arg = trimmed.substring(4).trim(); 262 int target = parseLineNumber(arg); 263 return ExecutionResult.jump(findLineIndex(lineNumbers, target)); 264 } 265 266 if (upper.equals("END")) { 267 return ExecutionResult.end(); 268 } 269 270 if (upper.equals("RUN")) { 271 runProgram(); 272 return ExecutionResult.end(); 273 } 274 275 if (trimmed.matches("^[A-Za-z][A-Za-z0-9_]*\\s*=.*$")) { 276 doLet(trimmed); 277 return ExecutionResult.continueNext(); 278 } 279 280 throw new BasicRuntimeException("SYNTAX ERROR: " + statement); 281 } 282 283 private void doPrint(String arg) { 284 if (arg.isEmpty()) { 285 outLine(""); 286 return; 287 } 288 289 if ((arg.startsWith("\"") && arg.endsWith("\"")) || (arg.startsWith("'") && arg.endsWith("'"))) { 290 outLine(arg.substring(1, arg.length() - 1)); 291 return; 292 } 293 294 int value = evalExpr(arg); 295 outLine(String.valueOf(value)); 296 } 297 298 private void initVideoOut(Machine machine) { 299 try { 300 gpu = machine.video().gpu; 301 gpu.init(Video.Modes.CGA_TEXT_40x25); 302 redrawVideo(); 303 } catch (Exception ignored) { 304 gpu = null; 305 } 306 } 307 308 private void initKeyboardIn(Machine machine) { 309 try { 310 keyboard = machine.keyboard(); 311 } catch (Exception ignored) { 312 keyboard = null; 313 } 314 } 315 316 private void outLine(String text) { 317 System.out.println(text); 318 appendScreenLine(text); 319 } 320 321 private void appendScreenLine(String text) { 322 String value = text == null ? "" : text; 323 for (String part : wrapLine(value)) { 324 screenLines.add(part); 325 } 326 327 while (screenLines.size() > VIDEO_ROWS - 2) { 328 screenLines.remove(0); 329 } 330 331 redrawVideo(); 332 } 333 334 private List<String> wrapLine(String text) { 335 List<String> wrapped = new ArrayList<>(); 336 if (text.isEmpty()) { 337 wrapped.add(""); 338 return wrapped; 339 } 340 341 int start = 0; 342 while (start < text.length()) { 343 int end = Math.min(start + VIDEO_COLUMNS, text.length()); 344 wrapped.add(text.substring(start, end)); 345 start = end; 346 } 347 return wrapped; 348 } 349 350 private void redrawVideo() { 351 if (gpu == null) { 352 return; 353 } 354 355 gpu.clear(0); 356 gpu.drawText(0, 0, fitLine("NOVA BASIC"), 15, 0, false); 357 358 int row = 1; 359 for (String line : screenLines) { 360 if (row >= VIDEO_ROWS - 1) { 361 break; 362 } 363 gpu.drawText(0, row * 8, fitLine(line), 15, 0, false); 364 row++; 365 } 366 367 String prompt = "> " + currentInput + "_"; 368 gpu.drawText(0, (VIDEO_ROWS - 1) * 8, fitLine(prompt), 14, 0, false); 369 370 gpu.present(); 371 } 372 373 private String readLineFromKeyboard() { 374 StringBuilder builder = new StringBuilder(); 375 currentInput = ""; 376 redrawVideo(); 377 378 while (true) { 379 Integer keyCode = keyboard.pollKeyCode(); 380 if (keyCode == null) { 381 sleep(10); 382 continue; 383 } 384 385 if (keyCode == KeyEvent.VK_ENTER) { 386 String line = builder.toString(); 387 appendScreenLine("> " + line); 388 System.out.println("> " + line); 389 currentInput = ""; 390 redrawVideo(); 391 return line; 392 } 393 394 if (keyCode == KeyEvent.VK_BACK_SPACE) { 395 if (builder.length() > 0) { 396 builder.deleteCharAt(builder.length() - 1); 397 currentInput = builder.toString(); 398 redrawVideo(); 399 } 400 continue; 401 } 402 403 if (keyCode == KeyEvent.VK_ESCAPE) { 404 builder.setLength(0); 405 builder.append("QUIT"); 406 currentInput = builder.toString(); 407 redrawVideo(); 408 continue; 409 } 410 411 Character mapped = mapKeyCodeToChar(keyCode); 412 if (mapped != null) { 413 builder.append(mapped); 414 currentInput = builder.toString(); 415 redrawVideo(); 416 } 417 } 418 } 419 420 private Character mapKeyCodeToChar(int keyCode) { 421 if (keyCode >= KeyEvent.VK_A && keyCode <= KeyEvent.VK_Z) { 422 return (char) ('A' + (keyCode - KeyEvent.VK_A)); 423 } 424 if (keyCode >= KeyEvent.VK_0 && keyCode <= KeyEvent.VK_9) { 425 return (char) ('0' + (keyCode - KeyEvent.VK_0)); 426 } 427 if (keyCode >= KeyEvent.VK_NUMPAD0 && keyCode <= KeyEvent.VK_NUMPAD9) { 428 return (char) ('0' + (keyCode - KeyEvent.VK_NUMPAD0)); 429 } 430 431 return switch (keyCode) { 432 case KeyEvent.VK_SPACE -> ' '; 433 case KeyEvent.VK_PERIOD, KeyEvent.VK_DECIMAL -> '.'; 434 case KeyEvent.VK_COMMA -> ','; 435 case KeyEvent.VK_MINUS, KeyEvent.VK_SUBTRACT -> '-'; 436 case KeyEvent.VK_EQUALS -> '='; 437 case KeyEvent.VK_PLUS, KeyEvent.VK_ADD -> '+'; 438 case KeyEvent.VK_SLASH, KeyEvent.VK_DIVIDE -> '/'; 439 case KeyEvent.VK_ASTERISK, KeyEvent.VK_MULTIPLY -> '*'; 440 case KeyEvent.VK_SEMICOLON -> ';'; 441 case KeyEvent.VK_COLON -> ':'; 442 case KeyEvent.VK_QUOTE -> '\''; 443 case KeyEvent.VK_OPEN_BRACKET -> '['; 444 case KeyEvent.VK_CLOSE_BRACKET -> ']'; 445 case KeyEvent.VK_BACK_SLASH -> '\\'; 446 default -> null; 447 }; 448 } 449 450 private void sleep(long millis) { 451 try { 452 Thread.sleep(millis); 453 } catch (InterruptedException e) { 454 Thread.currentThread().interrupt(); 455 } 456 } 457 458 private String fitLine(String text) { 459 String value = text == null ? "" : text; 460 if (value.length() > VIDEO_COLUMNS) { 461 return value.substring(0, VIDEO_COLUMNS); 462 } 463 return value + " ".repeat(VIDEO_COLUMNS - value.length()); 464 } 465 466 private void doLet(String assign) { 467 int eq = assign.indexOf('='); 468 if (eq <= 0) { 469 throw new BasicRuntimeException("LET EXPECTS: NAME = EXPR"); 470 } 471 472 String name = assign.substring(0, eq).trim().toUpperCase(); 473 if (!name.matches("[A-Z][A-Z0-9_]*")) { 474 throw new BasicRuntimeException("BAD VARIABLE NAME: " + name); 475 } 476 477 String expr = assign.substring(eq + 1).trim(); 478 int value = evalExpr(expr); 479 variables.put(name, value); 480 } 481 482 private ExecutionResult doIf(String statement, List<Integer> lineNumbers) { 483 String body = statement.substring(2).trim(); 484 int thenPos = body.toUpperCase().indexOf("THEN"); 485 if (thenPos < 0) { 486 throw new BasicRuntimeException("IF EXPECTS THEN"); 487 } 488 489 String cond = body.substring(0, thenPos).trim(); 490 String thenTarget = body.substring(thenPos + 4).trim(); 491 492 if (evaluateCondition(cond)) { 493 int target = parseLineNumber(thenTarget); 494 return ExecutionResult.jump(findLineIndex(lineNumbers, target)); 495 } 496 return ExecutionResult.continueNext(); 497 } 498 499 private boolean evaluateCondition(String cond) { 500 String[] ops = {"<=", ">=", "<>", "=", "<", ">"}; 501 502 for (String op : ops) { 503 int idx = cond.indexOf(op); 504 if (idx > 0) { 505 int left = evalExpr(cond.substring(0, idx).trim()); 506 int right = evalExpr(cond.substring(idx + op.length()).trim()); 507 508 return switch (op) { 509 case "=" -> left == right; 510 case "<>" -> left != right; 511 case "<" -> left < right; 512 case ">" -> left > right; 513 case "<=" -> left <= right; 514 case ">=" -> left >= right; 515 default -> false; 516 }; 517 } 518 } 519 520 throw new BasicRuntimeException("BAD IF CONDITION: " + cond); 521 } 522 523 private int parseLineNumber(String text) { 524 try { 525 return Integer.parseInt(text.trim()); 526 } catch (NumberFormatException e) { 527 throw new BasicRuntimeException("BAD LINE NUMBER: " + text); 528 } 529 } 530 531 private int findLineIndex(List<Integer> lineNumbers, int line) { 532 if (lineNumbers == null) { 533 throw new BasicRuntimeException("GOTO/IF THEN needs a running program"); 534 } 535 536 int idx = lineNumbers.indexOf(line); 537 if (idx < 0) { 538 throw new BasicRuntimeException("UNDEFINED LINE " + line); 539 } 540 return idx; 541 } 542 543 private int evalExpr(String expr) { 544 return new ExpressionParser(expr).parse(); 545 } 546 547 private static final class ExecutionResult { 548 private final Integer nextPc; 549 private final boolean endProgram; 550 551 private ExecutionResult(Integer nextPc, boolean endProgram) { 552 this.nextPc = nextPc; 553 this.endProgram = endProgram; 554 } 555 556 private static ExecutionResult continueNext() { 557 return new ExecutionResult(null, false); 558 } 559 560 private static ExecutionResult jump(int nextPc) { 561 return new ExecutionResult(nextPc, false); 562 } 563 564 private static ExecutionResult end() { 565 return new ExecutionResult(null, true); 566 } 567 } 568 569 private static final class BasicRuntimeException extends RuntimeException { 570 private BasicRuntimeException(String message) { 571 super(message); 572 } 573 } 574 575 private final class ExpressionParser { 576 private final String input; 577 private int pos; 578 579 private ExpressionParser(String input) { 580 this.input = input; 581 } 582 583 private int parse() { 584 int value = parseExpression(); 585 skipSpaces(); 586 if (pos != input.length()) { 587 throw new BasicRuntimeException("BAD EXPR: " + input); 588 } 589 return value; 590 } 591 592 private int parseExpression() { 593 int value = parseTerm(); 594 while (true) { 595 skipSpaces(); 596 if (match('+')) { 597 value += parseTerm(); 598 } else if (match('-')) { 599 value -= parseTerm(); 600 } else { 601 return value; 602 } 603 } 604 } 605 606 private int parseTerm() { 607 int value = parseFactor(); 608 while (true) { 609 skipSpaces(); 610 if (match('*')) { 611 value *= parseFactor(); 612 } else if (match('/')) { 613 int divisor = parseFactor(); 614 if (divisor == 0) { 615 throw new BasicRuntimeException("DIV BY ZERO"); 616 } 617 value /= divisor; 618 } else { 619 return value; 620 } 621 } 622 } 623 624 private int parseFactor() { 625 skipSpaces(); 626 627 if (match('(')) { 628 int value = parseExpression(); 629 skipSpaces(); 630 if (!match(')')) { 631 throw new BasicRuntimeException("MISSING )"); 632 } 633 return value; 634 } 635 636 if (match('-')) { 637 return -parseFactor(); 638 } 639 640 if (pos < input.length() && Character.isDigit(input.charAt(pos))) { 641 return parseNumber(); 642 } 643 644 if (pos < input.length() && Character.isLetter(input.charAt(pos))) { 645 String name = parseIdentifier().toUpperCase(); 646 return variables.getOrDefault(name, 0); 647 } 648 649 throw new BasicRuntimeException("BAD EXPR: " + input); 650 } 651 652 private int parseNumber() { 653 int start = pos; 654 while (pos < input.length() && Character.isDigit(input.charAt(pos))) { 655 pos++; 656 } 657 return Integer.parseInt(input.substring(start, pos)); 658 } 659 660 private String parseIdentifier() { 661 int start = pos; 662 while (pos < input.length() && (Character.isLetterOrDigit(input.charAt(pos)) || input.charAt(pos) == '_')) { 663 pos++; 664 } 665 return input.substring(start, pos); 666 } 667 668 private void skipSpaces() { 669 while (pos < input.length() && Character.isWhitespace(input.charAt(pos))) { 670 pos++; 671 } 672 } 673 674 private boolean match(char expected) { 675 if (pos < input.length() && input.charAt(pos) == expected) { 676 pos++; 677 return true; 678 } 679 return false; 680 } 681 } 682} 683[FILE END](C) 2025 0x4248 (C) 2025 4248 Media and 4248 Systems, All part of 0x4248 See LICENCE files for more information. Not all files are by 0x4248 always check Licencing.