/**********************************************/
/* */
/* LOGO Asteroids */
/* */
/**********************************************/
/* This code was written by Matthew Scroggs */
/* */
/* mscroggs.co.uk/logo-asteroids */
/* github.com/mscroggs/logo-asteroids */
/**********************************************/
/* This code is licensed under an MIT license */
/**********************************************/
// constants
var WIDTH = 800
var HEIGHT = 450
var VERSION = "1.0"
// game data
var turtle = {"x": WIDTH/2, "y": HEIGHT/2, "rotation": 0, "pd": true, "st": true}
var gameoptions = {"bg": "#000000", "pc": "#FFFFFF", "pensize": 2}
var drawnlines = []
var asterN = 2
var asteroids = []
var explosions = []
var fires = []
var score = 0
var lives = 0
var running = false
var cmd_history = []
var history_n = 0
var history_overwritten = {}
var custom_commands = {}
var global_variables = {}
var builtindesc = "0=BLACK,1=BLUE,2=GREEN,3=CYAN,4=RED,5=MAGENTA,6=YELLOW,7=WHITE"
var builtin = [[0, 0, 0], [0, 0, 255], [0, 255, 0], [0, 255, 255], [255, 0, 0], [255, 0, 255], [255, 255, 0], [255, 255, 255]]
var commands = [
// FORMAT: [ [command(s)], "description", [example(s)], [arg(s)]]
// turtle commands
[["bk", "back", "backward"], "move backward", ["bk 100"], ["NUMBER"], false],
[["cs", "clearscreen", "clean"], "erase all the lines", [], [], false],
[["distance"], "get the distance from the turtle to a point", ["print distance 100 50"], ["NUMBER", "NUMBER"], true],
[["end"], "ends a procedure", ["to square repeat 4 [fd 100 rt 90] end"], [], true],
[["er", "erase"], "erase a (custom) prodecure", ["er square"], ["STRING"], false],
[["erall", "eraseall"], "erase all (custom) procedures", [], [], false],
[["fd", "forward"], "move forward", ["fd 100"], ["NUMBER"], false],
[["heading"], "the current heading (angle with horizontal) of the turtle", ["print heading"], [], true],
[["home"], "move the turtle back to the centre", [], [], false],
[["ht", "hideturtle"], "hide the turtle", [], [], false],
[["lt", "left"], "turn left", ["lt 60"], ["NUMBER"], false],
[["make"], "set a variable", ["make :a 10"], [":", "NUMBER"], false],
[["pd", "pendown"], "put the pen down: after this is done, lines will be drawn", [], [], false],
[["pos", "position"], "the turtle's current position", ["print pos"], [], true],
[["posx", "positionx"], "the turtle's current x-position", ["print posx"], [], true],
[["posy", "positiony"], "the turtle's current y-positioon", ["print posy"], [], true],
[["pots", "printouttitles"], "print out names of (custom) procedures", [], [], false],
[["print", "pr"], "print out a value", ["print random 100"], ["QUOTE OR NUMBER"], false],
[["pu", "penup"], "lift the pen up: after this is done, lines will not be drawn", [], [], false],
[["random"], "pick a random integer", ["fd random 100"], ["INT"], true],
[["repcount"], "the current iteration number in a repeat loop", ["repeat 4 [fd 10 * repcount rt 90]"], [], true],
[["repeat"], "repeat a set of commands", ["repeat 4 [fd 100 rt 90]"], ["INT", "[]"], false],
[["reset", "cleargraphics", "cg"], "erase all the lines are move back to the centre", [], [], false],
[["rt", "right"], "turn right", ["rt 90"], ["NUMBER"], false],
[["setbg", "setbackground"], "set the background color to a built-in color (" + builtindesc + ") OR AN RGB COLOR", ["setbg 3", "setbg [255 163 0]"], ["COLOR"], false],
[["setpc", "setpencolor"], "set the pen color to a built-in color (" + builtindesc + ") OR AN RGB COLOR", ["setbg 3", "setbg [255 163 0]"], ["COLOR"], false],
[["setpensize"], "set the pen size (from 0 to 255)", ["setpensize 20"], ["INT"], false],
[["seth", "setheading"], "set the turtle's heading (angle with horizontal)", [], ["NUMBER"], false],
[["setx"], "set the turtle's x position", [], ["NUMBER"], false],
[["setxy"], "set the turtle's x and y positions", [], ["NUMBER", "NUMBER"], false],
[["sety"], "set the turtle's y position", [], ["NUMBER"], false],
[["st", "showturtle"], "show the turtle", [], [], false],
[["to"], "define a (custom) procedure", ["to square repeat 4 [fd 100 rt 90] end", "to square :size repeat 4 [fd :size rt 90] end"], ["STRING", "?: ... END"], false],
[["towards"], "point the turtle towards a point", ["towards 100 50"], ["NUMBER", "NUMBER"], false],
// maths commands
[["arccos", "acos"], "compute the inverse cosine of a number", ["print arccos 0.5"], ["NUMBER"], true],
[["arcsin", "asin"], "compute the inverse sine of a number", ["print arcsin 0.5"], ["NUMBER"], true],
[["arctan", "atan"], "compute the inverse tangent of a number", ["print arctan 0.5"], ["NUMBER"], true],
[["abs"], "compute the absolute value of a number", ["print abs -100"], ["NUMBER"], true],
[["cos", "cosine"], "compute the cosine of a number", ["print cos 30"], ["NUMBER"], true],
[["exp"], "compute e to the power of a number", ["print exp 2"], ["NUMBER"], true],
[["ln", "log"], "compute the log (base e) of a number", ["print ln 2"], ["NUMBER"], true],
[["quotient"], "compute the integer division of two numbers", ["print quotient 10 3"], ["INT", "INT"], true],
[["remainder"], "compute the remainder when a value is divided by another number", ["print remainder 10 3"], ["INT", "INT"], true],
[["sin", "sine"], "compute the sine of a number", ["print sin 30"], ["NUMBER"], true],
[["sum"], "compute the sum of two values", ["print sum 30 50"], ["NUMBER", "NUMBER"], true],
[["sqrt"], "compute the square root of a number", ["print sin 30"], ["NUMBER"], true],
[["tan", "tangent"], "compute the tangent of a number", ["print tan 30"], ["NUMBER"], true],
// special commands
[["fire"], "fire at the asteroids", [], [], false],
[["help"], "show command help", [], [], false],
[["start"], "start the game", [], [], false],
]
function parse_arg(cmds, format, variables) {
if (cmds.length == 0) {
return ["ERROR", "NUMBER IS MISSING"]
}
var value = cmds.shift()
if (format == "QUOTE OR NUMBER") {
if (value[0] == "\"") {
value = value.substr(1)
while (cmds.length > 0) {
value += " " + cmds.shift()
}
return ["QUOTE", value]
} else if (value == "pos") {
return ["QUOTE", turtle["x"] + " " + turtle["y"]]
} else {
format = "NUMBER"
}
}
if (format == "NUMBER" || format == "INT") {
var out = ["ERROR", "ERROR PARSING NUMBER"]
var trig_functions = {
"cos": Math.cos,
"sin": Math.sin,
"tan": Math.tan,
}
var atrig_functions = {
"arccos": Math.acos,
"arcsin": Math.asin,
"arctan": Math.atan,
}
var functions = {
"abs": Math.abs,
"sqrt": Math.sqrt,
"exp": Math.exp,
"ln": Math.log,
}
for (var i = 0; i < commands.length; i++) {
for (var j = 0; j < commands[i][0].length; j++) {
if (value == commands[i][0][j]) {
value = commands[i][0][0]
}
}
}
if (value == "random") {
var next = parse_arg(cmds, "INT", variables)
if (next[0] == "ERROR") {
return next
}
out = [format, Math.floor(Math.random() * next[1])]
} else if (value == "heading") {
out = [format, turtle["rotation"] * 180 / Math.PI]
if (format == "INT") {
out[1] = Math.floor(out[1])
}
} else if (value == "posx") {
out = [format, turtle["x"]]
if (format == "INT") {
out[1] = Math.floor(out[1])
}
} else if (value == "posy") {
out = [format, turtle["y"]]
if (format == "INT") {
out[1] = Math.floor(out[1])
}
} else if (value == "distance") {
var next1 = parse_arg(cmds, "NUMBER", variables)
var next2 = parse_arg(cmds, "NUMBER", variables)
if (next1[0] == "ERROR") {
return next1
}
if (next2[0] == "ERROR") {
return next2
}
if (next1[1] < 0 || next1[1] > WIDTH) {
return ["ERROR", "FIRST INPUT TO `distance` MUST BE BETWEEN 0 AND " + WIDTH]
}
if (next2[1] < 0 || next2[1] > HEIGHT) {
return ["ERROR", "SECOND INPUT TO `distance` MUST BE BETWEEN 0 AND " + HEIGHT]
}
out = ["NUMBER", Math.sqrt(Math.pow(next1[1] - turtle["x"], 2) + Math.pow(next2[1] - turtle["y"], 2))]
} else if (value == "quotient") {
var next1 = parse_arg(cmds, "INT", variables)
var next2 = parse_arg(cmds, "INT", variables)
if (next1[0] == "ERROR") {
return next1
}
if (next2[0] == "ERROR") {
return next2
}
out = ["INT", Math.floor(next1[1] / next2[1])]
} else if (value == "remainder") {
var next1 = parse_arg(cmds, "INT", variables)
var next2 = parse_arg(cmds, "INT", variables)
if (next1[0] == "ERROR") {
return next1
}
if (next2[0] == "ERROR") {
return next2
}
out = ["INT", next1[1] % next2[1]]
} else if (value == "sum") {
var next1 = parse_arg(cmds, "NUMBER", variables)
var next2 = parse_arg(cmds, "NUMBER", variables)
if (next1[0] == "ERROR") {
return next1
}
if (next2[0] == "ERROR") {
return next2
}
out = ["NUMBER", next1[1] + next2[1]]
} else if (value in trig_functions) {
var next = parse_arg(cmds, "NUMBER", variables)
if (next[0] == "ERROR") {
return next
}
if (value == "tan" && (next[1] - 90) % 180 == 0) {
return ["ERROR", "MATH DOMAIN ERROR"]
}
out = [format, trig_functions[value](next[1] * Math.PI / 180)]
} else if (value in atrig_functions) {
var next = parse_arg(cmds, "NUMBER", variables)
if (next[0] == "ERROR") {
return next
}
if ((value == "arccos" || value == "arcsin") && (next[1] > 1 || next[1] < -1)) {
return ["ERROR", "MATH DOMAIN ERROR"]
}
out = [format, atrig_functions[value](next[1]) * 180 / Math.PI]
} else if (value in functions) {
var next = parse_arg(cmds, "NUMBER", variables)
if (next[0] == "ERROR") {
return next
}
out = [format, functions[value](next[1])]
} else if (value == "repcount") {
if ("!repcount" in variables) {
out = [format, variables["!repcount"]]
} else {
return ["ERROR", "`repcount` MUST BE USED INSIDE A REPEAT LOOP"]
}
} else if (value[0] == ":") {
var v = value.substr(1)
if (v in variables) {
out = [format, variables[v]]
} else {
return ["ERROR", "VARIABLE `:" + v + "` HAS NO VALUE"]
}
} else if (format == "NUMBER") {
if(isNaN(value)) {
return ["ERROR", "COULD NOT PARSE NUMBER `" + value + "`"]
}
out = [format, value / 1]
} else { // format == "INT"
if(isNaN(value)) {
return ["ERROR", "COULD NOT PARSE INTEGER `" + value + "`"]
}
if (value * 1 != Math.floor(value*1)) {
return ["ERROR", "COULD NOT PARSE INTEGER `" + value + "`"]
}
out = [format, value * 1]
}
if (cmds.length > 0 && (cmds[0] == "+" || cmds[0] == "*" || cmds[0] == "/" || cmds[0] == "-")) {
var op = cmds.shift()
var next = parse_arg(cmds, format, variables)
if (next[0] == "ERROR") {
return next
}
if (op == "+") {
return [format, out[1] + next[1]]
}
if (op == "-") {
return [format, out[1] - next[1]]
}
if (op == "*") {
return [format, out[1] * next[1]]
}
if (op == "/") {
return [format, out[1] / next[1]]
}
}
return out
}
if (format == "STRING") {
value = value.toLowerCase()
if(!value.match(/^[a-z_][a-z0-9_]*$/)) {
return ["ERROR", "`" + value + "` IS AN INVALID NAME"]
}
return [format, value]
} else if (format == "COLOR") {
var col = [0, 0, 0]
if (value[0] == "[") {
var bracketed = value.substr(1)
while (cmds.length >= 0) {
if (bracketed[bracketed.length - 1] == "]" && (bracketed.match(/\[/g) || []).length + 1 == (bracketed.match(/\]/g) || []).length) {
bracketed = bracketed.substr(0, bracketed.length - 1)
break
}
if (cmds.length == 0) {break}
bracketed += " " + cmds.shift()
}
if ((bracketed.match(/\[/g) || []).length != (bracketed.match(/\]/g) || []).length) {
return ["ERROR", "INVALID SQUARE BRACKETS"]
}
bracketed = bracketed.substr(0, bracketed.length - 1).split(" ")
var n0 = parse_arg(bracketed, "INT", variables)
if (n0[0] == "ERROR") { return n0 }
var n1 = parse_arg(bracketed, "INT", variables)
if (n1[0] == "ERROR") { return n1 }
var n2 = parse_arg(bracketed, "INT", variables)
if (n2[0] == "ERROR") { return n2 }
if (bracketed.length > 0) { return ["ERROR", "TOO MANY NUMBERS"] }
col = [n0[1], n1[1], n2[1]]
} else {
cmds.unshift(value)
var col_n = parse_arg(cmds, "INT", variables)
if (col_n[0] == "ERROR") { return col_n }
col_n = col_n[1]
if (col_n >= 0 && col_n < builtin.length && col_n == Math.floor(col_n)) {
col = builtin[col_n]
} else { return ["ERROR", "INVALID COLOR `" + col_n + "`"] }
}
if (col[0] < 0 || col[0] > 255 || col[1] < 0 || col[1] > 255 || col[2] < 0 || col[2] > 255) {
return ["ERROR", "RGB VALUES MUST BE BETWEEN 0 and 255"]
}
return ["COLOR", "#" + to_hex(col[0]) + to_hex(col[1]) + to_hex(col[2])]
} else if (format == ":") {
value = value.toLowerCase()
if(!value.match(/^:[a-z_][a-z0-9_]*$/)) {
return ["ERROR", "`" + value + "` IS AN INVALID NAME"]
}
return [format, value]
} else if (format == "[]") {
if (value[0] != "["){
return ["ERROR", "INPUT TO COMMAND `" + cmd + "` NEEDS SQUARE BRACKETS"]
}
var bracketed = value.substr(1)
while (cmds.length >= 0) {
if (bracketed[bracketed.length - 1] == "]" && (bracketed.match(/\[/g) || []).length + 1 == (bracketed.match(/\]/g) || []).length) {
bracketed = bracketed.substr(0, bracketed.length - 1)
break
}
if (cmds.length == 0) {break}
bracketed += " " + cmds.shift()
}
if ((bracketed.match(/\[/g) || []).length != (bracketed.match(/\]/g) || []).length) {
return ["ERROR", "INPUT TO COMMAND `" + cmd + "` NEEDS SQUARE BRACKETS"]
}
return [format, bracketed.split(" ")]
} else if (format == "?: ... END") {
inputs = []
while (value[0] == ":") {
if(!value.match(/^:[a-z_][a-z0-9_]*$/)) {
return ["ERROR", "`" + value + "` IS AN INVALID NAME"]
}
inputs.push(value.substr(1))
value = cmds.shift()
}
var body = value
while (value != "end" && cmds.length > 0) {
value = cmds.shift()
body += " " + value
}
if (value != "end") {
return ["ERROR", "BODY OF SUBROUTINE MUST CONCLUDE WITH `end`"]
}
body = body.substr(0, body.length - 4)
return [format, [inputs, body.split(" ")]]
} else {
return ["ERROR", "PARSING OF INPUT OF TYPE `" + format + "` NOT SUPPORTED"]
}
}
function expand_commands(cmds, depth, variables) {
if (cmds.length == 0) { return [] }
if (depth > 50) { return [["ERROR", "MAXIMUM RECURSION DEPTH REACHED"]] }
var args = []
var cmd = cmds.shift()
if (cmd in custom_commands) {
for (var i = 0; i < custom_commands[cmd][0].length; i++) {
args.push("NUMBER")
}
} else {
for (var i = 0; i < commands.length; i++){
for (var j = 0; j < commands[i][0].length; j++) {
if (cmd == commands[i][0][j]) {
if (commands[i][4]) {
return [["ERROR", "COMMAND `" + cmd + "` MUST BE USED INSIDE ANOTHER COMMAND"]]
} else {
args = commands[i][3]
cmd = commands[i][0][0]
}
}
}
}
}
if (args.length > cmds.length) {
return [["ERROR", "WRONG NUMBER OF INPUTS FOR COMMAND `" + cmd + "`"]]
}
var c = [cmd]
for (var i = 0; i < args.length; i++) {
arg = parse_arg(cmds, args[i], variables)
if (arg[0] == "ERROR") {
return [arg]
}
c.push(arg[1])
}
var out = []
if (c[0] == "repeat") {
var new_variables = {}
for (var v in global_variables) {
new_variables[v] = global_variables[v]
}
for (var v in variables) {
new_variables[v] = variables[v]
}
for (var j = 0; j < c[1]; j++) {
new_variables["!repcount"] = j
var repeat_me = [].concat(c[2])
out = out.concat(expand_commands(repeat_me, depth + 1, new_variables))
}
} else if (c[0] in custom_commands) {
var new_variables = {}
for (var v in global_variables) {
new_variables[v] = global_variables[v]
}
for (var v in variables) {
new_variables[v] = variables[v]
}
for (var j = 0; j < custom_commands[c[0]][0].length; j++) {
new_variables[custom_commands[c[0]][0][j]] = c[j+1]
}
var cc = []
for (var i = 0; i < custom_commands[c[0]][1].length; i++) {
cc.push(custom_commands[c[0]][1][i])
}
out = expand_commands(cc, depth + 1, new_variables)
} else if (c[0] == "to") {
if (is_command(c[1])) {
return [["ERROR", "CANNOT OVERWRITE BUILT IN COMMAND `" + c[1] + "`"]]
}
custom_commands[c[1]] = c[2]
} else {
out.push(c)
}
return out.concat(expand_commands(cmds, depth, variables))
}
function to_hex(n) {
var digits = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "A", "B", "C", "D", "E", "F"]
return digits[Math.floor(n / 16)] + digits[n % 16]
}
function run_command() {
var infobox = document.getElementById("infobox")
var inputbox = document.getElementById("inputbox")
var command = inputbox.value
command = command.split(";")[0]
while (command[0] == " ") {
command = command.substr(1)
}
while (command[command.length - 1] == " ") {
command = command.substr(0, command.length - 1)
}
while (command.includes(" ")) {
command = command.replaceAll(" ", " ")
}
if (command == "") {
return
}
if (cmd_history[cmd_history.length - 1] != command) {
cmd_history.push(command)
}
history_n = 0
history_overwritten = {}
if (infobox.innerHTML != "") {
infobox.innerHTML += "\n"
}
infobox.innerHTML += command
inputbox.value = ""
var error = false
var distance = 0
var firecount = 0
var cmds = expand_commands(command.toLowerCase().split(" "), 0, global_variables)
for (var i = 0; i < cmds.length; i++) {
var c = cmds[i]
if (c[0] == "ERROR") {
infobox.innerHTML += "\n " + c[1]
error = true
}
if (c[0] == "fd" || c[0] == "bk") {
distance += Math.abs(c[1])
}
if (c[0] == "fire") {
firecount++
}
}
if (distance > 1000) {
error = true
infobox.innerHTML += "\n CANNOT TRAVEL MORE THAN 1000 UNITS IN ONE COMMAND"
}
if (firecount > 10) {
error = true
infobox.innerHTML += "\n CANNOT FIRE MORE THAN 10 TIMES IN ONE COMMAND"
}
if (!error) {
if (!running) {
while (cmds.length > 0) {
var c = cmds.shift()
if (c[0] == "start" && !running) {
start_game()
} else if (c[0] == "help" && !running) {
show_logohelp()
} else if (lives == 0) {
infobox.innerHTML += "\n CANNOT RUN COMMAND WHEN GAME NOT RUNNING. RUN `start` TO BEGIN"
}
}
} else {
while (cmds.length > 0) {
var c = cmds.shift()
if (c[0] == "fd") {
var old_x = turtle["x"]
var old_y = turtle["y"]
turtle["x"] += c[1] * Math.cos(turtle["rotation"])
turtle["y"] += c[1] * Math.sin(turtle["rotation"])
if (turtle["pd"]) {
make_line([old_x, old_y], [turtle["x"], turtle["y"]])
}
turtle_wrap()
} else if (c[0] == "bk") {
var old_x = turtle["x"]
var old_y = turtle["y"]
turtle["x"] -= c[1] * Math.cos(turtle["rotation"])
turtle["y"] -= c[1] * Math.sin(turtle["rotation"])
if (turtle["pd"]) {
make_line([old_x, old_y], [turtle["x"], turtle["y"]])
}
turtle_wrap()
} else if (c[0] == "home") {
if (turtle["pd"]) {
make_line([turtle["x"], turtle["y"]], [WIDTH/2, HEIGHT/2])
}
turtle["x"] = WIDTH/2
turtle["y"] = HEIGHT/2
turtle_wrap()
} else if (c[0] == "rt") {
turtle["rotation"] += c[1] * Math.PI / 180
while (turtle["rotation"] > 2 * Math.PI) { turtle["rotation"] -= 2 * Math.PI}
while (turtle["rotation"] < 0) { turtle["rotation"] += 2 * Math.PI}
} else if (c[0] == "lt") {
turtle["rotation"] -= c[1] * Math.PI / 180
while (turtle["rotation"] > 2 * Math.PI) { turtle["rotation"] -= 2 * Math.PI}
while (turtle["rotation"] < 0) { turtle["rotation"] += 2 * Math.PI}
} else if (c[0] == "seth") {
if (c[1] >= 0 && c[1] <= 360) {
turtle["rotation"] = c[1] * Math.PI / 180
} else {
infobox.innerHTML += "\n HEADING MUST BE SET TO A VALUE BETWEEN 0 AND 360"
}
} else if (c[0] == "setbg") {
gameoptions["bg"] = c[1]
} else if (c[0] == "setpc") {
gameoptions["pc"] = c[1]
} else if (c[0] == "setpensize") {
if (c[1] < 0 || c[1] > 255) {
infobox.innerHTML += "\n PENSIZE MUST BE BETWEEN 0 AND 255"
} else {
gameoptions["pensize"] = 0.2 + c[1] / 20
}
} else if (c[0] == "towards") {
if (c[1] >= 0 && c[1] <= WIDTH && c[2] >= 0 && c[2] <= HEIGHT) {
turtle["rotation"] = Math.atan2(c[2] - turtle["y"], c[1] - turtle["x"])
}
if (c[1] < 0 || c[1] > WIDTH) {
infobox.innerHTML += "\n X MUST BE A VALUE BETWEEN 0 AND " + WIDTH
}
if (c[2] < 0 || c[2] > HEIGHT) {
infobox.innerHTML += "\n Y MUST BE A VALUE BETWEEN 0 AND " + HEIGHT
}
} else if (c[0] == "setx") {
if (c[1] >= 0 && c[1] <= WIDTH) {
if (turtle["pd"]) {
make_line([turtle["x"], turtle["y"]], [c[1], turtle["y"]])
}
turtle["x"] = c[1]
} else {
infobox.innerHTML += "\n X MUST BE SET TO A VALUE BETWEEN 0 AND " + WIDTH
}
} else if (c[0] == "sety") {
if (c[1] >= 0 && c[1] <= HEIGHT) {
if (turtle["pd"]) {
make_line([turtle["x"], turtle["y"]], [turtle["x"], c[1]])
}
turtle["y"] = c[1]
} else {
infobox.innerHTML += "\n Y MUST BE SET TO A VALUE BETWEEN 0 AND " + HEIGHT
}
} else if (c[0] == "setxy") {
if (c[1] >= 0 && c[1] <= WIDTH && c[2] >= 0 && c[2] <= HEIGHT) {
if (turtle["pd"]) {
make_line([turtle["x"], turtle["y"]], [c[1], c[2]])
}
turtle["x"] = c[1]
turtle["y"] = c[2]
}
if (c[1] < 0 || c[1] > WIDTH) {
infobox.innerHTML += "\n X MUST BE SET TO A VALUE BETWEEN 0 AND " + WIDTH
}
if (c[2] < 0 || c[2] > HEIGHT) {
infobox.innerHTML += "\n Y MUST BE SET TO A VALUE BETWEEN 0 AND " + HEIGHT
}
} else if (c[0] == "pu") {
turtle["pd"] = false
} else if (c[0] == "pd") {
turtle["pd"] = true
} else if (c[0] == "ht") {
turtle["st"] = false
} else if (c[0] == "st") {
turtle["st"] = true
} else if (c[0] == "cs") {
drawnlines = []
} else if (c[0] == "print") {
infobox.innerHTML += "\n " + c[1]
} else if (c[0] == "pots") {
var list = ""
for (var f in custom_commands) {
if (list != "") { list += ", " }
list += f
}
infobox.innerHTML += "\n " + list
} else if (c[0] == "reset") {
drawnlines = []
turtle = {"x": WIDTH/2, "y": HEIGHT/2, "rotation": 0, "pd": true, "st": true}
} else if (c[0] == "er") {
if (c[1] in custom_commands) {
delete custom_commands[c[1]]
} else {
infobox.innerHTML += "\n CANNOT ERASE `" + c[1] + "` (DOES NOT EXIST)"
}
} else if (c[0] == "make") {
global_variables[c[1].substr(1)] = c[2]
} else if (c[0] == "erall") {
custom_commands = {}
} else if (c[0] == "help") {
show_logohelp()
} else if (c[0] == "fire") {
fires.push({"age": 40,"x": turtle["x"]+15*Math.cos(turtle["rotation"]),"y": turtle["y"]+15*Math.sin(turtle["rotation"]),"rotation": turtle["rotation"]})
} else if (c[0] == "start") {
infobox.innerHTML += "\n GAME ALREADY RUNNING"
} else {
infobox.innerHTML += "\n UNKNOWN COMMAND `" + c[0] + "`"
}
}
}
}
infobox.scrollTop = infobox.scrollHeight
}
function turtle_wrap() {
while (turtle["x"] > WIDTH) {
turtle["x"] -= WIDTH
}
while (turtle["x"] < 0) {
turtle["x"] += WIDTH
}
while (turtle["y"] > HEIGHT) {
turtle["y"] -= HEIGHT
}
while (turtle["y"] < 0) {
turtle["y"] += HEIGHT
}
while (turtle["rotation"] > 2*Math.PI) {
turtle["rotation"] -= 2*Math.PI
}
while (turtle["rotation"] < 0) {
turtle["rotation"] += 2*Math.PI
}
}
function make_line(start, end) {
if (start[0] < 0) {
make_line([start[0] + WIDTH, start[1]], [end[0] + WIDTH, end[1]])
} else if (start[0] > WIDTH) {
make_line([start[0] - WIDTH, start[1]], [end[0] - WIDTH, end[1]])
} else if (start[1] < 0) {
make_line([start[0], start[1] + HEIGHT], [end[0], end[1] + HEIGHT])
} else if (start[1] > HEIGHT) {
make_line([start[0], start[1] - HEIGHT], [end[0], end[1] - HEIGHT])
} else if (end[0] < 0) {
var y = start[1] - start[0] * (end[1] - start[1]) / (end[0] - start[0])
make_line(start, [0, y])
make_line([WIDTH, y], [end[0] + WIDTH, end[1]])
} else if (end[0] > WIDTH) {
var y = start[1] + (WIDTH - start[0]) * (end[1] - start[1]) / (end[0] - start[0])
make_line(start, [WIDTH, y])
make_line([0, y], [end[0] - WIDTH, end[1]])
} else if (end[1] < 0) {
var x = start[0] - start[1] * (end[0] - start[0]) / (end[1] - start[1])
make_line(start, [x, 0])
make_line([x, HEIGHT], [end[0], end[1] + HEIGHT])
} else if (end[1] > HEIGHT) {
var x = start[0] + (HEIGHT - start[1]) * (end[0] - start[0]) / (end[1] - start[1])
make_line(start, [x, HEIGHT])
make_line([x, 0], [end[0], end[1] - HEIGHT])
} else {
drawnlines.push([start[0], start[1], end[0], end[1], 500, gameoptions["pc"], gameoptions["pensize"]])
}
}
function add_lines(ctx, points) {
for (var x = -1; x <= 1; x++) {
for (var y = -1; y <= 1; y++) {
ctx.moveTo(x*WIDTH + points[0][0], y*HEIGHT + points[0][1])
for (var i = 1; i < points.length; i++) {
ctx.lineTo(x*WIDTH + points[i][0], y*HEIGHT + points[i][1])
}
}
}
}
function draw_game() {
var canvas = document.getElementById("asteroids")
var ctx = canvas.getContext("2d")
ctx.fillStyle = gameoptions["bg"]
ctx.fillRect(0,0,WIDTH,HEIGHT)
for (var i = 0; i < drawnlines.length; i++) {
var line = drawnlines[i]
ctx.globalAlpha = Math.min(1, drawnlines[i][4] / 50)
ctx.strokeStyle = drawnlines[i][5]
ctx.lineWidth = drawnlines[i][6]
ctx.beginPath()
ctx.moveTo(line[0], line[1])
ctx.lineTo(line[2], line[3])
ctx.stroke()
}
ctx.globalAlpha = 1
ctx.strokeStyle = "#FFFFFF"
ctx.lineWidth = 2
ctx.beginPath()
if (lives > 0 && turtle["st"]) {
add_lines(ctx, [
[turtle["x"], turtle["y"]],
[turtle["x"]+10*Math.cos(3*Math.PI/4+turtle["rotation"]),
turtle["y"]+10*Math.sin(3*Math.PI/4+turtle["rotation"])],
[turtle["x"]+15*Math.cos(turtle["rotation"]),
turtle["y"]+15*Math.sin(turtle["rotation"])],
[turtle["x"]+10*Math.cos(-3*Math.PI/4+turtle["rotation"]),
turtle["y"]+10*Math.sin(-3*Math.PI/4+turtle["rotation"])],
[turtle["x"], turtle["y"]]
])
}
for (var i = 0; i < asteroids.length; i++) {
var a = asteroids[i]
var r = radius(a["sides"])
var pts = []
for (var j = 0; j <= a["sides"]; j++) {
pts.push([a["x"] + r*Math.cos(a["rotation"] + 2*Math.PI * j / a["sides"]),
a["y"] + r*Math.sin(a["rotation"] + 2*Math.PI * j / a["sides"])])
}
add_lines(ctx, pts)
}
ctx.stroke()
for (var i = 0; i < fires.length; i++) {
var f = fires[i]
ctx.strokeStyle = "#FFFFFF"
ctx.globalAlpha = Math.min(1, f["age"] / 15)
ctx.beginPath()
add_lines(ctx, [[f["x"], f["y"]], [f["x"]+10*Math.cos(f["rotation"]), f["y"]+10*Math.sin(f["rotation"])]])
ctx.stroke()
}
for (var i = 0; i < explosions.length; i++) {
var e = explosions[i]
ctx.strokeStyle = "#FFFFFF"
ctx.globalAlpha = Math.min(1, e["age"] / 15)
ctx.beginPath()
add_lines(ctx, [[e["x"], e["y"]], [e["x"]+5/e["speed"]*Math.cos(e["rotation"]), e["y"]+5/e["speed"]*Math.sin(e["rotation"])]])
ctx.stroke()
}
ctx.globalAlpha = 1
ctx.font = "15px monospace"
ctx.fillStyle = "#FFFFFF"
ctx.textAlign = "right"
ctx.fillText("SCORE: " + score, WIDTH-5, 20)
ctx.fillText("LIVES: " + lives, WIDTH-5, 38)
}
function radius(sides) {
return (sides * sides - sides + 30) / 6
}
function pass() {}
var interval = setInterval(pass, 10000)
function start_game() {
running = true
reset()
tick()
clearInterval(interval)
interval = setInterval(tick,1000/60)
}
function gameover() {
setTimeout(show_gameover, 1000)
}
function show_gameover() {
end_game()
var canvas = document.getElementById("asteroids")
var ctx = canvas.getContext("2d")
ctx.fillStyle = "#000000"
ctx.fillRect(200,200,WIDTH-400,HEIGHT-350)
ctx.font = "25px monospace"
ctx.fillStyle = "#FFFFFF"
ctx.textAlign = "center"
ctx.fillText("GAME OVER", WIDTH/2, HEIGHT/2)
ctx.font = "15px monospace"
ctx.fillText("SCORE: " + score, WIDTH/2, HEIGHT/2 + 30)
ctx.fillText("RUN COMMAND `start` TO PLAY AGAIN", WIDTH/2, HEIGHT/2 + 60)
}
function end_game() {
clearInterval(interval)
interval = setInterval(pass, 10000)
running = false
}
function reset() {
turtle = {"x": WIDTH/2, "y": HEIGHT/2, "rotation": 0, "pd": true, "st": true}
asteroids = make_new_asteroids(asterN)
drawnlines = []
fires = []
explosions = []
score = 0
lives = 3
}
function tick() {
move_asteroids()
move_fires()
move_explosions()
check_collisions()
update_lines()
draw_game()
}
function update_lines() {
var new_lines = []
for (var i = 0; i < drawnlines.length; i++) {
drawnlines[i][4] -= 1
if (drawnlines[i][4] > 0){
new_lines.push(drawnlines[i])
}
}
drawnlines = new_lines
}
function check_collisions() {
var new_asteroids = []
var fired = []
for (var j = 0; j < fires.length; j++) {
fired.push(false)
}
var ship_points = [
[turtle["x"], turtle["y"]],
[turtle["x"]+10*Math.cos(3*Math.PI/4+turtle["rotation"]),
turtle["y"]+10*Math.sin(3*Math.PI/4+turtle["rotation"])],
[turtle["x"]+15*Math.cos(turtle["rotation"]),
turtle["y"]+15*Math.sin(turtle["rotation"])],
[turtle["x"]+10*Math.cos(-3*Math.PI/4+turtle["rotation"]),
turtle["y"]+10*Math.sin(-3*Math.PI/4+turtle["rotation"])],
]
for (var i = 0; i < asteroids.length; i++) {
var a = asteroids[i]
var hit = false
for (var j = 0; j < fires.length; j++) {
var f = fires[j]
if (Math.pow(f["x"]-a["x"], 2) + Math.pow(f["y"]-a["y"], 2) < Math.pow(radius(a["sides"]), 2)) {
hit = true
fired[j] = true
for (var k = 0; k < 6; k++) {
explosions.push({"x": f["x"], "y": f["y"], "rotation": Math.random()*Math.PI*2, "speed": 0.5 + 2 * Math.random(), "age": 50})
}
score += 10*(7-a["sides"])
if (a["sides"] > 3) {
for (var k = 0; k < 2; k++) {
new_asteroids.push({
"x": a["x"], "y": a["y"], "sides": a["sides"] - 1,
"speed": 0.5+(8-a["sides"])*Math.random()*0.5, "rotation": Math.random()*Math.PI*2,
"direction": Math.random()*Math.PI*2})
}
}
}
}
if (lives > 0) {
for (var j = 0; j < ship_points.length; j++) {
var s = ship_points[j]
if (Math.pow(s[0]-a["x"], 2) + Math.pow(s[1]-a["y"], 2) < Math.pow(radius(a["sides"]), 2)) {
lives--
for (var k = 0; k < 20; k++) {
explosions.push({"x": turtle["x"], "y": turtle["y"], "rotation": Math.random()*Math.PI*2, "speed": 0.5 + 2 * Math.random(), "age": 50})
}
if (lives == 0) {
gameover()
} else {
var x = turtle["x"]
var y = turtle["y"]
var attempts = 0
while (too_close_any(x, y) && attempts < 50) {
x = 20 + Math.random() * (WIDTH - 40)
y = 20 + Math.random() * (HEIGHT - 40)
attempts++
}
turtle["x"] = x
turtle["y"] = y
turtle["speed"] = 0
turtle["rotation"] = Math.random() * 2 * Math.PI
}
}
}
}
if (!hit) {
new_asteroids.push(a)
}
}
asteroids = new_asteroids
if (asteroids.length == 0) {
asterN++
lives++
asteroids = make_new_asteroids(asterN)
}
var new_fires = []
for (var j = 0; j < fires.length; j++) {
if (!fired[j]) {
new_fires.push(fires[j])
}
}
fires = new_fires
}
function make_new_asteroids(n) {
var out = []
for (var i = 0; i < n; i++) {
new_a = {"x": turtle["x"], "y": turtle["y"], "sides": 6,
"speed": 0.5+Math.random()*0.5, "rotation": Math.random()*Math.PI*2,
"direction": Math.random()*Math.PI*2}
var attempts = 0
while (too_close(new_a, turtle) && attempts < 50) {
new_a["x"] = Math.random() * WIDTH
new_a["y"] = Math.random() * HEIGHT
attempts++
}
out[i] = new_a
}
return out
}
function move_asteroids() {
for (var i = 0; i < asteroids.length; i++) {
var new_pos = addsteroid(asteroids[i]["x"], asteroids[i]["y"], asteroids[i]["direction"], asteroids[i]["speed"])
asteroids[i]["x"] = new_pos["x"]
asteroids[i]["y"] = new_pos["y"]
asteroids[i]["direction"] = new_pos["direction"]
}
}
function move_fires() {
var new_fires = []
for (var i = 0; i < fires.length; i++) {
fires[i]["age"]--
fires[i]["x"] += 2*Math.cos(fires[i]["rotation"])
fires[i]["y"] += 2*Math.sin(fires[i]["rotation"])
if (fires[i]["age"] > 0) {
new_fires.push(fires[i])
}
}
fires = new_fires
}
function move_explosions() {
var new_explosions = []
for (var i = 0; i < explosions.length; i++) {
explosions[i]["age"]--
explosions[i]["x"] += explosions[i]["speed"]*Math.cos(explosions[i]["rotation"])
explosions[i]["y"] += explosions[i]["speed"]*Math.sin(explosions[i]["rotation"])
if (explosions[i]["age"] > 0) {
new_explosions.push(explosions[i])
}
}
explosions = new_explosions
}
function addsteroid(x, y, d, s) {
while (x < 0) {
x += WIDTH
}
while (x > WIDTH) {
x -= WIDTH
}
while (y < 0) {
y += HEIGHT
}
while (y > HEIGHT) {
y -= HEIGHT
}
var new_x = x + s * Math.cos(d)
var new_y = y + s * Math.sin(d)
var new_d = d
var candidate = [2, {}]
for (var i = 0; i < drawnlines.length; i++) {
var line = drawnlines[i]
var b = ((line[0] - x) * (line[3] - line[1]) + (y - line[1]) * (line[2] - line[0])) / ((new_x - x) * (line[3] - line[1]) - (new_y - y) * (line[2] - line[0]))
var a = -1
if (Math.abs(line[3] - line[1]) > Math.abs(line[2] - line[0])) {
a = (y + b * (new_y - y) - line[1]) / (line[3] - line[1])
} else {
a = (x + b * (new_x - x) - line[0]) / (line[2] - line[0])
}
if (a > 0 && a < 1 && b > 0 && b < 1) {
if (b < candidate[0]) {
candidate = [b, {"x": x + b * (new_x - x), "y": y + b * (new_y - y),
"direction": Math.PI + 2 * Math.atan2(line[0] - line[2], line[3] - line[1]) - d}]
}
}
}
if (candidate[0] < 1) {
var more = s * (1 - candidate[0])
var eps = more / 100
return addsteroid(candidate[1]["x"] + eps*Math.cos(candidate[1]["direction"]), candidate[1]["y"] + eps*Math.sin(candidate[1]["direction"]), candidate[1]["direction"], more - eps)
}
if (new_x > WIDTH) {
var edge_y = y + (WIDTH - x) * (new_y - y) / (new_x - x)
if (edge_y >= 0 && edge_y <= HEIGHT) {
var more = Math.sqrt(Math.pow(new_x-WIDTH, 2) + Math.pow(new_y-edge_y, 2))
var eps = more / 100
return addsteroid(eps*Math.cos(new_d), edge_y + eps*Math.sin(new_d), new_d, more - eps)
}
}
if (new_x < 0) {
var edge_y = y + - x * (new_y - y) / (new_x - x)
if (edge_y >= 0 && edge_y <= HEIGHT) {
var more = Math.sqrt(Math.pow(new_x, 2) + Math.pow(new_y-edge_y, 2))
var eps = more / 100
return addsteroid(WIDTH + eps*Math.cos(new_d), edge_y + eps*Math.sin(new_d), new_d, more - eps)
}
}
if (new_y > HEIGHT) {
var edge_x = x + (HEIGHT - y) * (new_x - x) / (new_y - y)
if (edge_x >= 0 && edge_x <= WIDTH) {
var more = Math.sqrt(Math.pow(new_x-edge_x, 2) + Math.pow(new_y-HEIGHT, 2))
var eps = more / 100
return addsteroid(edge_x + eps*Math.cos(new_d), eps*Math.sin(new_d), new_d, more - eps)
}
}
if (new_y < 0) {
var edge_x = x - y * (new_x - x) / (new_y - y)
if (edge_x >= 0 && edge_x <= WIDTH) {
var more = Math.sqrt(Math.pow(new_x-edge_x, 2) + Math.pow(new_y, 2))
var eps = more / 100
return addsteroid(edge_x + eps*Math.cos(new_d), HEIGHT + eps*Math.sin(new_d), new_d, more - eps)
}
}
return {"x": new_x, "y": new_y, "direction": new_d}
}
function too_close(a, b) {
var dx = Math.min(Math.abs(a["x"] - b["x"]), Math.abs(a["x"] - b["x"] + WIDTH), Math.abs(a["x"] - b["x"] - WIDTH))
var dy = Math.min(Math.abs(a["y"] - b["y"]), Math.abs(a["y"] - b["y"] + HEIGHT), Math.abs(a["y"] - b["y"] - HEIGHT))
return dx * dx + dy * dy < 40000
}
function too_close_any(x, y) {
for (var i = 0; i < asteroids.length; i++) {
if (too_close(asteroids[i], {"x": x, "y": y})) {
return true
}
}
return false
}
function is_command(c) {
for (var i = 0; i < commands.length; i++) {
for (var j = 0; j < commands[i][0]; j++) {
if (c == commands[i][0][j]) {
return true
}
}
}
return false
}
function show_logohelp() {
var info = "Hide command help
"
info += "Supported commands:"
info += "