Improving Chaos Game efficiency - python

I've written this script to use the turtle module to replicate the Chaos Game I saw on Numberphile's channel. There are a large amount of dots being drawn to actually make it work well in a larger scale. I assume the large amount of dots is what causes the program to begin running slower after a bit and I was wondering if anyone could help me come up with a workaround for this.
I'm open for any kind of solution, as long as the controls remain the same and the number of vertexes can be any number above 3.
If someone doesn't know what the Chaos Game is, it's a game where you have polygon with any amount of vertexes. At first you place a dot inside the polygon, randomly choose one of the vertexes and draw a new dot halfway in-between the dot you just placed earlier and the randomly chosen vertex. You keep repeating this process and each time you'll use the newly drawn dot.
In this script I've also included a rule to make sure it doesn't choose the same vertex two times in a row to form nice fractals with more than 3 vertexes. 3 vertexes actually forms the Sierpinski triangle.
Here's a link to Numberphile's video: https://www.youtube.com/watch?v=kbKtFN71Lfs
As you can probably tell, I'm somewhat new to Python and coding in general.
Full code:
import turtle as t
import tkinter as tk
from tkinter import ttk
from random import randint
wn = t.Screen()
wn.colormode(255)
t.pu();t.ht();t.speed(0)
plist = []
l = 0
val = 0
pb=ttk.Progressbar(orient="horizontal",length=wn.window_width(),mode="determinate")
pb.pack(side=tk.BOTTOM)
pb["value"]=0
def Clear():
t.clear()
plist = []
def Dot(x, y):
t.goto(x, y)
t.dot(5, (0, 0, 255))
plist.append(t.pos())
def Run(x, y):
wn.onscreenclick(None)
wn.tracer(0, 0)
l = len(plist)
pb["maximum"]=l*1000
xyc = randint(0, l-1)
xyc_old = 0
for _ in range(l*10):
xyc = randint(0, l-1)
for i in range(100):
xyc = randint(0, l-1);
if l >= 4:
while xyc == xyc_old:
xyc = randint(0, l-1);
xyc_old = xyc;
t.goto((t.pos()[0]+plist[xyc][0])/2, (t.pos()[1]+plist[xyc][1])/2);
t.dot(2, (255, 0, 0));
pb["value"]+=1;
pb.update()
wn.update()
plist.clear()
wn.onscreenclick(Dot, btn=1)
wn.onscreenclick(Run, btn=3)
wn.onscreenclick(Dot, btn=1)
wn.onscreenclick(Run, btn=3)
wn.onkey(Clear, "c")
wn.listen()
wn.mainloop()
I feel like my coding style is very different from a lot of the people on here, but I hope that isn't an issue.
Thank you!

I assume the large amount of dots is what causes the program to begin
running slower after a bit and I was wondering if anyone could help me
come up with a workaround for this.
Surprisingly, no. It's your own instrumentation (progress bar) slowing you down. Comment out:
pb.update()
and watch what happens.
I feel like my coding style is very different from a lot of the people
on here, but I hope that isn't an issue.
It's an issue where it overlaps with bad coding style. E.g. lack of white space, use of semicolons, effective no-ops in the code, etc. My rework of your code:
from turtle import Screen, Turtle
import tkinter as tk
from tkinter import ttk
from random import randrange
def clear():
turtle.clear()
plist.clear()
def dot(x, y):
turtle.goto(x, y)
turtle.dot(5, 'blue')
plist.append(turtle.position())
def run(x, y):
screen.onscreenclick(None, btn=1)
screen.onscreenclick(None, btn=3)
screen.onkey(None, 'c')
length = len(plist)
flag = length >= 4
pb['maximum'] = length * 1000
xyc_old = 0
for _ in range(length * 100):
for _ in range(10):
xyc = randrange(length)
if flag:
while xyc == xyc_old:
xyc = randrange(length)
xyc_old = xyc
x, y = turtle.position()
dx, dy = plist[xyc]
turtle.goto((x + dx) / 2, (y + dy) / 2)
turtle.dot(2)
pb['value'] += 10
pb.update()
plist.clear()
pb['value'] = 0
screen.onscreenclick(dot, btn=1)
screen.onscreenclick(run, btn=3)
screen.onkey(clear, 'c')
plist = []
screen = Screen()
screen.tracer(False)
turtle = Turtle()
turtle.hideturtle()
turtle.setundobuffer(None)
turtle.color('red')
turtle.penup()
pb = ttk.Progressbar(orient='horizontal', length=screen.window_width(), mode='determinate')
pb.pack(side=tk.BOTTOM)
pb['value'] = 0
screen.onscreenclick(dot, btn=1)
screen.onscreenclick(run, btn=3)
screen.onkey(clear, 'c')
screen.listen()
screen.mainloop()
Other changes include:
The plist = [] in Clear() won't work without a global plist. Use plist.clear() instead.
You also need to disable (and reenable) wn.onkey(Clear, "c") during Run otherwise your user can break the program. You also need to disable the two mouse buttons independently.
You really want randrange(), not randint().

Related

Is there any way I can avoid Turtle slowing down the more lines it has to print?

I made a program to generate a random 2D array of 1s and 0s that I then print as green "land" tiles above a blue background to generate a little 'map'. I've tried to optimise the code as much as I know how but when I try to print anything above 100x100, it slows down by a LOT. If I set it to print line by line, I see that it's taking quite a lot longer to print lines the more of the map turtle has already printed. I don't know why this is (I don't know much about turtle or rendering in general) but I would quite like to know how to solve this problem.
If this is not possible, could I have recommendations for other libraries/languages that I could try this in? I'd like to do more programs like this and I don't mind learning something new. :)
Here is my code:
import turtle
import random
import time
d = turtle.Turtle()
d.ht()
wn = turtle.Screen()
wn.setup(width=0.45,height=0.8,startx=800,starty=50)
wn.bgcolor("#002240")
turtle.tracer(0,0)
d.penup()
interval = 10
def square(col) :
d.pendown()
d.color(col)
d.begin_fill()
for i in range(4) :
d.fd(interval)
d.right(90)
d.end_fill()
d.penup()
size = int(input("Enter map size "))
custom_interval = input("Custom interval? ")
if custom_interval != '' :
interval = int(custom_interval)
int_by_size = interval * size
start_gen = time.time()
data = []
for i in range(size) :
gen_line = []
[gen_line.append(random.randint(0,1)) for i in range(size)]
data.append(gen_line)
end_gen = time.time()
print("GEN DONE IN",end_gen-start_gen)
d.goto(-int_by_size/2,int_by_size/2)
start_draw = time.time()
d.pendown()
d.color("blue")
d.begin_fill()
for i in range(4) :
d.fd(int_by_size)
d.right(90)
d.end_fill()
d.penup()
for y,line in enumerate(data) :
for x,tile in enumerate(line) :
if tile == 1 :
square("green")
d.fd(interval)
else :
d.fd(interval)
#Comment out to print entire map at once
turtle.update()
d.backward(int_by_size)
d.sety(d.ycor()-interval)
end_draw = time.time()
print("DRAW DONE IN",end_draw-start_draw)
wn.exitonclick()
My rework of your code below speeds up the drawing by an order of magnitude by switching from drawing to stamping and other tricks:
from turtle import Screen, Turtle
from random import randint
import time
CURSOR_SIZE = 20
interval = 10
size = int(input("Enter map size: "))
custom_interval = input("Custom interval: ")
if custom_interval != '':
interval = int(custom_interval)
int_by_size = interval * size
screen = Screen()
screen.setup(width=0.45, height=0.8, startx=800, starty=50)
screen.bgcolor('#002240')
screen.tracer(False)
x, y = interval/2 - int_by_size/2, int_by_size/2 - interval/2
start_gen = time.time()
data = [(randint(False, True) for _ in range(size)) for _ in range(size)]
print("GEN DONE IN", time.time() - start_gen)
start_draw = time.time()
turtle = Turtle()
turtle.hideturtle()
turtle.shape('square')
turtle.penup()
turtle.color('blue')
turtle.shapesize(int_by_size / CURSOR_SIZE)
turtle.stamp()
turtle.color('green')
turtle.shapesize(interval / CURSOR_SIZE)
turtle.goto(x, y)
for line in data:
dx = x
for tile in line:
if tile:
turtle.setx(dx)
turtle.stamp()
dx += interval
# Comment out to print entire map at once
screen.update()
y -= interval
turtle.goto(x, y)
print("DRAW DONE IN", time.time() - start_draw)
screen.tracer(True)
screen.exitonclick()
Some of my optimizations you may need to undo, like making the data a list of generators instead of a list of lists (speeds up data generation but is only good for a single pass.) Also, remember that a window size of width=0.45, height=0.8 is relative to the size of your screen -- other folks running on other screens will get a different size window.

Random Walk - Any Direction

I'm new to this, but I'm trying to create a program that goes on a Random Walk in turtle and I have no idea what I'm doing.This is the program that I need to create Requirements, and this is what I have so far Code. I'm starting to get a little lost, and am wondering if I 'm headed towards the right direction or if i need to scrap it and start all over again
Thanks,
import turtle
import random
import math
def start():
myS = turtle.Screen()
myS.title("Random Walk")
border = 50
myWin.setworldcoordinates(-border, -border, border, border)
def go(heading, step_size):
turtle.setheading(heading)
turtle.forward(step_size)
def random_walk(step_size, steps):
angle = random.random() * 2 * math.pi
x = 0
y = 0
x = x + math.cos(angle)
y = y + math.sin(angle)
coord = (x, y)
for _ in range(steps):
go(random.choice(coord), step_size)
if __name__ == '__main__':
turtle.hideturtle()
turtle.speed('fastest')
random_walk(15, 1000)
Your random_walk function does way to many things; I think it grew over your head a little. What it should do for every step:
Calculate a random angle between 0 and 2*math.pi
Calculate x and y with the given formulas
Call turtle.goto(x,y) to go to these coordinates
Also there was a little typo in start.
import turtle
import random
import math
def start():
myS = turtle.Screen()
myS.title("Random Walk")
border = 50
myS.setworldcoordinates(-border, -border, border, border)
def go(heading, step_size):
turtle.setheading(heading)
turtle.forward(step_size)
def random_walk(steps):
x = 0
y = 0
for i in range(steps):
angle = random.random()*2*math.pi
x = x + math.cos(angle)
y = y + math.sin(angle)
turtle.goto(x,y)
if __name__=="__main__":
start()
random_walk(100)
Because this looks like a homework assignment I don't want to just fix your errors but instead give you pointers to the right solution. I will also hide hints behind spoilers...
if you run random_walk() with e.g. parameters random_walk(1, 1) your will notice that the direction of the walk will not appear random. Use some form of debugging to find out why that is
your go() functions expects an angle, look at what random.choice() produces
if you fix the issue number 1. you will see that the turtle will not draw a random line at all again
looks like the angle does not change every step. find a solution that updates the angle before each step!
units of angles: find out what unit setheading() expects: hint
your requirements mention the output of straight line distance and distance travelled. This somehow sounds like you need some form of tally variable (one or several). hint: if you read the turtle documentation carefully you might find a funtion that maybe makes this task easier
read the documentation for turtle.position().

Python Turtle: change pencolor of square when clicking

I'm still a newbie to the programming world, trying to find some help. I don't understand most of the codes. I've tried different approaches to it before I ended up here:
import turtle
import random
window = turtle.Screen()
#window.colormode(255)
square = turtle.Turtle()
square.speed(0)
square.hideturtle()
square.up()
square.goto(-200, 200)
square.down()
for i in range(4):
square.forward(50)
square.right(90)
square.up()
square.goto(-205, 205)
square.write("Change Color")
pencil = turtle.Turtle()
pencil.shape("circle")
def drawing_controls(x, y):
if (-200 <= x <= -150) and (150 <= y <= 200):
pencil.color(random.random(), random.random(), random.random())
#def sqc (self, x, y):
window.onclick(drawing_controls)
#window.onclick(square.pencolor(random.random(), random.random(),
random.random()))
pencil.onrelease(pencil.goto)
The lines with the # comments are from one of my many previous attempts in solving this frustrating problem.
I'm not certain of your actual goal but I think the rework below will give you the behaviors you're looking for. Rather than draw a square to click in, the turtle becomes the square that will be clicked:
from turtle import Turtle, Screen
from random import random
def drawing_controls(x, y):
pencil.color(random(), random(), random())
square.color(*pencil.color())
def drag_pencil(x, y):
pencil.ondrag(None)
pencil.goto(x, y)
pencil.ondrag(drag_pencil)
square = Turtle('square', visible=False)
square.speed('fastest')
square.hideturtle()
square.up()
square.goto(-200, 230)
square.write('Change Color', align='center')
square.goto(-200, 200)
square.shapesize(50 / 20)
square.showturtle()
pencil = Turtle('circle')
square.onclick(drawing_controls)
pencil.ondrag(drag_pencil)
window = Screen()
window.mainloop()
Instead of onrelease(), I switched to ondrag() to make it more interactive. However, using pencil.goto won't work smoothly, despite being a common example of an ondrag() handler, so I wrote a hander that disables the drag event while in the drag handler to smooth the motion.

Random Line Generation For pygame

My goal is to make a retro lunar lander game using pygame. Right now I'm trying to create the background of mountains and platforms to land on. I would like for every time a new game is started the platforms and mountains be different. My idea is to draw a random number of horizontal lines with random lengths as the platform. I have a few problems with this:
How to make sure the horizontal lines don't overlap and are all accesible in regards of looking at it like you're landing.
I think the best way to draw the mountains would be to connect the platforms with more lines that create triangles, but I have no idea where to begin to do that
the code I have been working for currently generates random lines, but I'm not sure how to develop the code further.
for platforms in range(1, random.randint(1, 10)):
rand_x_start = random.randint(0, 400)
rand_x_end = random.randint(0, 400)
rand_y = random.randint(HEIGHT / 2, HEIGHT)
pygame.draw.line(self.background, white, (rand_x_start, rand_y), (rand_x_end, rand_y), 2)
I've tried looking through many questions and tutorials about random generation, but none are remotely close to what I'm looking for.
A thought I had to fix the overlapping problem was if there was a way to "read" or "see" previously created lines and to limit drawing based on that some how...but I'm not sure of a way to do that.
I put together the following code as something that might help you out:
import random
from collections import namedtuple
from PIL import Image, ImageDraw
GaussianDistribution = namedtuple('GaussianDistribution', ['mu', 'sigma'])
Platform = namedtuple('Platform', ['start', 'end', 'height'])
Point = namedtuple('Point', ['x', 'y'])
PLATFORM_HEIGHT_DISTRIBUTION = GaussianDistribution(50, 10)
PLATFORM_WIDTH_DISTRIBUTION = GaussianDistribution(100, 10)
INTER_PLATFORM_DISTRIBUTION = GaussianDistribution(500, 20)
RANDOM = random.Random()
def sample_distribution(distribution):
return RANDOM.normalvariate(distribution.mu, distribution.sigma)
def create_platforms(num_platforms):
last_platform_end = 0
for i in range(num_platforms):
start = int(sample_distribution(INTER_PLATFORM_DISTRIBUTION) + last_platform_end)
end = int(sample_distribution(PLATFORM_WIDTH_DISTRIBUTION) + start)
height = int(sample_distribution(PLATFORM_HEIGHT_DISTRIBUTION))
last_platform_end = end
yield Platform(start, end, height)
def create_mountains_between_points(p1, p2):
mountain_start = p1.x
mountain_end = p2.x
num_points = int((mountain_end - mountain_start) / 10)
for i in range(num_points):
# TODO: use 1D Perlin function to generate point.y
yield Point(mountain_start + i * 10, RANDOM.random() * 100)
def create_terrain(platforms):
origin = Point(0, 0)
first_platform_start = Point(platforms[0].start, platforms[0].height)
terrain = []
terrain += list(create_mountains_between_points(origin, first_platform_start))
for i, platform in enumerate(platforms):
platform_starts_at = Point(platform.start, platform.height)
platform_ends_at = Point(platform.end, platform.height)
terrain.append(platform_starts_at)
terrain.append(platform_ends_at)
if i < len(platforms) - 1:
next_platform = platforms[i + 1]
next_platform_starts_at = Point(next_platform.start, next_platform.height)
mountains = create_mountains_between_points(platform_ends_at, next_platform_starts_at)
terrain += mountains
return terrain
def draw_terrain(terrain):
im = Image.new('RGBA', (4000, 200), (255, 255, 255, 0))
draw = ImageDraw.Draw(im)
draw.line(terrain, fill=(0, 0, 0, 255))
im.show()
if __name__ == '__main__':
platforms = list(create_platforms(num_platforms=10))
terrain = create_terrain(platforms)
draw_terrain(terrain)
This generates the following terrain. You can see that there are flat areas (platforms) separated by mountains. Platforms are guaranteed not to overlap. They are also guaranteed to be "accessible" in that there will never be any terrain generated over them.
I generate the y ordinates for the mountains by sampling from a uniform distribution. You can probably make the mountains look better by using the Perlin noise algorithm I mentioned earlier. I found this attempt to use Perlin noise in a pygame application.

Implementing the Koch Curve?

I was looking at the wikipedia page for the Koch Snowflake (here) and was bothered by the all the examples all being in the logo/turtle style. So i set out to make my own that returned a list or coordinates.
My implementation is in python and i basically ripped off the python turtle implementation but replaced the turtle specific stuff with basic trig. It resulted in some ugly code. My challenge for you is to either improve my code or come up with a more elligant solution of your own. It can be in python, or your favorite language.
My Code:
from math import sin, cos, radians
def grow(steps, length = 200, startPos = (0,0)):
angle = 0
try:
jump = float(length) / (3 ** steps)
except:
jump = length
set="F"
for i in xrange(steps): set=set.replace("F", "FLFRFLF")
coords = [startPos]
for move in set:
if move is "F":
coords.append(
(coords[-1][0] + jump * cos(angle),
coords[-1][1] + jump * sin(angle)))
if move is "L":
angle += radians(60)
if move is "R":
angle -= radians(120)
return coords
EDIT: due to lazy copying, i forgot the import
I don't see it as particularly ugly and I'd only refactor it incrementally, e.g. as a first step (I've removed the try/except because I don't know what you're trying to ward against... if it needs to get back in it should be a bit more explicit, IMHO):
import math
angles = [math.radians(60*x) for x in range(6)]
sines = [math.sin(x) for x in angles]
cosin = [math.cos(x) for x in angles]
def L(angle, coords, jump):
return (angle + 1) % 6
def R(angle, coords, jump):
return (angle + 4) % 6
def F(angle, coords, jump):
coords.append(
(coords[-1][0] + jump * cosin[angle],
coords[-1][1] + jump * sines[angle]))
return angle
decode = dict(L=L, R=R, F=F)
def grow(steps, length=200, startPos=(0,0)):
pathcodes="F"
for i in xrange(steps):
pathcodes = pathcodes.replace("F", "FLFRFLF")
jump = float(length) / (3 ** steps)
coords = [startPos]
angle = 0
for move in pathcodes:
angle = decode[move](angle, coords, jump)
return coords
If a second step was warranted I'd probably roll this functionality up into a class, but I'm not sure that would make things substantially better (or, better at all, in fact;-).
I liked your question so much that I posted an answer to it as a new question, so that other people could improve it:
https://stackoverflow.com/questions/7420248
I used no Logo/Turtle stuff, neither trigonometry.
Congrats for being the first one bringing this problem to StackOverflow!
Mathematica is superior when it comes to math stuff:
points = {{0.0, 1.0}};
koch[pts_] := Join[
pts/3,
(RotationMatrix[60 Degree].#/3 + {1/3, 0}) & /# pts,
(RotationMatrix[-60 Degree].#/3 + {1/2, 1/Sqrt[12]}) & /# pts,
(#/3 + {2/3, 0}) & /# pts
];
Graphics[Line[Nest[koch, points, 5]], PlotRange -> {{0, 1}, {0, 0.3}}] //Print
Something to consider, if not for your implementation then for testing your implementation, is that Python turtle can record what it's doing and give you back the coordinates. You use begin_poly() and end_poly() around the code you want to record and then use get_poly() afterwards to get the points.
In this example, I'll draw the snowflake based on code from this site then register those coordinates back as a new turtle shape that I'll randomly (and quickly) stamp about the screen:
import turtle
from random import random, randrange
def koch_curve(turtle, steps, length):
if steps == 0:
turtle.forward(length)
else:
for angle in [60, -120, 60, 0]:
koch_curve(turtle, steps - 1, length / 3)
turtle.left(angle)
def koch_snowflake(turtle, steps, length):
turtle.begin_poly()
for _ in range(3):
koch_curve(turtle, steps, length)
turtle.right(120)
turtle.end_poly()
return turtle.get_poly()
turtle.speed("fastest")
turtle.register_shape("snowflake", koch_snowflake(turtle.getturtle(), 3, 100))
turtle.reset()
turtle.penup()
turtle.shape("snowflake")
width, height = turtle.window_width() / 2, turtle.window_height() / 2
for _ in range(24):
turtle.color((random(), random(), random()), (random(), random(), random()))
turtle.goto(randrange(-width, width), randrange(-height, height))
turtle.stamp()
turtle.done()
You can have the pen up and turtle hidden during polygon generation if you don't want this step to be seen by the user.

Categories