Fractal Mountains in Pygame
A fractal mountain, 1987 style. Sorry, no, the generator doesn’t use a CRT shader just yet.
During the 2020 shutdowns, I decided to revive a really old interest of mine. Back in the 1980s when I was just a kid, my parents subscribed me to Compute! Magazine. For the folks who didn’t have the pleasure of living through those times or didn’t have a computer back then, software would be distributed as source code in books and magazines; the folks at home would have to type in the program to see it run. For some of us, it was a learning experience.
I remembered seeing this in a 1987 issue of Compute! My family had a Tandy 1000 EX, and this one was written for the Commodore Amiga. The Amiga was the artist’s computer, with more color and better sound, for a price of course.
The main difference between Amiga BASIC and other BASIC interpreters of the time, is that they did away with line numbers and replaced them with labels. Other than that, and a few statements specific to the Amiga, it was just BASIC. The goal I set for myself was to change as little as possible, but get it to work in Python and PyGame. Basically, run the program from the command line, press Escape to exit, press ‘S’ to save an image in the working director under the name mountain-YYYYMMDD-HHMMSS.png, and any other key to draw a new mountain.
Other than having to make some minor syntax changes for the change from BASIC to Python, the only other difficulties I found were going from 12-bit to 24-bit color, and a couple of minor OCR problems. I had this in print at one point, but 1987 was a while ago.
Video of an Amiga running the original
Link to Compute! August 1987 on Archive.org
#!/usr/bin/env python
# Originally Copyright 1987 Compute! Publications, Inc.
# All Rights Reserved.
# Ported to Pygame 2020 Shane Simmons
import pygame, sys, random, pygame.gfxdraw
from pygame.locals import *
from datetime import datetime
window = pygame.display.set_mode((960, 720))
class drawMountain:
llv = 0
maxLv = 0
xm = 4.5
hl = 0
ym =2
x = 0
y = 0
xshift = .9
yp = 45
snowline = 0
def __init__(self):
self.lv = [[0.0] * 65 for f in range (65)]
self.colors = [[None] * 3] * 32
self.surf = pygame.Surface((320,200))
for a in range(16):
r = min(int(16*(a/15.0)), 15)
g = min(int(16*(a/25.0)), 15)
b = min(int(16*(a/50.0)), 15)
r += (r << 4)
g += (g << 4)
b += (b << 4)
self.colors[a] = [r, g, b] # dirt tones
self.colors[a+16] = [r, r, r] # snow tones
self.colors[16] = [0, 64, 128] # water color
def getshade(self, a, b, x, y):
c = x + 1 - (b - y)
d = y + (a - x)
xc = x + .5
yc = y + .5
xrun1 = xc - a
xrun2 = xc - c
yrun1 = yc - b
yrun2 = yc - d
rise1 = self.llv - self.lv[a][b]
rise2 = self.llv - self.lv[c][d]
yrise = abs((rise1 * xrun2) - (rise2 * xrun1))
yrun = abs((yrun1 * xrun2) - (xrun1 * yrun2))
if (yrun == yrise):
yrun = 1
yrise = 1
xrise = abs((rise1 * yrun2) - (rise2 * yrun1))
xrun = abs((xrun1 * yrun2) - (yrun1 * xrun2))
if (xrun == xrise):
xrun = 1
xrise = 1
xrise = xrise / 2
yrise = yrise / 2
xshade = 1-abs(xrise / (xrun + xrise))
yshade = 1-abs(yrise / (yrun + yrise))
shade = 14 * xshade * yshade + 1
if (self.llv > self.snowline):
shade = shade + 16
if (self.llv <= 0):
shade = 16
return(int(shade))
def draw(self):
self.maxLv = 0
self.surf.fill((0,0,0))
max = random.uniform(0.95, 1.15) # maximum variation, original program says "1 is nice"
for iter in range(6,0,-1):
sk = 2 ** iter
hl = int(sk / 2)
# do tops
for y in range(0,65,sk):
for x in range(hl,64,sk):
ran = (random.random() - 0.5) * max * sk
old = (self.lv[x-hl][y] + self.lv[x + hl][y]) / 2
self.lv[x][y] = old + ran
# do bottoms
for x in range(0, 65, sk):
for y in range(hl, 65, sk):
ran = (random.random() - 0.5) * max * sk
old = (self.lv[x][y-hl] + self.lv[x][y+hl]) / 2
self.lv[x][y] = old + ran
# do centers
for x in range(hl, 65, sk):
for y in range(hl, 65, sk):
ran = (random.random()-0.5) * max * sk
old1 = (self.lv[x+hl][y-hl] + self.lv[x-hl][y+hl]) / 2
old2 = (self.lv[x-hl][y-hl] + self.lv[x+hl][y+hl]) / 2
old = (old1 + old2) / 2
self.lv[x][y] = old + ran
if self.lv[x][y] > self.maxLv:
self.maxLv = self.lv[x][y]
self.snowline = self.maxLv - self.maxLv/4
for x in range(0,65):
if (self.lv[x][0] < 0):
self.lv[x][0] = 0
for y in range(0,64):
if (self.lv[0][y] < 0):
self.lv[0][y] = 0
for x in range(0,64):
if (self.lv[x+1][y+1] < 0):
self.lv[x+1][y+1] = 0
self.llv = self.lv[x][y] + self.lv[x+1][y] + self.lv[x][y+1]
self.llv = (self.llv + self.lv[x+1][y+1]) / 4
a = x
b = y
rx1 = self.xm * a + self.xshift * b
ry1 = self.ym * b + self.yp - self.lv[a][b]
shade1 = tuple(self.colors[self.getshade(a, b, x, y)])
a = x + 1
rx2 = self.xm * a + self.xshift * b
ry2 = self.ym * b + self.yp - self.lv[a][b]
shade2 = tuple(self.colors[self.getshade(a, b, x, y)])
a = x
b = y + 1
rx3 = self.xm * a + self.xshift * b
ry3 = self.ym * b + self.yp - self.lv[a][b]
shade3 = tuple(self.colors[self.getshade(a, b, x, y)])
a = x + 1
rx4 = self.xm * a + self.xshift * b
ry4 = self.ym * b + self.yp - self.lv[a][b]
shade4 = tuple(self.colors[self.getshade(a, b, x, y)])
a = x + .5
b = y + .5
rx = self.xm * a + self.xshift * b
ry = self.ym * b + self.yp
a = x
b = y
ry = ry - self.llv
pygame.gfxdraw.filled_polygon(self.surf, ((rx, ry), (rx1, ry1), (rx2, ry2)), shade1)
pygame.gfxdraw.filled_polygon(self.surf, ((rx, ry), (rx2, ry2), (rx4, ry4)), shade2)
pygame.gfxdraw.filled_polygon(self.surf, ((rx, ry), (rx1, ry1), (rx3, ry3)), shade3)
pygame.gfxdraw.filled_polygon(self.surf, ((rx, ry), (rx3, ry3), (rx4, ry4)), shade4)
pygame.transform.smoothscale(self.surf,(960,720), window)
window.blit(self.surf, (960,720))
pygame.display.flip()
pygame.display.update()
def image_save(self):
now = datetime.now()
fn = now.strftime("mountain-%Y%m%d-%H%M%S.png")
pygame.image.save(self.surf, fn)
print("doopy")
dm = drawMountain()
dm.draw()
done = False
while not done:
for event in pygame.event.get():
if event.type == pygame.QUIT:
pygame.quit()
quit()
if event.type == KEYDOWN:
if event.key == K_ESCAPE:
done = True
elif event.key == pygame.K_s:
dm.image_save()
else:
dm.draw()