# Draws and synthesises the art kit the beginner's course uses, into docs/learn/art. # # A tutorial cannot say "find a picture of a spaceship": the pictures have to be there, named the # same on every machine, or half the readers stop at lesson nine. Everything here is drawn from # shapes rather than painted, so it is ours, it is tiny, and it can be regenerated. The engine # already gives every reader a video, two models, a font, and a click (the Singe folder it unpacks # on the first run), so this kit is only the things a 2D game needs: a ship, a rock, a shot, a # star, one walk cycle, and two noises; and one small database, the stages lesson twenty-nine's # game reads. # # Usage: python3 util/learnKit.py import math import os import sqlite3 import struct import wave from PIL import Image, ImageDraw REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) OUT = os.path.join(REPO, "docs", "learn", "art") RATE = 22050 def blank(width, height): return Image.new("RGBA", (width, height), (0, 0, 0, 0)) def save(image, name): path = os.path.join(OUT, name) image.save(path) print("%s %dx%d" % (name, image.width, image.height)) # A ship pointing up: a hull, a nose, and two fins, so it reads at 32 pixels. def ship(): image = blank(32, 24) draw = ImageDraw.Draw(image) draw.polygon([(16, 0), (23, 17), (9, 17)], fill=(120, 200, 255, 255)) draw.polygon([(16, 3), (20, 16), (12, 16)], fill=(230, 245, 255, 255)) draw.polygon([(9, 12), (2, 22), (9, 20)], fill=(70, 130, 200, 255)) draw.polygon([(23, 12), (30, 22), (23, 20)], fill=(70, 130, 200, 255)) draw.rectangle([13, 17, 18, 21], fill=(255, 170, 60, 255)) draw.rectangle([14, 19, 17, 23], fill=(255, 230, 120, 255)) return image # A rock: an uneven lump with a couple of craters, so a rotation would not be missed. def rock(): image = blank(24, 24) draw = ImageDraw.Draw(image) points = [(12, 1), (19, 4), (23, 11), (20, 19), (13, 23), (5, 21), (1, 13), (3, 5)] draw.polygon(points, fill=(150, 140, 130, 255)) draw.polygon([(12, 3), (17, 6), (19, 11), (16, 17), (11, 20), (6, 18), (4, 12), (6, 6)], fill=(185, 175, 165, 255)) draw.ellipse([8, 7, 12, 11], fill=(140, 130, 120, 255)) draw.ellipse([14, 13, 17, 16], fill=(140, 130, 120, 255)) return image def shot(): image = blank(4, 10) draw = ImageDraw.Draw(image) draw.rectangle([0, 2, 3, 9], fill=(255, 210, 90, 255)) draw.rectangle([1, 0, 2, 9], fill=(255, 255, 220, 255)) return image def star(): image = blank(8, 8) draw = ImageDraw.Draw(image) draw.line([4, 0, 4, 7], fill=(255, 240, 160, 255)) draw.line([0, 4, 7, 4], fill=(255, 240, 160, 255)) draw.point([(3, 3), (5, 3), (3, 5), (5, 5)], fill=(255, 210, 90, 255)) return image # Four frames of a walk, side by side in one picture: the sheet the frames lesson steps through. # The legs swing, the arms swing the other way, and frames two and four differ so the cycle reads. def walk(): image = blank(24 * 4, 32) draw = ImageDraw.Draw(image) swing = [(-5, 5), (0, 0), (5, -5), (0, 0)] for frame in range(4): x = frame * 24 + 12 lead = swing[frame][0] back = swing[frame][1] bob = 1 if frame % 2 == 0 else 0 draw.ellipse([x - 4, 3 + bob, x + 4, 11 + bob], fill=(245, 205, 160, 255)) draw.rectangle([x - 4, 12 + bob, x + 4, 22 + bob], fill=(80, 130, 200, 255)) draw.line([x - 4, 13 + bob, x - 4 - abs(back), 19 + bob], fill=(245, 205, 160, 255), width=2) draw.line([x + 4, 13 + bob, x + 4 + abs(lead), 19 + bob], fill=(245, 205, 160, 255), width=2) draw.line([x - 1, 22 + bob, x - 1 + back, 30], fill=(60, 60, 90, 255), width=3) draw.line([x + 1, 22 + bob, x + 1 + lead, 30], fill=(40, 40, 70, 255), width=3) return image # A noise, written straight out as a 16 bit mono WAV: no library, and small enough to read. def tone(name, seconds, shape): frames = int(RATE * seconds) data = bytearray() for i in range(frames): t = i / RATE value = shape(t, t / seconds) value = max(-1.0, min(1.0, value)) data += struct.pack("> 16) % 2000 - 1000) / 1000.0 return (noise * 0.8 + math.sin(2.0 * math.pi * 70.0 * t) * 0.5) * (1.0 - done) ** 3 # The stages of lesson twenty-nine's reaction test, as the database a game would ship: a name and # the shortest and longest wait before "NOW!", in milliseconds. Written fresh each time, so the # file is the same on every machine. STAGES = [ (1, "Easy", 1000, 3000), (2, "Patient", 3000, 6000), (3, "Twitchy", 300, 1200), (4, "Cruel", 200, 8000), ] def stages(): path = os.path.join(OUT, "stages.db") if os.path.exists(path): os.remove(path) db = sqlite3.connect(path) db.execute("CREATE TABLE stages (id INTEGER PRIMARY KEY, name TEXT NOT NULL, waitMin INTEGER NOT NULL, waitMax INTEGER NOT NULL)") db.executemany("INSERT INTO stages VALUES (?, ?, ?, ?)", STAGES) db.commit() db.execute("VACUUM") db.close() print("stages.db %d stages" % len(STAGES)) def main(): os.makedirs(OUT, exist_ok=True) save(ship(), "spaceship.png") save(rock(), "asteroid.png") save(shot(), "bullet.png") save(star(), "star.png") save(walk(), "walk.png") tone("shoot.wav", 0.18, shootShape) tone("boom.wav", 0.45, boomShape) stages() if __name__ == "__main__": main()