簡易タートルグラフィクス環境を作ってみた。

by termat
勉強がてら、簡易タートルグラフィクス環境を作ってみた。
 下部のテキストフィールドにLispもどきのスクリプトをいれ、
 「右クリック」→「メニュー:実行」で描画が開始される。
 まだ、関数定義ができないので、たいした図形は描けそうにない。
 とりあえず、ソースの行数500~600程度に抑えて、コッホ曲線が
 描けるようにしたいなあ。
♥0 | Line 732 | Modified 2010-01-17 03:39:41 | MIT License
play

ActionScript3 source code

/**
 * Copyright termat ( http://wonderfl.net/user/termat )
 * MIT License ( http://www.opensource.org/licenses/mit-license.php )
 * Downloaded from: http://wonderfl.net/c/idLq
 */

package
{
	/**
	 勉強がてら、簡易タートルグラフィクス環境を作ってみた。
	 下部のテキストフィールドにLispもどきのスクリプトをいれ、
	 「右クリック」→「メニュー:実行」で描画が開始される。
	 まだ、関数定義ができないので、たいした図形は描けそうにない。
	 とりあえず、ソースの行数500~600程度に抑えて、コッホ曲線が
	 描けるようにしたいなあ。
	*/
	import flash.display.Sprite;
	import flash.events.ContextMenuEvent;
	import flash.events.Event;
	import flash.text.TextField;
	import flash.text.TextFieldType;
	import flash.ui.ContextMenu;
	import flash.ui.ContextMenuItem;

	[SWF(framerate="30",width="480",height="480",backgroundColor="0xffffff")]
	public class Turtle extends Sprite
	{
		private var text:TextField;
		private var vm:VM;
		private var tg:TurtleGraphics;
		
		public function Turtle() 
		{
			text = new TextField();
            text.width = 470;	
			text.height = 70;
            text.multiline = true;
			var str:String = "(width 4)\n(point (350 250))\n(def n 0)\n(def r 255)\n(def g 0)\n(def b 0)\n";
			str = str + "(while (< n 200)\n((color (r g b))\n(move (- 50 (/ n 4)))\n(turn -20)\n(set n (+ n 1))\n";
			str=str+"(set r ( - r 2))\n(set g ( + g 1))\n(set b (+ b 2)))\n)"
			text.text = str;
            text.border = true;
            text.type = TextFieldType.INPUT;
            text.x = 5;
            text.y = 405;
			this.addChild(text);
			var cm:ContextMenu = new ContextMenu();
			cm.hideBuiltInItems();
			addItem("実 行",cm);
			addItem("コマンド消去",cm);
			addItem("画面初期化",cm);
			text.contextMenu = cm;
			tg = new TurtleGraphics(this, 480, 400);
			vm = new VM();
			TurtleFunc.turtle = tg;
			tg.init();
			addEventListener(Event.ENTER_FRAME,tg.play);
		}
		
		private function addItem(str:String,cm:ContextMenu):void {
			var item1:ContextMenuItem = new ContextMenuItem(str);
			item1.separatorBefore = true;
			item1.enabled = true;
			cm.customItems.push(item1);
			item1.addEventListener(ContextMenuEvent.MENU_ITEM_SELECT, onSelectItem);
		}
		
		private function interpret():void {
			vm.run(text.text);
			vm.stackFlush();
			tg.playing = true;
		}
		
		private function onSelectItem(e:ContextMenuEvent):void {
			var str:String = e.target.caption;
			if (str == "コマンド消去") {
				text.text = "";
			}else if (str == "実 行") {
				interpret();
			}else if (str == "画面初期化") {
				vm.stackFlush();
				tg.clear();
				tg.init();
			}
		}
	}
}

import flash.display.MovieClip;
import flash.display.Sprite;
import flash.events.WeakMethodClosure;
import flash.geom.Point;
import flash.events.Event;
//import org.flashdevelop.utils.FlashConnect;

/* パーサー */
class Parser {
	private const LPAR:int="(".charCodeAt(0);
	private const RPAR:int=")".charCodeAt(0);
	private const PLUS:int="+".charCodeAt(0);
	private const MINUS:int="-".charCodeAt(0);
	private const MUL:int="*".charCodeAt(0);
	private const DIV:int="/".charCodeAt(0);	
	private const EQ:int="=".charCodeAt(0);
	private const GT:int=">".charCodeAt(0);
	private const LT:int="<".charCodeAt(0);
	private const WHITE:int = " ".charCodeAt(0);
	private const DIGIT_0:int = "0".charCodeAt(0);
	
	private var line:String;
	private var index:int;
	private var currentLine:int;

	/* 文字列の解釈 */
	public function parse(str:String):Array {
		if (str.length==0) return new Array();
		index = 0;
		currentLine = 0;
		var code:Array = new Array();
		line = str;
		line = (line.split("\r\n")).join("\n");
		line = (line.split("\r")).join("\n");
		while (index < line.length) {
			var o:Object = getToken();
			if (o != null) code.push(o);
		}
		return code;
	}
	
	/* 解釈したコードの表示 */
	public static function show(c:Array):String {
		var ret:String = "(";
		for (var i:int = 0; i < c.length; i++) {
			var o:Object = c[i];
			if (o is Array) {
				ret = ret + show(o as Array)+" ";
			}else if (o is Symbol) {
				ret = ret + (o as Symbol).toStr()+" ";
			}else if (o is Func) {
				ret = ret + (o as Func).toStr()+" ";
			}else {
				ret = ret + o.toString()+" ";
			}
		}
		ret = ret + ")";
		return ret;
	}
	
	private function getToken():Object {
		if (index < line.length) {
			var str:String = line.charAt(index++);
			if (isDigit(str)) return number(str);
			if (isDelimiter(str)) return getToken();
			var ch:int = str.charCodeAt(0);
			switch(ch) {
				case MUL:
				case DIV:
				case EQ:
				case GT:
				case LT:
					return symbol(str);
				case LPAR:
					return cell();
				case PLUS:
				case MINUS:
					return plus(str);
				default:
					return symbol(str);	
			}
		}else {
			return null;
		}
	}
	
	private function isDelimiter(s:String):Boolean {
		var n:int = s.charCodeAt(0);
		if (n == WHITE) {
			return true;
		}else {
			return (s == "\n");
		}
	}
	
	private function isDigit(s:String):Boolean {
		var ch:int = s.charCodeAt(0);
		return (ch >= DIGIT_0 && ch <= DIGIT_0 + 9);
	}
	
	private function symbol(ch:String):Object {
		var ret:String = ch;
		while (index < line.length) {
			var str:String = line.charAt(index++);
			if(isDelimiter(str)){
				break;
			}else {
				if (str.charCodeAt(0) == RPAR) {
					index--;
					return new Symbol(ret);
				}else {
					ret = ret + str;
				}
			}
		}
		return new Symbol(ret);
	}
	
	private function plus(ch:String):Object {
		var str:String = line.charAt(index++);
		if (isDelimiter(str)) {
			return new Symbol(ch);
		}else if (isDigit(str)) {
			return number(ch+str);
		}else {
			return symbol(ch);
		}
	}
	
	private function number(ch:String):Object {
		while (index < line.length) {
			var str:String = line.charAt(index++);
			if (isDigit(str)) {
				ch = ch + str;
			}else if (isDelimiter(str)) {
				break;
			}else {
				if (str.charCodeAt(0) == RPAR) {
					index--;
					return parseFloat(ch);
				}else {
					throw(new Error("Syntax Error. line="+currentLine.toString(),0));
				}
			}
		}
		return parseFloat(ch);
	}
	
	private function cell():Object{
		var ret:Array = new Array();
		var obj:Object;
		while (true) {
			if (index < line.length) {
				var str:String = line.charAt(index);
				if (str == "\n") {
					currentLine++;
					index++;
				}else if(isDelimiter(str)){
					index++;
				}else {
					var ch:int = str.charCodeAt(0);
					switch(ch){
						case RPAR:
							index++;
							return ret;
						default:
							obj = getToken();
							if (obj != null) ret.push(obj);
					}
				}
			}else{
				break;
			}
		}
		throw(new Error("Syntax Error. line="+currentLine.toString(),0));
	}
	
	public function compile(list:Array):Array {
		var ret:Array = new Array();
		while (list.length>0) {
			var obj:Object = list.shift();
			if (obj is Array) {
				ret.push(compile(obj as Array));
			}else if (obj is Symbol) {
				ret.push(obj);
				ret.push(VM.ACC);
			}else {
				ret.push(obj);
			}
		}
		return ret;
	}

}

class Env {
	private var map:Array;
	private var parent:Env = null;

	public function Env(e:Env=null):void {
		parent = e;
		map = new Array();
	}
	
	/* 上位の環境を取得 */
	public function getParent():Env {
		return parent;
	}
	
	/* 最上位の環境を取得 */
	public function getRoot():Env {
		if (parent == null) {
			return this;
		}else {
			return parent.getRoot();
		}
	}
	
	/* シンボルの登録 */
	public function put(key:Symbol, val:Object):void {
		map[key.str] = val;
	}
	
	/* 値の取得 */
	public function get(key:Symbol):Object {
		if (map[key.str] != undefined) {
			return map[key.str];
		}else {
			if (parent == null) {
				return null;
			}else {
				return parent.get(key);
			}
		}
	}
	
	/* 値の更新 */
	public function set(key:Symbol, val:Object):void {
		if (map[key.str] == undefined) {
			if (parent != null) {
				parent.set(key, val);
			}
		}else {
			map[key.str] = val;
		}
	}
}

/* シンボルオブジェクト */
class Symbol {
	public var str:String;
	public function Symbol(s:String):void {
		str = s.toLocaleLowerCase();
	}
	public function toStr():String{return "["+str+"]"}
}

/* インタプリタ */
class VM {
	public static const ACC:Symbol = new Symbol("#ACC");
	internal var stack:Array;
	internal var env:Env = new Env();
	internal var dump:Array;
	private var parser:Parser = new Parser();
	
	public function VM():void {
		stack = new Array();
		env = new Env();
		dump = new Array();
		env.put(new Symbol("+"), new NumFunc(NumFunc.ADD));
		env.put(new Symbol("-"), new NumFunc(NumFunc.SUB));
		env.put(new Symbol("*"), new NumFunc(NumFunc.MUL));
		env.put(new Symbol("/"), new NumFunc(NumFunc.DIV));
		env.put(new Symbol("="), new CompFunc(CompFunc.EQ));
		env.put(new Symbol(">"), new CompFunc(CompFunc.GT));
		env.put(new Symbol("<"), new CompFunc(CompFunc.LT));
		env.put(new Symbol("and"), new CompFunc(CompFunc.AND));
		env.put(new Symbol("or"), new CompFunc(CompFunc.OR));
		env.put(new Symbol("def"), new Bind(Bind.DEF));
		env.put(new Symbol("set"), new Bind(Bind.SET));
		env.put(new Symbol("while"), new While());
		env.put(new Symbol("move"), new TurtleFunc(TurtleFunc.MOVE));
		env.put(new Symbol("turn"), new TurtleFunc(TurtleFunc.TURN));
		env.put(new Symbol("point"), new TurtleFunc(TurtleFunc.POINT));
		env.put(new Symbol("color"), new TurtleFunc(TurtleFunc.COLOR));
		env.put(new Symbol("width"), new TurtleFunc(TurtleFunc.WIDTH));
		env.put(new Symbol("dir"), new TurtleFunc(TurtleFunc.DIR));
		env.put(new Symbol("clear"), new TurtleFunc(TurtleFunc.CLEAR));
	}
	
	public function run(s:String):void {
		var c:Array = parser.parse(s);
//		FlashConnect.trace(Parser.show(c));
		eval(c);
		stackFlush();
	}
	
	public function stackFlush():void {
//		while(stack.length>0)FlashConnect.trace(stack.pop());
	}
	
	public function eval(code:Array):void {
		pushEnv();
		var obj:Object;
		while (code.length > 0) {
			obj = code.shift();
			if (obj is Array) {
				eval(obj as Array);
			}else if(obj is Func){
				(obj as Func).accept(code,this);
			}else if (obj is Symbol) {
				obj = env.get(obj as Symbol);
				if (obj != null) {
					code.unshift(obj);
				}else {
					throw(new Error("Error. :"+obj,0));
				}
			}else {
				stack.push(obj);
			}
		}
		popEnv();
	}
	
	public function pushEnv():void {
		dump.push(new Array(stack, env));
		stack = new Array();
		env = new Env(env);
	}
	
	public function popEnv():void {
		var tmp:Array = dump.pop();
		var val:Array = stack;
		stack = tmp[0] as Array;
		if (val.length == 1) {
			stack.push(val.pop());
		}else if (val.length > 1) {
			stack.push(val);
		}
		env = tmp[1] as Env;
	}
	
}

class Func {
	protected var id:int;
	public function Func(i:int):void {id = i;}
	public function accept(code:Array, vm:VM):void {}
	public function toStr():String{return "#FUNC"}
}

class Bind extends Func {
	public static const DEF:int = 0;
	public static const SET:int = 1;
	
	public function Bind(i:int ):void {
		super(i);
	}
	
	public override function accept(code:Array, vm:VM):void {
		vm.pushEnv();
		var obj:Object = code.shift();
		if (obj is Symbol) {
			var sym:Symbol = obj as Symbol;
			var val:Object = code.shift();
			if (val is Array) {
				vm.eval(val as Array);
				bind(vm,sym,vm.stack.pop());
			}else if (val is Symbol) {
				val = vm.env.get(val as Symbol);
				if (val == null) {
					throw(new Error("Error. :"+obj,0));
				}else {
					bind(vm,sym,val);
				}
			}else {
				bind(vm,sym,val);
			}
			while (code.length > 0) code.pop();
		}else {
			throw(new Error("Error. :"+obj,0));
		}
		vm.popEnv();
	}
	
	private function bind(vm: VM, sym:Symbol,obj:Object):void {
		if (id == DEF) {
			vm.env.getRoot().put(sym,obj);
		}else {
			var tmp:Object = vm.env.get(sym);
			if (tmp == null) {
				vm.env.put(sym,obj);
			}else {
				vm.env.set(sym, obj);
			}
		}
	}
	
}

class NumFunc extends Func {
	public static const ADD:int = 0;
	public static const SUB:int = 1;
	public static const MUL:int = 2;
	public static const DIV:int = 3;
	
	public function NumFunc(i:int ):void {
		super(i);
	}
	
	public override function accept(code:Array,vm:VM):void {
		vm.pushEnv();
		var obj:Object;
		while (code.length > 0) {
			obj = code.shift();
			if (obj is Array) {
				vm.eval(obj as Array);
				number(vm, vm.stack.pop() as Number);
			}else if (obj is Symbol) {
				obj = vm.env.get(obj as Symbol);
				if (obj != null) {
					code.unshift(obj);
				}else {
					throw(new Error("Error. :"+obj,0));
				}
			}else if (obj is Number) {
				number(vm, obj as Number);
			}else if(obj is Boolean){
				boolean(vm,obj as Boolean);
			}else {
				throw(new Error("Error. :"+obj,0));
			}
		}
		vm.popEnv();
	}
	
	protected function number(vm:VM, n:Number):void {
		if (vm.stack.length == 0) {
			vm.stack.push(n);
		}else {
			var val:Number = vm.stack.pop() as Number;
			switch(id) {
				case ADD:
					vm.stack.push(val + n);
					break;
				case SUB:
					vm.stack.push(val - n);
					break;
				case MUL:
					vm.stack.push(val * n);
					break;
				default:
					vm.stack.push(Math.round(val / n));
					break;
			}
		}
	}
	
	protected function boolean(vm:VM, n:Boolean):void {
		throw(new Error("Error. :"+n,0));	
	}
}

class While extends Func {
	private var flg:Array;
	private var code:Array;
	
	public function While() {
		super(0);
	}
	
	public override function accept(code:Array, vm:VM):void {
		this.flg = code[0] as Array;
		this.code = code[1] as Array;
		doLoop(vm);
	}
	
	private function doLoop(vm:VM):void {
		vm.pushEnv();
		var limit:int = 0;
		while (isLoop(vm)) {
			var tp:Array = copyArray(code);
			vm.eval(tp);
			if (limit++ > 400) break;	//暴走防止。現段階では、ループは400回までに制限
		}
		vm.popEnv();			
	}
	
	private function isLoop(vm:VM):Boolean {
		vm.pushEnv();
		var tp:Array = copyArray(flg);
		vm.eval(tp);
		var val:Object = vm.stack.pop();
		vm.popEnv();
		if (val is Boolean) {
			return val;
		}else {
			throw(new Error("Error. :",0));
		}
	}
	
	private function copyArray(s:Array):Array {
		var ret:Array = new Array();
		for (var i:int = 0; i < s.length; i++) {
			if (s[i] is Array) {
				ret.push(copyArray(s[i] as Array));
			}else {
				ret.push(s[i]);
			}
		}
		return ret;
	}
}

class CompFunc extends NumFunc {
	public static const EQ:int = 0;
	public static const GT:int = 1;
	public static const LT:int = 2;
	public static const AND:int = 3;
	public static const OR:int = 4;
	
	public function CompFunc(i:int ):void {
		super(i);
	}
	protected override function number(vm:VM, n:Number):void {
		if (vm.stack.length == 0) {
			vm.stack.push(n);
		}else {
			var val:Number = vm.stack.pop() as Number;
			switch(id) {
				case EQ:
					vm.stack.push((val == n));
					break;
				case GT:
					vm.stack.push((val > n));
					break;
				case LT:
					vm.stack.push((val < n));
					break;
				default:
					throw(new Error("Error. :"+n,0));
			}
		}
	}
	
	protected override function boolean(vm:VM, n:Boolean):void {
		if (vm.stack.length == 0) {
			vm.stack.push(n);
		}else {
			var val:Boolean = vm.stack.pop() as Boolean;
			switch(id) {
				case EQ:
				case GT:
				case LT:
					throw(new Error("Error. :" + n, 0));
				case AND:
					vm.stack.push(val && n);
				case AND:
					vm.stack.push(val || n);
				default:
					throw(new Error("Error. :"+n,0));
			}
		}
	}
}

class TurtleFunc extends Func {
	public static var turtle:TurtleGraphics;
	public static const MOVE:int = 0;
	public static const TURN:int = 1;
	public static const POINT:int = 2;
	public static const COLOR:int = 3;
	public static const WIDTH:int = 4;
	public static const DIR:int = 5;
	public static const CLEAR:int = 6;
	
	public function TurtleFunc(i:int):void {
		super(i);
	}
	
	public override function accept(code:Array, vm:VM):void {
		vm.pushEnv();
		var obj:Object = code.shift();
		if (obj is Number) {
			play(obj);
		}else if (obj is Array) {
			vm.eval(obj as Array);
			play(vm.stack.pop());
		}else if (obj is Symbol) {
			obj = vm.env.get(obj as Symbol);
			if (obj != null) {
					code.unshift(obj);
			}else {
				throw(new Error("Error. :"+obj,0));
			}
		}else {
			throw(new Error("Error. :"+obj,0));
		}
		vm.popEnv();
	}
	
	private function play(obj:Object):void {
		switch(id) {
			case MOVE:
				if (obj is Number) {
					turtle.move(obj as Number);
				}else {
					throw(new Error("Error. :"+obj,0));
				}
				break;
			case TURN:
				if (obj is Number) {
					turtle.turn(obj as Number);
				}else {
					throw(new Error("Error. :"+obj,0));
				}
				break;
			case POINT:
				if (obj is Array) {
					var tp:Array = obj as Array;
					turtle.setPoint(tp[0] as Number, tp[1] as Number);
				}else {
					throw(new Error("Error. :"+obj,0));
				}
				break;
			case COLOR:
				if (obj is Array) {
					tp= obj as Array;
					turtle.setLineColor(tp[0] as Number,tp[1] as Number,tp[2] as Number);
				}else {
					throw(new Error("Error. :"+obj,0));
				}
				break;
			case WIDTH:
				if (obj is Number) {
					turtle.setLineWidth(obj as Number);
				}else {
					throw(new Error("Error. :"+obj,0));
				}
				break;
			case DIR:
				if (obj is Number) {
					turtle.dir(obj as Number);
				}else {
					throw(new Error("Error. :"+obj,0));
				}
				break;
			case CLEAR:
				turtle.clear();
		}
	}
}


/* タートルグラフィクス */
class TurtleGraphics {
	private var sp:Sprite;
	private var pos:Point;
	private var angle:Number;
	private var vx:Number;
	private var vy:Number;
	private var lineColor:uint;
	private var lineWidth:Number;
	private var data:Array;
	public var playing:Boolean = false;
	
	public function TurtleGraphics(s:Sprite,w:Number,h:Number):void {
		sp = s;
		pos = new Point();
		sp.graphics.lineStyle(1, 0) ;
		sp.graphics.drawRect(1, 1, w - 2, h - 2);
		data = new Array();
	}
	
	public function init():void {
		clear();
		pos.x = sp.width / 2;
		pos.y = sp.height / 2;
		angle = -90;
		turn(0);
		setLineColor(0,0,0);
		setLineWidth(1);
	}
	
	public function play(e:Event):void {
		if (playing && data.length > 0) {
			var l:Line = data.shift();
			sp.addChild(l);
			if (data.length == 0) playing = false;
		}
	}
	
	public function turn(deg:Number):void {
		angle += deg;
		vx = Math.cos(angle / 180 * Math.PI);
		vy = Math.sin(angle / 180 * Math.PI);
	}
	
	public function move(v:Number):void {
		var p2:Point = new Point(pos.x + vx * v, pos.y + vy * v);
		var l:Line = new Line(pos, p2,lineColor,lineWidth);
		data.push(l);
		pos = p2;
	}
	
	public function dir(deg:Number):void {
		angle = deg;
		vx = Math.cos(angle / 180 * Math.PI);
		vy = Math.sin(angle / 180 * Math.PI);
	}
	
	public function setPoint(xx:Number, yy:Number ):void {
		pos.x = xx;
		pos.y = yy;
	}
	
	public function clear():void {
		while (sp.numChildren > 1) {
			sp.removeChildAt(sp.numChildren - 1);
			
		}
	}
	
	public function setLineColor(r:Number, g:Number, b:Number):void {
			if (r > 255) r = 255;
			if (g > 255) g = 255;
			if (b > 255) b = 255;
			if (r < 0) r = 0;
			if (g < 0) g = 0;
			if (b < 0) b = 0;
			lineColor = (r<<16)+(g<<8)+b as uint;
	}
	
	public function setLineWidth(c:Number):void {
		lineWidth = c;
	}
}

class Line extends MovieClip {
	public function Line(p0:Point, p1:Point, col:uint, wd:Number) {
		graphics.lineStyle(wd, col);
		graphics.moveTo(p0.x, p0.y);
		graphics.lineTo(p1.x,p1.y);
	}
}