diff --git a/display.nim b/display.nim new file mode 100644 index 0000000..0f8c7fa --- /dev/null +++ b/display.nim @@ -0,0 +1,217 @@ +import input_type +import map_type +import turtle +import turtle_type + +import math +import sequtils +import strutils +import strformat +import terminal +import algorithm + +const blueCutoff = 0.05 +const cyanCutoff = 0.1 +const greenCutoff = 0.205 +const yellowCutoff = 0.335 +const redCutoff = 0.505 +const magentaCutoff = 1.0 + +proc setHotnessBackground(hotness: float, maxHotness: float) = + if hotness == 0: + stdout.setBackgroundColor(bgDefault) + else: + let hotnessFrac = hotness / maxHotness + if hotnessFrac < blueCutoff: + stdout.setBackgroundColor(bgBlue) + elif hotnessFrac < cyanCutoff: + stdout.setBackgroundColor(bgCyan) + elif hotnessFrac < greenCutoff: + stdout.setBackgroundColor(bgGreen) + elif hotnessFrac < yellowCutoff: + stdout.setBackgroundColor(bgYellow) + elif hotnessFrac < redCutoff: + stdout.setBackgroundColor(bgRed) + else: + stdout.setBackgroundColor(bgMagenta) + +proc printHotmapLegend(maxHotness: float, x,y: int) = + template line(num: int) = + setCursorPos(x, y + num - 1) + template showColor(col: BackgroundColor) = + stdout.setBackgroundColor(col) + stdout.write(" ") + stdout.setBackgroundColor(bgBlack) + stdout.write(" ") + + proc writeNumber(num: float) = + stdout.write(fmt"{num.int()}") + + line(1) + showColor(bgDefault) + writeNumber(0.0) + + line(2) + showColor(bgBlue) + writeNumber(blueCutoff * maxHotness) + + line(3) + showColor(bgCyan) + writeNumber(cyanCutoff * maxHotness) + + line(4) + showColor(bgGreen) + writeNumber(greenCutoff * maxHotness) + + line(5) + showColor(bgYellow) + writeNumber(yellowCutoff * maxHotness) + + line(6) + showColor(bgRed) + writeNumber(redCutoff * maxHotness) + + line(7) + showColor(bgMagenta) + writeNumber(magentaCutoff * maxHotness) + + + +proc displayGeneration*(generationNumber: int, timeSeconds: float, seed: int, input: Input, map: Map, turtles: var seq[Turtle]) = + + # SCREEN INIT + stdout.setBackgroundColor(bgBlack) + stdout.setForegroundColor(fgWhite) + eraseScreen() + setCursorPos(0, 0) + + # STATISTICS + let scoreCount = turtles.len() + var scoreSum = 0 + var stepsSum = 0 + for turtle in turtles: + scoreSum += turtle.score + stepsSum += turtle.steps + + let scoreAvg = scoreSum.float() / scoreCount.float() + let stepsAvg = stepsSum.float() / scoreCount.float() + + let variance = turtles.mapIt((it.score.float() - scoreAvg)^2).sum() / scoreCount.float() + let stddev = variance.sqrt() + + # PRINT BASIC INFO + echo &"Generation number: {generationNumber}" + echo &"Computation time since last stop: {timeSeconds} s" + echo &"Seed: {seed}" + echo &"Population: {turtles.len()} turtles" + echo &"Mean score: {scoreAvg} (in {stepsAvg} steps)" + echo &"Standard deviation: {stddev}" + echo &"Top score: {turtles[0].score} (in {turtles[0].steps} steps)" + + # PERCENTILS + turtles.sort(compareTurtles) + var percentils: seq[int] = @[] + for i in 1..9: + let index = (input.population div 10) * i + percentils.add(turtles[index].score) + let percentilsString = percentils.join(", ") + echo &"Score of every 10%th percentil: {percentilsString}" + + # CHOOSE NOTABLE + var top10Paths: seq[Turtle] = @[] + var notables: seq[int] = @[] + var i = 0 + while true: + if i > turtles.high(): + break + + if top10Paths.len() == 0 or top10Paths[top10Paths.high()].score != turtles[i].score: + top10Paths.add(turtles[i]) + notables.add(i) + + if top10Paths.len() >= 10: + break + + inc i + + let notableString = notables.join(", ") + echo &"Notable turtles: {notableString}" + + # HOTNESS MAP + var hotness: seq[seq[int]] = @[] + var top10Hot: seq[seq[int]] = @[] + var topHot: seq[seq[int]] = @[] + for i in 0..input.size-1: + hotness.add(@[]) + top10Hot.add(@[]) + topHot.add(@[]) + for j in 0..input.size-1: + hotness[i].add(0) + top10Hot[i].add(0) + topHot[i].add(0) + + template populateHotness(turtles: seq[Turtle], hotSeq: var seq[seq[int]], maxHot: var int) = + for turtle in turtles: + let path = turtle.path + for i in 0..path.xs.high(): + let x = path.xs[i] + let y = path.ys[i] + hotSeq[y][x].inc() + if hotSeq[y][x] > maxHot: + maxHot = hotSeq[y][x] + + var maxHotness = 0 + populateHotness(turtles, hotness, maxHotness) + var maxTop10Hotness = 0 + populateHotness(top10Paths, top10Hot, maxTop10Hotness) + var maxTopHotness = 0 + populateHotness(@[turtles[0]], topHot, maxTopHotness) + + # DISPLAY HOTNESS MAP + echo "Hotmap of All/Notable/Best Turtles" + + const yOffset = 10 + let xOffset = input.size + 2 + + for y in 0..input.size-1: + for x in 0..input.size-1: + let hotness = hotness[y][x] + let top10hotness = top10Hot[y][x] + let topHotness = topHot[y][x] + let symbol = map[y][x].tileToString() + + # hotmap + setHotnessBackground(hotness.float(), maxHotness.float()) + setCursorPos(x, y+yOffset) + stdout.write(symbol) + + # top10 map + setHotnessBackground(top10hotness.float(), maxTop10Hotness.float()) + setCursorPos(x+xOffset, y+yOffset) + stdout.write(symbol) + + # top map + setHotnessBackground(topHotness.float(), maxTopHotness.float()) + setCursorPos(x+2*xOffset, y+yOffset) + stdout.write(symbol) + + + stdout.setBackgroundColor(bgBlack) + stdout.write("\n") + + echo "" + + # display legend + let legendYOffset = yOffset + 1 + input.size + printHotmapLegend(maxHotness.float(), 0, legendYOffset) + printHotmapLegend(maxTop10Hotness.float(), xOffset, legendYOffset) + printHotmapLegend(maxTopHotness.float(), 2*xOffset, legendYOffset) + echo "" + echo "" + # FINISH + echo "Press enter to continue." + discard stdin.readLine() + stdout.setBackgroundColor(bgDefault) + eraseScreen() + setCursorPos(1,1) + echo "" diff --git a/input.json b/input.json new file mode 100644 index 0000000..44a834c --- /dev/null +++ b/input.json @@ -0,0 +1,8 @@ +{ + "size": 19, + "treasure_count": 7, + "lifetime": 500, + "population": 500, + "instruction_set": "anybit", + "bits": 12 +} \ No newline at end of file diff --git a/input_type.nim b/input_type.nim new file mode 100644 index 0000000..a6594f4 --- /dev/null +++ b/input_type.nim @@ -0,0 +1,11 @@ + +type + Input* = object + size*: int + treasure_count*: int + lifetime*: int + population*: int + instruction_set*: string + bits*: int + + diff --git a/instruction_sets/any_bit.nim b/instruction_sets/any_bit.nim new file mode 100644 index 0000000..0f29d99 --- /dev/null +++ b/instruction_sets/any_bit.nim @@ -0,0 +1,105 @@ +import ../input_type +import ../turtle_type +import ../map_type +import bitops +import random + +const opcodeLength = 3 +# 3 if there are 8 different instructions + +# anybit +proc genAnybitTurtle*(input: Input, bits: int): Turtle = + # generates a turtle of bits long memory cells + let maxAddress = (1 shl (bits - opcodeLength)) - 1 + let maxValue = (1 shl bits) - 1 + + new(result) + for i in 0..maxAddress: + result.memory.add(rand(0..maxValue).uint32) + +proc executeAnybit*(instructionIndex: uint32, turtle: Turtle, map: Map, input: Input, bits: int): int = + let maxAddress = ((1 shl (bits - opcodeLength)) - 1).uint32 + let maxValue = ((1 shl bits) - 1).uint32 + + var newInstructionIndex: uint32 + + let + icIncrement = 0'u64 + icDecrement = (0b001 shl (bits-opcodeLength)).uint32 + icJump = (0b010 shl (bits-opcodeLength)).uint32 + icMove = (0b011 shl (bits-opcodeLength)).uint32 + icDoubleMove = (0b100 shl (bits-opcodeLength)).uint32 + icCond = (0b101 shl (bits-opcodeLength)).uint32 + icNand = (0b110 shl (bits-opcodeLength)).uint32 + icXor = (0b111 shl (bits-opcodeLength)).uint32 + + let instruction = turtle.memory[instructionIndex] + let instructionCode = instruction.bitand(icXor) + let instructionArg = instruction.bitand(bitnot(icXor)) + + template addTo(target: var uint32, delta: uint32, subtract: bool = false) = + if subtract: + target = (target - delta).bitand(maxValue) + else: + target = (target + delta).bitand(maxValue) + + template nextBlock: uint32 = + turtle.memory[(instructionIndex + 1'u32).bitand(maxAddress)] + + template mapBounded(i: int): int = + max(min(i, input.size), 0) + + if instructionCode == icIncrement: + turtle.memory[instructionArg].addTo(1) + newInstructionIndex = instructionIndex + 1'u32 + elif instructionCode == icDecrement: + turtle.memory[instructionArg].addTo(1, subtract = true) + newInstructionIndex = instructionIndex + 1'u32 + elif instructionCode == icJump: + newInstructionIndex = instructionArg + elif instructionCode == icMove: + let direction = instructionArg.bitand(3'u32) + if direction == 0'u32: + turtle.y += 1 + elif direction == 1'u32: + turtle.y -= 1 + elif direction == 2'u32: + turtle.x += 1 + elif direction == 3'u32: + turtle.x -= 1 + if turtle.x < 0 or turtle.y < 0 or turtle.x >= input.size or turtle.y >= input.size: + return -1 + newInstructionIndex = instructionIndex + 1 + elif instructionCode == icDoubleMove: + let amount = (instructionArg.bitand(28'u32) shr 2).int() + 1 + let direction = instructionArg.bitand(3'u32) + if direction == 0'u32: + turtle.y += amount + elif direction == 1'u32: + turtle.y -= amount + elif direction == 2'u32: + turtle.x += amount + elif direction == 3'u32: + turtle.x -= amount + if turtle.x < 0 or turtle.y < 0 or turtle.x >= input.size or turtle.y >= input.size: + return -1 + newInstructionIndex = instructionIndex + 1 + + + elif instructionCode == icCond: + # jumps to address if the next block is even + if nextBlock().bitand(1'u32) == 0'u32: + newInstructionIndex = instructionArg + else: + newInstructionIndex = instructionIndex + 2 + elif instructionCode == icNand: + # bitwise nand of the address with the next code block + turtle.memory[instructionArg] = bitnot(turtle.memory[instructionArg].bitand(nextBlock())).bitand(maxValue) + newInstructionIndex = instructionIndex + 2 + elif instructionCode == icXor: + turtle.memory[instructionArg] = turtle.memory[instructionArg].bitxor(nextBlock()).bitand(maxValue) + newInstructionIndex = instructionIndex + 2 + else: + raise newException(Defect, "Bad instruction code.") + + return newInstructionIndex.bitand(maxAddress).int() diff --git a/main b/main new file mode 100755 index 0000000..68be78c Binary files /dev/null and b/main differ diff --git a/main.nim b/main.nim new file mode 100644 index 0000000..6f18822 --- /dev/null +++ b/main.nim @@ -0,0 +1,67 @@ +import strutils +import json +import random +import times +import os + +import input_type +import map_type +import turtle +import turtle_type +import score +import select +import mutate +import display + +let now = getTime() +var seed = now.toUnix * 1_000_000_000 + now.nanosecond +if paramCount() > 0: + seed = parseInt(paramStr(1)) + +randomize(seed) + +# first, get the inputs +let input = readFile("input.json").parseJson().to(Input) + +if input.bits < 4 or input.bits > 32: + raise newException(Defect, "bits in input json must be between 4 and 32 (inclusive).") + +# get a map +let map = input.generateMap() + +# get population turtles +var turtles: seq[Turtle] +for i in 0..input.population-1: + turtles.add(input.genTurtle()) + +var lastTime = cpuTime() + +proc generation(generationNumber: int) = + # does a whole generation + + # gets the path of the turtles + for turtle in turtles: + turtle.execute(map, input) + score(turtle, map, input) + + # display generation + + if generationNumber mod 500 == 0: + let newTime = cpuTime() + let timeSeconds = newTime - lastTime + lastTime = newTime + displayGeneration(generationNumber, timeSeconds, seed.int(), input, map, turtles) + + turtles.select(input) + + # add mutated turtles from previous generation + let prevGenCount = turtles.high() + while turtles.len() < input.population: + let basis = turtles[rand(prevGenCount)] + turtles.add(mutate(basis, input)) + + +var i = 0 +while true: + generation(i) + inc i \ No newline at end of file diff --git a/map_type.nim b/map_type.nim new file mode 100644 index 0000000..05c4c86 --- /dev/null +++ b/map_type.nim @@ -0,0 +1,54 @@ +import strutils +import sequtils +import random + +import input_type + +type + Tile* = enum + tileEmpty, tileTreasure + Map* = seq[seq[Tile]] + +proc generateMap*(input: Input): Map = + let treasureCount = input.treasure_count + + # create empty map + for i in 0..input.size: + result.add(@[]) + for j in 0..input.size: + result[i].add(tileEmpty) + + # populate treasures + var addedTreasures = 0 + while addedTreasures < treasureCount: + let randX = rand(input.size - 1) + let randY = rand(input.size - 1) + if result[randX][randY] == tileEmpty: + result[randX][randY] = tileTreasure + inc addedTreasures + +func tileToString*(tile: Tile): string = + case tile: + of tileEmpty: + " " + of tileTreasure: + "T" + +func lineToString*(line: seq[Tile]): string = + line.map(tileToString).join() + +proc mapToString*(map: Map): string = + map.map(lineToString).join("\n") + +proc printMap*(map: Map) = + map.mapToString().echo() + +func getTreasures*(map: Map): seq[(int, int)] = + var treasures: seq[(int, int)] + + for y in 0..map.high(): + for x in 0..map[0].high(): + if map[y][x] == tileTreasure: + treasures.add((x, y)) + + return treasures diff --git a/mutate.nim b/mutate.nim new file mode 100644 index 0000000..2af4c9f --- /dev/null +++ b/mutate.nim @@ -0,0 +1,14 @@ +import turtle +import turtle_type +import input_type +import random + +proc mutate*(turtle: Turtle, input: Input): Turtle = + # returns a mutated turtle + let newTurtle = genTurtle(input) + + for i in 0..newTurtle.memory.high(): + if rand(100) > 2: + newTurtle.memory[i] = turtle.memory[i] + + return newTurtle \ No newline at end of file diff --git a/score.nim b/score.nim new file mode 100644 index 0000000..2bdf019 --- /dev/null +++ b/score.nim @@ -0,0 +1,38 @@ +import map_type +import input_type +import turtle_type +import math + +proc score*(turtle: Turtle, map: Map, input: Input) = + let path = turtle.path + # create a list of treasures + let treasures = map.getTreasures() + + # create a list of distances from each treasure + var distances: seq[float] = @[] + + for i in 0..treasures.high(): + distances.add(-1.0) + + # iterate through the path, and change the distance if it decreases + for i in 0..path.ys.high(): + let x = path.xs[i] + let y = path.ys[i] + for j in 0..treasures.high(): + let (tx, ty) = treasures[j] + let deltaX = abs(tx - x) + let deltaY = abs(ty - y) + # taxicab distance + let distance = (deltaX + deltaY).float() + if distances[j] < 0 or distance < distances[j]: + distances[j] = distance + + # every distance contributes to score, exponentially dropping off + # by the formula Ae^(-k * dist) + var treasureScore = 0.0 + for distance in distances: + const A = 100.0 + const k = 0.2 + treasureScore += A * exp(- k * distance) + + turtle.score = treasureScore.int() - (turtle.steps div 5) \ No newline at end of file diff --git a/select.nim b/select.nim new file mode 100644 index 0000000..b7979bf --- /dev/null +++ b/select.nim @@ -0,0 +1,36 @@ +import random +import math +import algorithm + +import turtle +import turtle_type +import input_type + +proc select*(turtles: var seq[Turtle], input: Input) = + # sort turtles according to score + turtles.sort(compareTurtles) + + var scoreSum = 0 + for turtle in turtles: + scoreSum += turtle.score + let avgScore = scoreSum div turtles.len() + + var survivors: seq[Turtle] = @[] + + for i in 0..turtles.high(): + let turtle = turtles[i] + const smoothness = 0.08 + const chanceRandomLive = 0.02 + const chanceRandomDie = 0.0 + let chanceExp = exp(smoothness * float(turtles[i].score - avgScore)) + let chance = chanceRandomLive + (1-chanceRandomDie-chanceRandomLive) * chanceExp / (chanceExp + 1) + if rand(1.0) < chance: + survivors.add(turtle) + + var i = 0 + while i < turtles.len(): + if turtles[i] notin survivors: + turtles.del(i) + else: + inc i + diff --git a/turtle.nim b/turtle.nim new file mode 100644 index 0000000..0adcc33 --- /dev/null +++ b/turtle.nim @@ -0,0 +1,75 @@ +import input_type +import map_type +import sequtils + +import turtle_type +#import instruction_sets/eight_bit +#import instruction_sets/ten_bit +import instruction_sets/any_bit + +proc compareTurtles*(left, right: Turtle): int = + -cmp(left.score, right.score) + +# general: + +proc genTurtle*(input: Input): Turtle = + if input.instruction_set == "anybit": + return genAnybitTurtle(input, input.bits) + else: + raise newException(Defect, "Unknown instruction set.") + +proc executeInstruction(instructionIndex: int, turtle: Turtle, map: Map, input: Input): int = + if input.instruction_set == "anybit": + return executeAnybit(instructionIndex.uint32, turtle, map, input, input.bits) + else: + raise newException(Defect, "Unknown instruction set.") + + +proc execute*(turtle: Turtle, map: Map, input: Input) = + + let treasures = map.getTreasures() + + # back to start + turtle.y = input.size div 2 + turtle.x = input.size div 2 + turtle.path.xs = @[] + turtle.path.ys = @[] + turtle.score = 0 + turtle.found = repeat(false, treasures.len()) + # save turtle original code + let turtleOriginalCode = turtle.memory + + template tpath: Path = + turtle.path + + proc markLocation: bool = + tpath.xs.add(turtle.x) + tpath.ys.add(turtle.y) + if map[turtle.y][turtle.x] == tileTreasure: + for i in 0..treasures.high(): + let treasure = treasures[i] + if treasure[0] == turtle.x and treasure[1] == turtle.y: + turtle.found[i] = true + if turtle.found.allIt(it): + return true + return false + + + discard markLocation() + + var instructionNumber = 0 + var instructionIndex = 0 + while instructionNumber < input.lifetime: + instructionIndex = executeInstruction(instructionIndex, turtle, map, input) + if instructionIndex == -1: + # out of bounds of map + break + + if turtle.y != tpath.ys[tpath.ys.high()] or turtle.x != tpath.xs[tpath.xs.high()]: + if markLocation(): + break + inc instructionNumber + + turtle.steps = instructionNumber + # restore original code + turtle.memory = turtleOriginalCode diff --git a/turtle_type.nim b/turtle_type.nim new file mode 100644 index 0000000..b7d9629 --- /dev/null +++ b/turtle_type.nim @@ -0,0 +1,13 @@ +type + Turtle* = ref object + memory*: seq[uint32] + x*: int + y*: int + score*: int + path*: Path + steps*: int + found*: seq[bool] + + Path* = object + xs*: seq[int] + ys*: seq[int]