gehacktes /// noniq.at

0002Die 90er haben angerufen: Sie wollen Ihre Pixel zurück!

Pixelgrafik wie damals – in GW-Basic mit Zeilennummern.

Schon am C128 hab ich gern kleine Basic-Programme geschrieben, die Linien und Kreise auf den Bildschirm malten – allerdings war das Thema bei 320×200 Pixeln und 2 Farben bald ausgereizt. Ganz anders dann auf meinem ersten PC1: 640×350 Pixel in 16 Farben2 – so viele Auflösung, und nochdazu in Farbe und bunt!

Programmiert hab ich am PC damals in GW-Basic: Keine Spur von Methoden, lokalen Variablen oder ähnlichem neumodischen Zeugs – man hatte Zeilennummern, das musste reichen.

Ein (minimaler) GW-Basic-Emulator

Um dieses Feeling nochmal zu erleben hab ich irgendwann 2013 einen Mini-Emulator für Pixelbilder in GW-Basic geschrieben, der folgende Befehle unterstützt:

Außerdem sind Zuweisungen zu Variablen und mathematische Ausdrücke, in denen Variablen vorkommen können, möglich. Folgendes ist zu beachten:

Vier kurze, bearbeitbare Beispielprogramme zum Ausprobieren und Rumspielen:

250 Zeilen CoffeeScript

Der Emulator besteht aus ca. 250 Zeilen CoffeeScript. Damit der Output möglichst originalgetreu „pixelig“ ausschaut4, verwende ich zum Zeichnen von Linien und Kreisen keine Canvas-Funktionen, sondern erzeuge diese Figuren Pixel für Pixel mit dem Bresenham-Algorithmus (siehe die Funktionen line und circle gegen Schluss des Codes).

Disclaimer: Einen Parser / Interpreter auf Basis von RegExps zu schreiben funktioniert ungefähr so gut wie HTML mit RegExps zu parsen, nämlich eigentlich gar nicht. Für Spaßprojekte reicht’s aber. Wie man sowas richtig™ macht, lässt sich z.B. im – überhaupt sehr lesenswerten – Buch Understanding Computation nachlesen (Affiliatelink).

# Sample usage:
#   canvas = document.getElementById("gw-canvas")
#   interpreter = new GWBasicGraphics(new Screen(canvas))
#   interpreter.run("… [program code here] …")

class @GWBasicGraphics
  constructor: (@screen, @tickDelay = 20) ->

  run: (source, callbacks) ->
    @onError = callbacks.onError
    @onStop  = callbacks.onStop
    try
      @program  = new Program(source)
      @env      = new Environment(@program.lines.first(), @screen)
      @ticker   = setInterval(@tick, @tickDelay)
    catch err
      @stop()
      @onError(err)

  stop: ->
    clearInterval(@ticker)
    @onStop()

  tick: =>
    try
      if @env.currentLine then @step() else @stop()
    catch err
      @stop()
      @onError(err, @env.currentLine)

  step: ->
    line = @env.currentLine
    line.execute(@env)
    @env.currentLine = if @env.jumpToLineNr
      @program.lineWithNr(@env.jumpToLineNr)
    else if @env.redoCurrentLoop
      @program.lineAfter(@env.currentLoop().startLine)
    else
      @program.lineAfter(line)
    @env.jumpToLineNr = null
    @env.redoCurrentLoop = false


class Program
  constructor: (source) ->
    @lines = @parseLines(source)

  lineAfter: (givenLine) ->
    @lines.find (line) -> line.nr > givenLine.nr

  lineWithNr: (nr) ->
    @lines.find (line) -> line.nr == nr

  parseLines: (source) ->
    lines = source.split("\n").map (line) => @parseLine(line)
    lines.compact().sortBy (line) => line.nr

  parseLine: (string) ->
    return if string.match(/^ *$/)
    if matches = string.match(/^ *(\d+) +(.*)/)
      [nr, rest] = matches.splice(1)
      new Line(parseInt(nr, 10), rest)
    else
      throw "SYNTAX ERROR: #{string}"


class Line
  constructor: (@nr, @source) ->
    @statement = new Statement(@source)

  execute: (env) ->
    @statement.execute(env)


class Statement
  @EXECUTORS: [
    { 
      regex: /^GOTO (\d+) *$/,
      impl: (m, env) ->
        lineNr = parseInt(m[1], 10)
        env.jumpToLineNr = lineNr
    }, { 
      regex: /^FOR (\w+) *= *(.+) TO (.+?)(?: +STEP +(.+))? *$/, 
      impl: (m, env) -> 
        [variable, from, to, step] = m.slice(1)
        env.variables[variable] = env.evaluate(from)
        env.addLoop(variable, env.evaluate(to), env.evaluate(step)) 
    }, { 
      regex: /^NEXT *$/, 
      impl: (m, env) -> 
        loopData = env.currentLoop()
        env.variables[loopData.variable] += loopData.step
        if env.variables[loopData.variable] <= loopData.bound
          env.redoCurrentLoop = true
        else
          env.removeLoop()
    }, { 
      regex: /^IF +(.+) +THEN +(.+?)(?: +ELSE +(.*))? *$/,
      impl: (m, env) ->
        [condition, ifTrue, ifFalse] = m.slice(1)
        stmtSource = if env.evaluate(condition) then ifTrue else ifFalse
        new Statement(stmtSource).execute(env) if stmtSource
    }, { 
      regex: /^CLS *$/,
      impl: (m, env) ->
        env.screen.clear()
    }, { 
      regex: /^LINE (?:\((.+?), *(.+?)\))? *- *\((.+?), *(.+?)\)(?:,(?: *([^,]+))?(?:, *(BF?))?)? *$/,
      impl: (m, env) -> 
        [x1, y1, x2, y2] = m.slice(1, 5).map (expr) -> env.evaluate(expr) if expr
        x1 ?= env.lastReferencedPoint.x
        y1 ?= env.lastReferencedPoint.y
        env.lastReferencedPoint = {x: x2, y: y2}
        color = if m[5] then env.evaluate(m[5]) else 15
        switch m[6]
          when "BF" then env.screen.filledRect(x1, y1, x2, y2, color)
          when "B"  then env.screen.rect(x1, y1, x2, y2, color)
          else           env.screen.line(x1, y1, x2, y2, color)
    }, { 
      regex: /^CIRCLE \((.+?), *(.+?)\), *([^,]+?)(, *(.+))? *$/,
      impl: (m, env) -> 
        [x, y, radius] = m.slice(1, 4).map (expr) -> env.evaluate(expr)
        color = if m[5] then env.evaluate(m[5]) else 15
        env.screen.circle(x, y, radius, color)
    }, { 
      regex: /^(\w+) *= *(.+) *$/,
      impl: (m, env) -> 
        [variable, expr] = m.slice(1)
        env.variables[variable] = env.evaluate(expr)
    }, { 
      regex: /.+/,
      impl: (m, env) -> throw "SYNTAX ERROR" 
    }
  ]

  constructor: (@source) ->
    executor = Statement.EXECUTORS.find (e) => e.regex.test(@source)
    matches = @source.match(executor.regex)
    @implementation = executor.impl.bind(this, matches)

  execute: (env) ->
    @implementation(env)


class Environment
  constructor: (@currentLine, @screen, @variables = {}) ->
    @jumpToLineNr = null
    @redoCurrentLoop = false
    @loops = []
    @lastReferencedPoint = {x: 0, y: 0}

  addLoop: (variable, bound, step = 1) ->
    @loops.push(variable: variable, bound: bound, step: step, startLine: @currentLine)

  currentLoop: ->
    @loops.last()

  removeLoop: ->
    @loops.pop()

  evaluate: (expr) ->
    return unless expr
    for variable, value of @variables
      expr = expr.replace(new RegExp("\\b#{variable}\\b", "g"), value)
    expr = expr.replace(/RND/g, "Math.random()")
    eval(expr)


class @Screen
  COLORS = [
    "#000000", "#0000AA", "#00AA00", "#00AAAA", 
    "#AA0000", "#AA00AA", "#AA5500", "#AAAAAA", 
    "#555555", "#5555FF", "#55FF55", "#55FFFF", 
    "#FF5555", "#FF55FF", "#FFFF55", "#FFFFFF" ]

  constructor: (@canvas) ->
    @ctx       = @canvas.getContext("2d")
    @width     = @canvas.width
    @height    = @canvas.height
    @imageData = @ctx.createImageData(@width, @height)
    @clear()

  clear: ->
    @ctx.fillStyle = COLORS[0]
    @ctx.fillRect(0, 0, @width, @height)

  line: (x1, y1, x2, y2, color = 15) ->
    [x1, y1, x2, y2, color] = [x1, y1, x2, y2, color].map (v) -> Math.floor(v)
    dx = Math.abs(x2 - x1)
    dy = Math.abs(y2 - y1) 
    sx = if x1 < x2 then 1 else -1
    sy = if y1 < y2 then 1 else -1
    err = dx - dy
    loop
      @setPixel(x1, y1, color)
      break if x1 == x2 and y1 == y2
      e2 = 2 * err
      if e2 > -dy 
        err -= dy
        x1 += sx 
      if e2 < dx
        err += dx
        y1 += sy 

  rect: (x1, y1, x2, y2, color = 15) ->
    [x1, y1, x2, y2, color] = [x1, y1, x2, y2, color].map (v) -> Math.floor(v)
    @line(x1, y1, x2, y1, color)
    @line(x1, y2, x2, y2, color)
    @line(x1, y1, x1, y2, color)
    @line(x2, y1, x2, y2, color)

  filledRect: (x1, y1, x2, y2, color = 15) ->
    [x1, y1, x2, y2, color] = [x1, y1, x2, y2, color].map (v) -> Math.floor(v)
    @ctx.fillStyle = COLORS[color]
    [x2, x1] = [x1, x2] if x1 > x2
    [y2, y1] = [y1, y2] if y1 > y2
    @ctx.fillRect(x1, y1, x2 - x1, y2 - y1)

  circle: (x0, y0, radius, color = 15) ->
    [x0, y0, radius, color] = [x0, y0, radius, color].map (v) -> Math.floor(v)
    x = radius
    y = 0
    radiusError = 1 - x
    while x >= y
      @setPixel(x + x0, y + y0, color)
      @setPixel(y + x0, x + y0, color)
      @setPixel(-x + x0, y + y0, color)
      @setPixel(-y + x0, x + y0, color)
      @setPixel(-x + x0, -y + y0, color)
      @setPixel(-y + x0, -x + y0, color)
      @setPixel(x + x0, -y + y0, color)
      @setPixel(y + x0, -x + y0, color)
      y += 1
      if radiusError < 0
        radiusError += 2 * y + 1
      else
        x -= 1
        radiusError += 2 * (y - x + 1)


  setPixel: (x, y, color) ->
    @ctx.fillStyle = COLORS[color]
    @ctx.fillRect(x, y, 1, 1)


  1. Ein 386SX mit 2 MB RAM und VGA-Grafikkarte – gerade genug, um Windows 3.1 im „Extended Modus“ (echtes präemptives Multitasking!) betreiben zu können. 

  2. Eigentlich konnte die Grafikkarte sogar 640×480 in 16 Farben (VGA) – aber bei GW-Basic war mit EGA, also 640×350, Schluss (SCREEN 9, falls sich noch wer erinnert). 

  3. Auf diese Weise kann man dem Emulator natürlich auch diverse Sachen unterjubeln, die in GW-Basic nicht möglich waren – z.B. JavaScript-Objekte etc. Aber das wäre natürlich stillos … 

  4. Anti-Aliasing war noch eher unbekannt. 



0001Eine sd2iec für den Commodore 128WLAN für den Tischtenniskeller0003