langtonsant/Main.py

558 lines
16 KiB
Python
Executable File

#!/usr/bin/env python3
import pygame
from OpenGL.GL import *
from pygame.locals import *
import argparse
import sys
import random
import time
#TODO: remove global vars
pygame.init()
window_w = 1600
window_h = 900
livingSpaceWidth = 64
livingSpaceHeight = 36
creatureW = window_w/(livingSpaceWidth)
creatureH = window_h/(livingSpaceHeight)
FPS = 30
NORTH = 0
EAST = 1
SOUTH = 2
WEST = 3
livingSpace = []
livingSpaceColor = []
update_queue = []
old_update_queue = []
# doubled draw buffer because of display double buffering
draw_buffer = []
draw_buffer_old = []
antPosition = (livingSpaceWidth // 2, livingSpaceHeight // 2)
antRotation = NORTH
num_colors = 2
color_list = []
code = [False,True]
# store number of active cells
num_active_cells = 0
# helper function for colors:
def HSVtoRGB(h,s,v):
c = v*s
x = c*(1-abs((h/60)% 2 -1))
m = v-c
rr=0
gg=0
bb=0
if(h<60):
rr=c
gg=x
bb=0
elif (h < 120):
rr = x
gg = c
bb = 0
elif (h < 180):
rr = 0
gg = c
bb = x
elif (h < 240):
rr=0
gg=x
bb=c
elif (h < 300):
rr = x
gg = 0
bb = c
elif (h < 360):
rr = c
gg = 0
bb = x
return(rr+m,gg+m,bb+m)
def generate_colors():
global color_list
global num_colors
color_list = []
for i in range(num_colors):
color_list.append(HSVtoRGB(i * 360.0 / num_colors,1,1))
def resize(shape):
width, height = shape
if height == 0:
height = 1
glViewport(0, 0, width, height)
glMatrixMode(GL_PROJECTION)
glLoadIdentity()
glOrtho(0.0, livingSpaceWidth * creatureW, livingSpaceHeight * creatureH, 0.0, -6.0, 0.0)
glMatrixMode(GL_MODELVIEW)
glLoadIdentity()
def init():
glClearColor(0.0, 0.0, 0.0, 0.0)
def initLivingSpace():
for x in range(livingSpaceWidth):
livingSpace.append([])
livingSpaceColor.append([])
for y in range(livingSpaceHeight):
livingSpace[x].append(0)
rgba = [float(x)/float(livingSpaceWidth), float(livingSpaceWidth-x)/float(livingSpaceWidth), float(y)/float(livingSpaceHeight),0.15]
livingSpaceColor[x].append(rgba)
draw_buffer.append((x,y))
def isAlive(x,y):
return livingSpace[x][y] != 0
def draw():
global draw_buffer
global draw_buffer_old
#glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)
glLoadIdentity()
glTranslatef(0.0,0.0,3.0)
glBegin(GL_QUADS)
for column,row in draw_buffer + draw_buffer_old:
r,g,b,a = livingSpaceColor[column][row]
#glColor4f(255, 0, 0, 1.0)
glColor4f(a * r, a * g, a * b, 1.0)
x = column * creatureW
y = row * creatureH
glVertex3f(x,y,0.0)
glVertex3f(x + creatureW-1.0,y,0.0)
glVertex3f(x+creatureW-1,y+creatureH-1,0.0)
glVertex3f(x,y+creatureH-1,0.0)
# draw langton's ant quad:
x = antPosition[0] * creatureW
y = antPosition[1] * creatureH
offX = creatureW / 2
offY = creatureH / 2
glColor4f(1,1,1,0.5)
glVertex3f(x + offX, y, 0.0) if antRotation != SOUTH else glVertex3f(x + offX, y + creatureH/2, 0.0)
glVertex3f(x + creatureW - 1, y + offY, 0.0) if antRotation != WEST else glVertex3f(x + creatureW/2, y + offY, 0.0)
glVertex3f(x + offX, y + creatureH - 1, 0.0) if antRotation != NORTH else glVertex3f(x + offX, y + creatureH/2, 0.0)
glVertex3f(x, y + offY, 0.0) if antRotation != EAST else glVertex3f(x + creatureW/2, y + offY, 0.0)
glEnd()
draw_buffer_old = draw_buffer
draw_buffer = []
def activate(i,j,key = 1):
global num_active_cells
if livingSpace[i][j] == 0:
num_active_cells += 1
livingSpace[i][j] = key
if num_colors > 2:
livingSpaceColor[i][j] = [
color_list[key - 1][0],
color_list[key - 1][1],
color_list[key - 1][2],
1.0
]
else:
livingSpaceColor[i][j][3] = 1.0
update_queue.append((i,j))
def deactivate(i,j):
global num_active_cells
if livingSpace[i][j] != 0:
num_active_cells -= 1
livingSpace[i][j] = 0
# correct color:
livingSpaceColor[i][j] = [
float(i) / float(livingSpaceWidth),
float(livingSpaceWidth - i) / float(livingSpaceWidth),
float(j) / float(livingSpaceHeight),
0.6
]
update_queue.append((i,j))
def update_field():
global old_update_queue
global update_queue
old_update_queue = update_queue
update_queue = []
for i,j in old_update_queue:
draw_buffer.append((i,j))
if livingSpace[i][j] <= 0 and livingSpaceColor[i][j][3] > 0.15:
livingSpaceColor[i][j][3] *= 0.98
if livingSpaceColor[i][j][3] < 0.15:
livingSpaceColor[i][j][3] = 0.15
update_queue.append((i,j))
elif livingSpace[i][j] > 0 and livingSpaceColor[i][j][3] > 0.6:
livingSpaceColor[i][j][3] *= 0.98
if livingSpaceColor[i][j][3] < 0.6:
livingSpaceColor[i][j][3] = 0.6
update_queue.append((i,j))
def move_ant(dx, dy):
global antPosition
global antRotation
# finally move:
antPosition = (antPosition[0] + dx, antPosition[1] + dy)
# wrap around:
if antPosition[0] < 0:
antPosition = (livingSpaceWidth - 1, antPosition[1])
elif antPosition[0] >= livingSpaceWidth:
antPosition = (0, antPosition[1])
if antPosition[1] < 0:
antPosition = (antPosition[0], livingSpaceHeight - 1)
elif antPosition[1] >= livingSpaceHeight:
antPosition = (antPosition[0], 0)
update_queue.append(antPosition)
def update_ant():
global antPosition
global antRotation
# if we have only two colors, we will switch between on and off
if num_colors == 2:
# switch cell
if isAlive(antPosition[0], antPosition[1]):
deactivate(antPosition[0], antPosition[1])
else:
activate(antPosition[0], antPosition[1])
# turn
code_index = 1 if isAlive(antPosition[0], antPosition[1]) else 0
if code[code_index]:
# turn right
antRotation = (antRotation + 3) % 4
else:
# turn left
antRotation = (antRotation + 1) % 4
else:
# if we have more than 3 colors, we use the color code
old_key = livingSpace[antPosition[0]][antPosition[1]] - 1
new_key = (livingSpace[antPosition[0]][antPosition[1]]) % num_colors + 1 # avoiding key zero, it is deactivated
if not isAlive(antPosition[0], antPosition[1]):
# first activation time, increase key:
new_key += 1
old_key += 1
#determine direction:
if code[old_key]:
# turn left:
antRotation = (antRotation + 3) % 4
else:
# turn right:
antRotation = (antRotation + 1) % 4
# then activate with new key
activate(antPosition[0], antPosition[1], new_key)
# move on step:
dx = 0
dy = 0
if antRotation == NORTH:
dy = -1
elif antRotation == SOUTH:
dy = +1
elif antRotation == EAST:
dx = +1
elif antRotation == WEST:
dx = -1
move_ant(dx,dy)
def main():
global livingSpaceWidth
global livingSpaceHeight
global creatureW
global creatureH
global antPosition
global antRotation
global window_w
global window_h
global color_list
global num_colors
global code
# parsing args:
parser = argparse.ArgumentParser(description="langton\'s ant simulation tool", formatter_class=argparse.RawTextHelpFormatter)
parser.add_argument('--steps', dest='steps', default = 20 , help='steps per second. Default = 20')
parser.add_argument('--w', dest='w', default = livingSpaceWidth, help = 'field width')
parser.add_argument('--h', dest='h', default=livingSpaceHeight, help = 'field height')
parser.add_argument('--calc', dest='calc', default=0, help='calculate steps and only display result')
parser.add_argument('--fullscreen', dest='fullscreen', action='store_true')
parser.add_argument('--window_w', dest='win_w', default=window_w, help='window width')
parser.add_argument('--window_h', dest='win_h', default=window_h, help='window height')
parser.add_argument('--configurator', dest='configurator', action='store_true', help='start in field edit mode')
parser.add_argument('--code', dest='code', default='10', help='binary code for the ant (\'10\' corresponds to the starndard ant behaviour). 1 means \'turn left\', 0 means \'turn right\'')
parser.add_argument('--file', dest='file', default='', help='writing number of living cells per step in this file')
parser.add_argument('--pattern', dest='pattern', default='0',
help='initial pattern for the field. Possible values:\n' +
'\t * 0: all fields inactive\n' +
'\t * 1: all fields active\n' +
'\t * check: checkboard pattern\n' +
'\t * horizontal: horizontal stripes\n' +
'\t * vertical: vertical stripes\n' +
'\t * random: random values\n')
parser.set_defaults(fullscreen=False)
parser.set_defaults(configurator=False)
if (len(sys.argv) == 1):
# no parameters given. Print help and ask user at runtime for options:
settings = {}
settings["steps"] = 20
settings["w"] = livingSpaceWidth
settings["h"] = livingSpaceHeight
settings["calc"] = 0
settings["fullscreen"] = False
settings["window_w"] = window_w
settings["window_h"] = window_h
settings["configurator"] = False
settings["code"] = '10'
settings["file"] = ""
settings["pattern"] = '0'
while True:
parser.print_help()
print("current settings:")
for key in settings.keys():
print(str(key) + " = " + str(settings[key]))
a = input("enter parameter to change. press enter to continue:")
if len(a) == 0:
break
val = input("enter new value:")
settings[a] = val
# passing settings to parser:
parser.set_defaults(steps=settings["steps"])
parser.set_defaults(w=settings["w"])
parser.set_defaults(h=settings["h"])
parser.set_defaults(calc=settings["calc"])
parser.set_defaults(fullscreen=settings["fullscreen"])
parser.set_defaults(window_w=settings["window_w"])
parser.set_defaults(window_h=settings["window_h"])
parser.set_defaults(configurator=settings["configurator"])
parser.set_defaults(code=settings["code"])
if len(settings["file"]) > 0:
parser.set_defaults(file=settings["file"])
parser.set_defaults(pattern=settings["pattern"])
args = parser.parse_args()
steps_per_sec = int(args.steps)
livingSpaceWidth = int(args.w)
livingSpaceHeight = int(args.h)
calc = int(args.calc)
filename = args.file
fileobj = None
if len(filename) > 0:
fileobj = open(filename, mode='w')
video_flags = OPENGL | HWSURFACE | DOUBLEBUF
# parse code:
code = []
for c in args.code:
if c == '0':
code.append(False)
else:
code.append(True)
# generate colors:
num_colors = len(code)
generate_colors()
if args.fullscreen:
video_flags = OPENGL | HWSURFACE | DOUBLEBUF | FULLSCREEN
dinfo = pygame.display.Info()
window_w = dinfo.current_w
window_h = dinfo.current_h
else:
window_w = int(args.win_w)
window_h = int(args.win_h)
creatureW = window_w / (livingSpaceWidth)
creatureH = window_h / (livingSpaceHeight)
antPosition = (livingSpaceWidth // 2, livingSpaceHeight // 2)
pygame.display.set_mode((window_w,window_h),video_flags)
initLivingSpace()
resize((window_w, window_h))
init()
clock = pygame.time.Clock()
frames = 0
counter = 0
logic_frame_pause = FPS / steps_per_sec
configurator_mode = bool(args.configurator)
field_draws = 0
# apply pattern
if args.pattern == '0':
# nothing to do
pass
elif args.pattern == '1':
for i in range(livingSpaceWidth):
for j in range(livingSpaceHeight):
activate(i,j)
elif args.pattern == 'check':
for i in range(livingSpaceWidth):
for j in range(livingSpaceHeight):
if (i + j) % 2 == 0:
activate(i,j)
elif args.pattern == 'horizontal':
for i in range(livingSpaceWidth):
for j in range(livingSpaceHeight):
m = j % num_colors
if num_colors > 2:
m += 1
if m != 0:
activate(i,j,m)
elif args.pattern == 'vertical':
for i in range(livingSpaceWidth):
for j in range(livingSpaceHeight):
m = i % num_colors
if num_colors > 2:
m += 1
if m != 0:
activate(i,j,m)
elif args.pattern == 'random':
r = random.Random(time.time())
for i in range(livingSpaceWidth):
for j in range(livingSpaceHeight):
k = r.randint(0,num_colors - 1)
if num_colors > 2:
k += 1
if k != 0:
activate(i,j,k)
else:
print("error. unknown pattern")
parser.print_help()
sys.exit(-1)
if (calc > 0):
for i in range(calc):
update_ant()
#main loop:
while True:
ticktime = clock.tick(FPS)
#print ticktime
event = pygame.event.poll()
if event.type == QUIT or (event.type == KEYDOWN and event.key == K_ESCAPE):
break
update_field()
draw()
field_draws += len(draw_buffer) + len(draw_buffer_old)
frames += 1
if frames % FPS == 0:
print("average field draws per frame: " + str(field_draws/FPS))
field_draws = 0
pygame.display.flip()
if configurator_mode:
# move with keys:
if event.type == KEYDOWN:
draw_buffer.append((antPosition[0],antPosition[1]))
if event.key == K_DOWN:
move_ant(0,+1)
elif event.key == K_UP:
move_ant(0,-1)
elif event.key == K_LEFT:
move_ant(-1,0)
elif event.key == K_RIGHT:
move_ant(+1,0)
elif event.key == K_SPACE:
new_key = (livingSpace[antPosition[0]][antPosition[1]]) % num_colors + 1
if not isAlive(antPosition[0], antPosition[1]):
new_key += 1
activate(antPosition[0], antPosition[1], new_key)
elif event.key == K_BACKSPACE:
deactivate(antPosition[0],antPosition[1])
elif event.key == K_LCTRL:
antRotation = (antRotation - 1) % 4
move_ant(0,0)
elif event.key == K_RCTRL:
antRotation = (antRotation + 1) % 4
move_ant(0,0)
elif event.key == K_RETURN:
configurator_mode = False
else:
if (calc == 0):
counter += 1
if logic_frame_pause >= 1:
if counter > logic_frame_pause:
update_ant()
if fileobj != None:
fileobj.write(str(num_active_cells) + '\n')
counter = 0
else:
# multiple calculations per frame:
for i in range(int(1 / logic_frame_pause)):
update_ant()
if fileobj != None:
fileobj.write(str(num_active_cells) + '\n')
# switch to configurator mode:
if event.type == KEYDOWN and event.key == K_RETURN:
configurator_mode = True
if fileobj != None:
fileobj.close()
if __name__ == '__main__':
main()