View Source Document

Etcha.java

package tc.catseye.etcha;

// SPDX-FileCopyrightText: In 2012, Chris Pressey, the original author of this work, placed it into the public domain.
// For more information, please refer to <https://unlicense.org/>
// SPDX-License-Identifier: Unlicense

// Etcha.java
// Ridiculous over-implementation of the Etcha programming language.
// I hereby place this work into the public domain.
// Chris Pressey, Cat's Eye Technologies.
// $Id: Etcha.java 257 2009-10-04 03:21:43Z cpressey $

import java.util.List;
import java.util.ArrayList;
import java.util.Map;
import java.util.HashMap;
import java.util.Set;
import java.util.HashSet;
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;

interface Grid
{
    void togglePixel(int x, int y);
    boolean isPixelBlack(int x, int y);
}

class DefaultGrid implements Grid
{
    private HashMap<Integer, HashMap<Integer, Boolean>> grid;
    private int minX, minY, maxX, maxY;

    DefaultGrid()
    {
        grid = new HashMap<Integer, HashMap<Integer, Boolean>>();
        minX = minY = maxX = maxY = 0;
    }

    private static boolean getBoolean(Boolean b)
    {
        return b == null ? false : b;
    }

    public void togglePixel(int x, int y)
    {
        HashMap<Integer, Boolean> column = grid.get(x);
        if (column == null) {
            column = new HashMap<Integer, Boolean>();
            column.put(y, true);
            grid.put(x, column);
        } else {
            column.put(y, !getBoolean(column.get(y)));
        }
        if (x < minX) minX = x;
        if (y < minY) minY = y;
        if (x > maxX) maxX = x;
        if (y > maxY) maxY = y;
    }

    public boolean isPixelBlack(int x, int y)
    {
        HashMap<Integer, Boolean> column = grid.get(x);
        if (column == null) {
            return false;
        } else {
            return getBoolean(column.get(y));
        }
    }

    public void dump()
    {
        int width = maxY - minY;
        for (int i = 0; i <= width; i++)
            System.out.print("-");
        System.out.println();
        for (int y = minY; y <= maxY; y++) {
            for (int x = minX; x <= maxX; x++) {
                System.out.print(isPixelBlack(x, y) ? "#" : " ");
            }
            System.out.println();
        }
        for (int i = 0; i <= width; i++)
            System.out.print("-");
        System.out.println();
    }
}

interface Turtle
{
    void forward(int distance);
    void rotate(int degrees);
    void setPenDown(boolean penDown);
    public boolean isPenDown();
    boolean detectPixel();
}

class DefaultTurtle implements Turtle
{
    private Grid grid;
    private int x, y;
    private int dx, dy;
    private boolean penDown;
    private int heading;

    DefaultTurtle()
    {
        grid = new DefaultGrid();
        x = 0;
        y = 0;
        penDown = true;
        heading = 0;
    }

    DefaultTurtle(Grid g)
    {
        grid = g;
        x = 0;
        y = 0;
        penDown = true;
        heading = 0;
    }

    public void forward(int distance)
    {
        for (int i = 0; i < distance; i++) {
            if (penDown)
                grid.togglePixel(x, y);
            x += dx();
            y += dy();
        }
    }

    private int dx()
    {
        if (heading == 90)
            return 1;
        if (heading == 270)
            return -1;
        return 0;
    }

    private int dy()
    {
        if (heading == 0)
            return -1;
        if (heading == 180)
            return 1;
        return 0;
    }

    public void rotate(int degrees)
    {
        heading += degrees;
        heading %= 360;
    }

    public void setPenDown(boolean penDown)
    {
        this.penDown = penDown;
    }

    public boolean isPenDown()
    {
        return penDown;
    }

    public boolean detectPixel()
    {
        return grid.isPixelBlack(x, y);
    }
}

interface Counter
{
    public boolean count();
}

class DefaultCounter implements Counter
{
    private int modulus;
    private int counter;

    DefaultCounter()
    {
        modulus = 4;
        counter = 0;
    }

    DefaultCounter(int m)
    {
        modulus = m;
        counter = 0;
    }

    DefaultCounter(int m, int c)
    {
        modulus = m;
        counter = c;
    }

    public boolean count()
    {
        counter++;
        counter %= modulus;
        return (counter == 0);
    }
}

// An execution context knows what command it is currently executing.
interface ExecutionContext
{
    Turtle getTurtle();
    Counter getCounter();
    ExecutionContext step();
}

class BaseExecutionContext implements ExecutionContext
{
    protected Turtle turtle;
    protected Counter counter;
    protected ExecutionContext parent;

    BaseExecutionContext(Turtle t, Counter c)
    {
        turtle = t;
        counter = c;
        parent = null;
    }

    BaseExecutionContext(ExecutionContext parent)
    {
        turtle = parent.getTurtle();
        counter = parent.getCounter();
        this.parent = parent;
    }

    public Turtle getTurtle()
    {
        return turtle;
    }

    public Counter getCounter()
    {
        return counter;
    }

    public ExecutionContext step()
    {
        return parent;
    }
}

class PrimitiveExecutionContext extends BaseExecutionContext
{
    protected Command cmd;

    PrimitiveExecutionContext(Command cmd, ExecutionContext parent)
    {
        super(parent);
        this.cmd = cmd;
    }

    public ExecutionContext step()
    {
        cmd.execute(turtle, counter);
        return parent;
    }
}

class BlockExecutionContext extends PrimitiveExecutionContext
{
    protected int pos;
    protected Block cmd;

    BlockExecutionContext(Block cmd, ExecutionContext parent)
    {
        super(cmd, parent);
        this.cmd = cmd;  // because our cmd is not our parent's!
        pos = 0;
    }

    public ExecutionContext step()
    {
        Command sub = cmd.get(pos);
        if (sub == null) {
            return parent;
        }
        pos++;
        ExecutionContext e = sub.createExecutionContext(this);
        // Could just return e here.  But then we'd have to do twice as many
        // steps, stepping into each contained command
        return e.step();
    }
}

class LoopExecutionContext extends BlockExecutionContext
{
    protected Loop cmd;

    LoopExecutionContext(Loop cmd, ExecutionContext parent)
    {
        super(cmd, parent);
        this.cmd = cmd;  // because our cmd is not our parent's!
    }

    public ExecutionContext step()
    {
        if (pos >= cmd.size()) {
            pos = 0;
        }
        if (pos == 0 && !turtle.detectPixel()) {
            return parent;
        }
        return super.step();
    }
}

interface Command
{
    void execute(Turtle t, Counter c);
    ExecutionContext createExecutionContext(ExecutionContext parent);
}

abstract class AbstractCommand implements Command
{
    abstract public void execute(Turtle t, Counter c);
    public ExecutionContext createExecutionContext(ExecutionContext parent)
    {
        return new PrimitiveExecutionContext(this, parent);
    }
}

class Right extends AbstractCommand
{
    public void execute(Turtle t, Counter c)
    {
        t.rotate(90);
        if (c.count())
            t.setPenDown(!t.isPenDown());
    }
}

class Forward extends AbstractCommand
{
    public void execute(Turtle t, Counter c)
    {
        t.forward(1);
    }
}

class Block extends AbstractCommand
{
    private List<Command> cmds;

    Block(List<Command> cmds)
    {
        this.cmds = cmds;
    }

    public Command get(int index)
    {
        try {
            return cmds.get(index);
        } catch (IndexOutOfBoundsException e) {
            return null;
        }
    }

    public int size()
    {
        return cmds.size();
    }

    public void execute(Turtle t, Counter c)
    {
        for (Command cmd : cmds) {
            cmd.execute(t, c);
        }
    }

    public ExecutionContext createExecutionContext(ExecutionContext parent)
    {
        return new BlockExecutionContext(this, parent);
    }
}

class Loop extends Block
{
    Loop(List<Command> cmds)
    {
        super(cmds);
    }

    public void execute(Turtle t, Counter c)
    {
        while (t.detectPixel()) {
            super.execute(t, c);
        }
    }

    public ExecutionContext createExecutionContext(ExecutionContext parent)
    {
        return new LoopExecutionContext(this, parent);
    }
}

class Executor
{
    Turtle turtle;
    Counter counter;

    Executor()
    {
        turtle = new DefaultTurtle();
        counter = new DefaultCounter();
    }

    Executor(Turtle t, Counter c)
    {
        turtle = t;
        counter = c;
    }

    Executor(Grid g)
    {
        turtle = new DefaultTurtle(g);
        counter = new DefaultCounter();
    }

    public void run(Command program)
    {
        program.execute(turtle, counter);
    }

    // same as run, but uses the step methods
    public void stepAll(Command program)
    {
        ExecutionContext base = new BaseExecutionContext(turtle, counter);
        ExecutionContext e = program.createExecutionContext(base);
        while (e != base) {
            e = e.step();
        }
    }
}

interface Warner
{
    void warn(String warning);
}

interface ParseFactory<X>
{
    char getRepresentation();
    X generate(Parser p);
}

abstract class AbstractParseFactory<X> implements ParseFactory<X>
{
    public abstract char getRepresentation();
    public abstract X generate(Parser p);
}

class ForwardParseFactory extends AbstractParseFactory<Command>
{
    public char getRepresentation()
    {
        return '+';
    }

    public Command generate(Parser p)
    {
        return new Forward();
    }
}

class RightParseFactory extends AbstractParseFactory<Command>
{
    public char getRepresentation()
    {
        return '>';
    }

    public Command generate(Parser p)
    {
        return new Right();
    }
}

class LoopParseFactory extends AbstractParseFactory<Command>
{
    public char getRepresentation()
    {
        return '[';
    }

    public Command generate(Parser p)
    {
        p.advance();
        Set<Character> until = new HashSet<Character>();
        until.add(new Character(']'));
        List<Command> cmds = p.parseMany(until);
        return new Loop(cmds);
    }
}

class Parser
{
    private String str;
    private int pos;
    private Warner warner;
    private Map<Character,ParseFactory<Command>> factoryMap;

    Parser(String s)
    {
        str = s;
        pos = 0;
        warner = null;
        factoryMap = new HashMap<Character,ParseFactory<Command>>();
        ArrayList<ParseFactory<Command>> factories = new ArrayList<ParseFactory<Command>>();
        factories.add(new ForwardParseFactory());
        factories.add(new RightParseFactory());
        factories.add(new LoopParseFactory());
        for (ParseFactory<Command> factory : factories) {
            factoryMap.put(factory.getRepresentation(), factory);
        }
    }

    public void setWarner(Warner w)
    {
        warner = w;
    }

    public void advance()
    {
        pos++;
    }

    public Command parseOne()
    {
        char c = str.charAt(pos);
        Command cmd = null;

        ParseFactory<Command> factory = factoryMap.get(c);
        if (factory != null) {
            cmd = factory.generate(this);
        } else if (warner != null) {
            warner.warn("unrecognized command '" + c +
                        "' at position " + pos);
        }
        advance();

        return cmd;
    }

    public List<Command> parseMany(Set<Character> until)
    {
        List<Command> cmds = new ArrayList<Command>();

        while (pos < str.length()) {
            char c = str.charAt(pos);
            if (until != null && until.contains(c))
                break;
            Command cmd = parseOne();
            if (cmd != null)
                cmds.add(cmd);
        }

        return cmds;
    }
}

// A command-line interface to our Etcha interpreter.
class Main
{
    static public void main(String argv[])
    {
        // parse command line arguments
        for (String arg : argv)
            loadAndInterpret(arg);
    }

    static void loadAndInterpret(String filename)
    {
        // open and read file
        StringBuffer fileContents = new StringBuffer();
        try {
            BufferedReader input = new BufferedReader(new FileReader(filename));
            String str;
            str = input.readLine();
            while (str != null) {
                fileContents.append(str);
                str = input.readLine();
            }
            input.close();
        } catch (IOException e) {
            System.err.println("Couldn't read '" + filename + "'");
            return;
        }
        Parser parser = new Parser(fileContents.toString());
        Set<Character> empty = new HashSet<Character>();
        Block program = new Block(parser.parseMany(empty));
        DefaultGrid grid = new DefaultGrid();
        Executor executor = new Executor(grid);
        executor.stepAll(program);
        grid.dump();
    }
}