Last active
January 17, 2023 19:20
-
-
Save demodude4u/d387d94a7d7f08d3aad08b931508cfd6 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
from sys import path as syspath | |
syspath.insert(0, '/Games/RocketCup') | |
import time | |
import math | |
import thumbyGrayscale as thumby | |
#import thumby | |
import thumbyAudio | |
import random | |
from thumbySaves import saveData | |
from machine import Pin, UART | |
thumby.display.setFPS(30) | |
try: | |
import emulator | |
emulated = True | |
except ImportError: | |
emulated = False | |
def lerp(v1,v2,f): | |
return v1 + (v2-v1) * f | |
def saveDataOptItem(key, default): | |
if (saveData.hasItem(key)): | |
return saveData.getItem(key) | |
else: | |
return default | |
try: | |
sprBallTex = thumby.Sprite(12, 12, [bytearray([231,231,255,60,60,255,231,231,255,60,60,255, | |
9,9,15,15,15,15,9,9,15,15,15,15]),bytearray([60,60,219,231,231,219,60,60,219,231,231,219, | |
15,15,6,9,9,6,15,15,6,9,9,6])]) | |
sprBallVoid = thumby.Sprite(18, 18, bytearray([0,0,0,0,0,0,128,192,192,192,192,128,0,0,0,0,0,0, | |
0,0,0,0,0,0,7,15,15,15,15,7,0,0,0,0,0,0, | |
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]), key=1) | |
sprBallOutline = thumby.Sprite(8, 8, [bytearray([60,66,129,129,129,129,66,60]),bytearray([0,64,128,128,128,128,64,60])], key=0) | |
# BITMAP: width: 16, height: 8 | |
bmpWipe = bytearray([119,238,119,238,187,221,187,221,221,187,221,187,238,119,238,119]) | |
saveData.setName("RocketCup") | |
sdSound = saveDataOptItem("sound",1) | |
sdGrayscale = saveDataOptItem("grayscale",1) | |
sdAi = saveDataOptItem("ai",0) | |
sdTurnAssist = saveDataOptItem("turn-assist",0) | |
if (not sdGrayscale): | |
thumby.display.disableGrayscale() | |
thumbyAudio.audio.setEnabled(sdSound) | |
############### | |
## MENU LOOP ## | |
############### | |
# BITMAP: width: 45, height: 40 | |
bmpMenuCar = bytearray([0,88,16,96,224,96,96,96,48,48,48,48,16,24,24,24,24,24,24,29,10,4,8,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, | |
0,0,0,240,240,240,224,240,184,240,240,248,232,194,140,152,194,68,105,249,251,248,252,252,248,248,240,224,192,128,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, | |
0,160,128,192,187,247,215,31,47,15,15,11,127,95,255,255,252,248,240,224,193,131,143,255,231,135,135,3,3,3,3,2,140,216,224,192,128,192,192,192,128,128,0,0,0, | |
0,2,7,7,3,5,2,0,0,0,0,0,0,1,15,15,31,31,31,31,47,63,255,255,191,127,63,63,255,255,127,255,255,255,255,255,255,255,255,239,15,7,3,3,0, | |
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,31,62,63,0,7,2,0,0,0,0,1,3,12,11,11,0,9,0,4,4,18,1,0,1,0,0]) | |
bmpsMenuCar = bytearray([16,120,112,224,224,224,96,112,240,240,112,112,56,56,56,56,120,248,31,31,30,28,24,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, | |
0,0,112,248,184,48,16,112,233,223,140,28,62,62,126,126,190,191,191,175,15,14,22,12,24,8,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, | |
0,252,254,254,255,250,248,248,248,249,115,127,126,252,248,33,198,12,216,1,66,198,184,0,116,4,132,132,128,128,130,134,136,248,0,64,192,64,64,192,0,128,128,0,0, | |
0,3,15,31,63,63,63,31,15,1,0,0,0,3,15,31,27,30,157,235,251,242,227,226,230,224,232,193,209,224,192,52,192,128,144,64,128,11,222,255,254,252,255,127,0, | |
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,15,63,127,255,255,255,255,127,7,3,3,31,63,63,63,63,63,63,63,63,31,31,15,15,7,0,0]) | |
# BITMAP: width: 42, height: 8 | |
bmpMenuLogoRocket = bytearray([0,0,126,126,22,126,108,0,60,126,102,126,60,0,60,126,102,102,0,126,126,24,126,102,0,126,126,86,86,0,6,126,126,6,60,66,1,1,1,1,0,0]) | |
bmpsMenuLogoRocket = bytearray([60,126,129,193,233,129,219,255,195,129,153,193,227,255,195,129,153,221,255,129,193,231,129,221,255,129,129,169,253,255,249,129,193,253,255,195,129,129,129,129,66,60]) | |
# BITMAP: width: 33, height: 12 | |
bmpMenuLogoCup = bytearray([0,0,224,24,4,2,2,2,2,2,0,0,0,0,254,2,0,0,252,2,1,0,0,0,0,248,7,1,65,33,28,0,0, | |
0,0,3,4,8,0,4,4,4,4,2,1,0,0,15,0,0,2,3,0,0,0,0,8,7,0,0,0,0,0,0,0,0]) | |
bmpsMenuLogoCup = bytearray([28,50,226,249,253,95,71,67,99,62,28,124,134,130,254,254,64,32,252,254,255,1,124,134,3,249,255,31,65,99,126,126,60, | |
0,0,3,7,15,14,12,12,12,4,2,1,0,0,15,15,12,6,7,15,15,4,0,8,15,15,15,8,0,0,0,0,0]) | |
# BITMAP: width: 33, height: 8 | |
bmpMenuLogoCupShineA = bytearray([0,0,224,248,252,30,6,2,2,2,0,0,0,0,254,254,0,0,252,254,255,0,0,0,0,248,255,31,65,99,126,126,60]) | |
bmpsMenuLogoCupShineA = bytearray([28,50,2,225,249,93,69,65,97,60,28,124,134,130,0,252,64,32,0,252,254,1,124,134,3,1,248,30,0,66,98,126,60]) | |
bmpMenuLogoCupShineB = bytearray([0,0,3,7,15,14,12,12,12,4,2,1,0,0,15,15,12,6,7,15,15,4,0,8,15,15,15,8,0,0,0,0,0]) | |
bmpsMenuLogoCupShineB = bytearray([0,0,0,3,7,14,8,8,8,0,0,0,0,0,0,15,12,4,4,15,15,4,0,0,8,15,15,8,0,0,0,0,0]) | |
# BITMAP: width: 8, height: 8 | |
bmpMenuLogoCupShineMaskA = bytearray([248,199,56,192,0,7,63,255]) | |
bmpMenuLogoCupShineMaskB = bytearray([15,15,14,1,14,0,0,1]) | |
bmpMenuLogoCupShineSliceA = bytearray(8) | |
bmpsMenuLogoCupShineSliceA = bytearray(8) | |
bmpMenuLogoCupShineSliceB = bytearray(8) | |
bmpsMenuLogoCupShineSliceB = bytearray(8) | |
sfxNav = [600,100] | |
animFrame = 0 | |
animDuration = 45 | |
while (animFrame < animDuration + 5): | |
animFrame += 1 | |
# Car Driving | |
animLerp = ((-math.cos(math.pi * animFrame / animDuration) + 1) * 0.75) % 1 | |
animX = lerp(-70,70,animLerp) | |
animY = lerp(-45,45,animLerp) | |
thumby.display.fill(0) | |
# Car Brake Jerk | |
if (animFrame == animDuration + 5): | |
animX = 0 | |
animY = 0 | |
elif (animFrame > animDuration): | |
animX = 1 | |
animY = 1 | |
else: | |
#Ball Rotation | |
ballLerp = (-math.cos(math.pi * animFrame / animDuration) + 1) | |
sprBallTex.x = 66 - 9 + ((int(ballLerp * 10+1)) % 6) | |
sprBallTex.y = 6 - 9 + ((int(ballLerp * 5)) % 6) | |
thumby.display.drawSprite(sprBallTex) | |
sprBallVoid.x = 66 - 9 | |
sprBallVoid.y = 6 - 9 | |
thumby.display.drawSprite(sprBallVoid) | |
thumby.display.blit([bmpMenuCar, bmpsMenuCar],round(animX),round(animY),45,40,-1,0,0) | |
thumby.display.blit([bmpMenuLogoRocket,bmpsMenuLogoRocket],72 - 44,2,42,8,0,0,0) | |
thumby.display.blit([bmpMenuLogoCup,bmpsMenuLogoCup],72 - 33,11,33,12,0,0,0) | |
thumby.display.update() | |
showOptions = False | |
optionSelect = 0 | |
options = [["Play CPU"], | |
["Link Cable"], | |
["Training"], | |
["Sound OFF","Sound ON"], | |
["Display BW","Display GS"], | |
["AI EASY","AI MEDIUM","AI HARD"], | |
["Turn ONCE","Turn SLOW","Turn FAST"], | |
["Credits"]] | |
optionText = [0,0,0,sdSound,sdGrayscale,sdAi,sdTurnAssist,0] | |
gameMode = -1 | |
thumby.display.setFont("/lib/font3x5.bin", 3, 5, 2) | |
while (True): | |
if (not showOptions and | |
(thumby.buttonL.justPressed() | |
or thumby.buttonR.justPressed() | |
or thumby.buttonA.justPressed())): | |
showOptions = True | |
else: | |
if (thumby.buttonR.justPressed()): | |
optionSelect = (optionSelect + 1) % len(options) | |
thumbyAudio.audio.play(sfxNav[0],sfxNav[1]) | |
elif (thumby.buttonL.justPressed()): | |
optionSelect = (optionSelect + len(options) - 1) % len(options) | |
thumbyAudio.audio.play(sfxNav[0],sfxNav[1]) | |
elif (thumby.buttonA.justPressed()): | |
if (optionSelect == 0): # Play CPU | |
gameMode = 0 | |
break | |
elif (optionSelect == 1): # Link Cable | |
gameMode = 1 | |
break | |
elif (optionSelect == 2): # Training | |
gameMode = 2 | |
break | |
elif (optionSelect == 3): # Sound | |
sdSound = optionText[optionSelect] = (sdSound + 1) % 2 | |
saveData.setItem("sound",sdSound) | |
saveData.save() | |
thumbyAudio.audio.setEnabled(sdSound) | |
thumbyAudio.audio.play(sfxNav[0],sfxNav[1]) | |
elif (optionSelect == 4): # Display | |
sdGrayscale = optionText[optionSelect] = (sdGrayscale + 1) % 2 | |
saveData.setItem("grayscale",sdGrayscale) | |
saveData.save() | |
if (sdGrayscale): | |
thumby.display.enableGrayscale() | |
else: | |
thumby.display.disableGrayscale() | |
elif (optionSelect == 5): # AI | |
sdAi = optionText[optionSelect] = (sdAi + 1) % 3 | |
saveData.setItem("ai",sdAi) | |
saveData.save() | |
elif (optionSelect == 6): # Turn Assist | |
sdTurnAssist = optionText[optionSelect] = (sdTurnAssist + 1) % 3 | |
saveData.setItem("turn-assist",sdTurnAssist) | |
saveData.save() | |
elif (optionSelect == 7): # Credits | |
gameMode = 3 | |
break | |
animFrame += 1 | |
ballLerp += abs((math.pi*math.sin((math.pi*animFrame)/100))/100) | |
sprBallTex.x = 66 - 9 + ((int(ballLerp * 10+1)) % 6) | |
sprBallTex.y = 6 - 9 + ((int(ballLerp * 5)) % 6) | |
thumby.display.drawSprite(sprBallTex) | |
sprBallVoid.x = 66 - 9 | |
sprBallVoid.y = 6 - 9 | |
thumby.display.drawSprite(sprBallVoid) | |
thumby.display.blit([bmpMenuLogoRocket,bmpsMenuLogoRocket],72 - 44,2,42,8,0,0,0) | |
thumby.display.blit([bmpMenuLogoCup,bmpsMenuLogoCup],72 - 33,11,33,12,0,0,0) | |
shineFrame = min((animFrame * 3) % (30 * 15),(animFrame * 3 + 40) % (30 * 15)) | |
if (shineFrame < (33 + 7)): | |
shineX = shineFrame - 7 | |
shineY = 11 | |
for i in range(8): | |
j = shineX + i | |
if (j < 0 or j >= 33): | |
bmpMenuLogoCupShineSliceA[i] = 0 | |
bmpsMenuLogoCupShineSliceA[i] = 0 | |
bmpMenuLogoCupShineSliceB[i] = 0 | |
bmpsMenuLogoCupShineSliceB[i] = 0 | |
else: | |
bmpMenuLogoCupShineSliceA[i] = bmpMenuLogoCupShineA[j] | |
bmpsMenuLogoCupShineSliceA[i] = bmpsMenuLogoCupShineA[j] | |
bmpMenuLogoCupShineSliceB[i] = bmpMenuLogoCupShineB[j] | |
bmpsMenuLogoCupShineSliceB[i] = bmpsMenuLogoCupShineB[j] | |
thumby.display.blitWithMask([bmpMenuLogoCupShineSliceA,bmpsMenuLogoCupShineSliceA], | |
72 - 33 + shineX, shineY, 8, 8, 0, 0, 0, bmpMenuLogoCupShineMaskA) | |
thumby.display.blitWithMask([bmpMenuLogoCupShineSliceB,bmpsMenuLogoCupShineSliceB], | |
72 - 33 + shineX, shineY+8, 8, 8, 0, 0, 0, bmpMenuLogoCupShineMaskB) | |
thumby.display.blit([bmpMenuCar, bmpsMenuCar],0,0,45,40,0,0,0) | |
if (showOptions): | |
thumby.display.drawFilledRectangle(0, 40 - 11, 72, 9, 2) | |
text = options[optionSelect][optionText[optionSelect]] | |
thumby.display.drawText(text, 36 - int(5*len(text)/2), 40 - 9, 1) | |
thumby.display.update() | |
#Screen Wipe | |
for i in range(72//4 + 4 + 5): | |
for j in range(5): | |
thumby.display.blit(bmpWipe,i*4-j*4-12,32-j*8,16,8,1,0,0) | |
thumby.display.update() | |
#[CPU,Linked,Training,Credits] | |
oppExist = [True,True,False,False][gameMode] | |
ballExist = [True,True,True,False][gameMode] | |
scoreExist = [True,True,True,False][gameMode] | |
goalExist = [True,True,True,False][gameMode] | |
gmCPU = (gameMode == 0) | |
gmLinked = (gameMode == 1) | |
gmTraining = (gameMode == 2) | |
gmCredits = (gameMode == 3) | |
#################### | |
## LINK HANDSHAKE ## | |
#################### | |
class Link: | |
mode = 0 # 0 = write first, 1 = read first | |
syncFrames = 0 | |
def __init__(self): | |
self.uart = UART(0, baudrate=115200, rx=Pin(1, Pin.IN), tx=Pin(0, Pin.OUT), timeout=1000, txbuf=1, rxbuf=1) | |
Pin(2, Pin.OUT).value(1) | |
while (self.uart.any() > 0): | |
self.uart.read(1) | |
def tryHandshake(self): | |
self.uart.write(bytearray([0x80])) | |
self.uart.read(1) #echo | |
time.sleep(0.1) #enough time for a response | |
while (self.uart.any() > 0): | |
response = self.uart.read(1)[0] | |
if (response == 0x81): #HandshakeAck | |
self.mode = 1 | |
return True | |
return False | |
def tryHandshakeAck(self): | |
while (self.uart.any() > 0): | |
response = self.uart.read(1)[0] | |
if (response == 0x80): #Handshake | |
self.uart.write(bytearray([0x81])) | |
self.uart.read(1) #echo | |
self.mode = 0 | |
return True | |
return False | |
def sync(self,data): | |
self.syncFrames += 1 | |
self.waitCount = 0 | |
if (self.mode == 0): #write first | |
self.uart.write(bytearray([data])) | |
self.uart.read(1) #echo | |
while (self.uart.any() == 0): | |
self.waitCount += 1 | |
return self.uart.read(1)[0] | |
else: #read first | |
while (self.uart.any() == 0): | |
self.waitCount += 1 | |
ret = self.uart.read(1) | |
self.uart.write(bytearray([data])) | |
self.uart.read(1) #echo | |
return ret[0] | |
def clear(self): | |
while (link.uart.any() > 0): | |
link.uart.read(1) | |
class EmuLink: | |
syncFrames = 0 | |
def tryHandshake(self): | |
self.mode = 0 | |
return random.random() < 0.3 | |
def tryHandshakeAck(self): | |
self.mode = 1 | |
return random.random() < 0.01 | |
def sync(self,data): | |
self.syncFrames += 1 | |
return data | |
def clear(self): | |
pass | |
if (gmLinked): | |
if (emulated): | |
link = EmuLink() | |
else: | |
link = Link() | |
thumby.display.setFont("/lib/font3x5.bin", 3, 5, 2) | |
thumby.display.drawText("CONNECTING", 36 - 23, 10, 1) | |
HandshakeWait = 0 | |
while (True): | |
if (HandshakeWait >= 30): | |
HandshakeWait = 0 | |
if (link.tryHandshake()): | |
break | |
else: | |
if (link.tryHandshakeAck()): | |
break | |
HandshakeWait += 1 | |
lineX = 36 + HandshakeWait | |
thumby.display.drawLine(lineX-1, 20, lineX-1, 30, 0) | |
thumby.display.drawLine(72-lineX+1, 20, 72-lineX+1, 30, 0) | |
thumby.display.drawLine(lineX, 20, lineX, 30, 1) | |
thumby.display.drawLine(72-lineX, 20, 72-lineX, 30, 1) | |
thumby.display.update() | |
thumby.display.drawText("MODE "+str(link.mode), 36 - 15, 35, 1) | |
thumby.display.update() | |
link.clear() | |
time.sleep(1) | |
############### | |
## GAME LOOP ## | |
############### | |
# BITMAP: width: 16, height: 16 | |
bmpCountdown3 = [bytearray([0,0,0,252,252,76,76,79,76,12,12,252,4,0,0,0, | |
0,0,0,31,31,18,18,18,18,16,16,31,0,0,0,0]), | |
bytearray([0,0,0,0,248,72,72,72,75,8,8,248,248,0,0,0, | |
0,0,0,32,63,50,50,50,50,48,48,63,63,0,0,0])] | |
bmpCountdown2 = [bytearray([0,0,0,252,252,220,204,79,12,12,156,252,4,0,0,0, | |
0,0,0,31,31,17,16,16,18,19,19,31,0,0,0,0]), | |
bytearray([0,0,0,0,248,216,200,72,11,8,152,248,248,0,0,0, | |
0,0,0,32,63,49,48,48,50,51,51,63,63,0,0,0])] | |
bmpCountdown1 = [bytearray([0,0,0,252,252,220,204,15,12,252,252,252,4,0,0,0, | |
0,0,0,31,31,19,19,16,16,19,19,31,0,0,0,0]), | |
bytearray([0,0,0,0,248,216,200,8,11,248,248,248,248,0,0,0, | |
0,0,0,32,63,51,51,48,48,51,51,63,63,0,0,0])] | |
bmpCountdownRotate1 = [bytearray([0,0,0,0,0,252,252,15,12,252,4,0,0,0,0,0, | |
0,0,0,0,0,31,31,16,16,31,0,0,0,0,0,0]), | |
bytearray([0,0,0,0,0,0,248,8,11,248,248,0,0,0,0,0, | |
0,0,0,0,0,32,63,48,48,63,63,0,0,0,0,0])] | |
bmpCountdownRotate2 = [bytearray([0,0,0,0,0,0,0,255,0,0,0,0,0,0,0,0, | |
0,0,0,0,0,0,0,31,0,0,0,0,0,0,0,0]), | |
bytearray([0,0,0,0,0,0,0,0,255,0,0,0,0,0,0,0, | |
0,0,0,0,0,0,0,32,63,0,0,0,0,0,0,0])] | |
bmpCountdownNumber = [bmpCountdown3,bmpCountdown2,bmpCountdown1] | |
bmpCountdownAnim = [bmpCountdownRotate2,bmpCountdownRotate1,bmpCountdownNumber[0],bmpCountdownNumber[0],bmpCountdownRotate1] | |
bmpCountdownArrow = [bytearray([24,40,79,129,129,79,40,24]),bytearray([0,16,48,126,126,48,16,0])] | |
# BITMAP: width: 8, height: 8 | |
bmpCarUp = bytearray([0,0,100,90,90,100,0,0]) | |
bmpsCarUp = bytearray([0,0,100,38,38,100,0,0]) | |
bmpCarDiag = bytearray([0,16,40,76,58,18,12,0]) | |
bmpsCarDiag = bytearray([0,24,24,118,102,30,28,0]) | |
bmpCarRight = bytearray([0,60,36,24,24,36,24,0]) | |
bmpsCarRight = bytearray([0,36,60,0,0,60,24,0]) | |
bmpCarPlayer = bmpCarUp + bmpCarDiag + bmpCarRight | |
bmpsCarPlayer = bmpsCarUp + bmpsCarDiag + bmpsCarRight | |
bmpCarOpponent = bmpCarUp + bmpCarDiag + bmpCarRight | |
bmpsCarOpponent = bmpsCarUp + bmpsCarDiag + bmpsCarRight | |
bmpTrail0 = bytearray([0,0,0,0,0,0]) | |
bmpsTrail0 = bytearray([0,0,0,0,0,0]) | |
bmpTrail1 = bytearray([0,0,0,0,0,0]) | |
bmpsTrail1 = bytearray([0,0,12,12,0,0]) | |
bmpTrail2 = bytearray([0,0,12,12,0,0]) | |
bmpsTrail2 = bytearray([0,12,30,30,12,0]) | |
bmpTrail3 = bytearray([12,30,51,51,30,12]) | |
bmpsTrail3 = bytearray([12,18,33,33,18,12]) | |
bmpTrail4 = bytearray([0,0,0,0,0,0]) | |
bmpsTrail4 = bytearray([0,0,0,0,0,0]) | |
bmpTrails = bmpTrail0 + bmpTrail1 + bmpTrail2 + bmpTrail3 + bmpTrail4 | |
bmpsTrails = bmpsTrail0 + bmpsTrail1 + bmpsTrail2 + bmpsTrail3 + bmpsTrail4 | |
trailFrameCount = 5 | |
trailInterval = 2 | |
trailDuration = 16 | |
trailSpriteCount = (trailDuration + (trailInterval-1)) // trailInterval | |
sprGoal = thumby.Sprite(4, 16, [bytearray([253,0,0,0,191,0,0,0]),bytearray([6,0,2,72,96,0,64,18])], key=0) | |
bmpDigits = [bytearray([0,14,0]), | |
bytearray([31,31,0]), | |
bytearray([2,10,8]), | |
bytearray([10,10,0]), | |
bytearray([24,27,0]), | |
bytearray([8,10,2]), | |
bytearray([0,10,2]), | |
bytearray([30,30,0]), | |
bytearray([0,10,0]), | |
bytearray([8,10,0])] | |
sprDigit = thumby.Sprite(3,5, bmpDigits[0], key=1) | |
bmpScoreTab = [bytearray([15,63,255,127,127,127,127,127,127,127,127,127,31,7,0]), | |
bytearray([0,7,31,255,255,255,255,255,255,255,255,255,255,63,15])] | |
mapRotX = [1,1,0,-1,-1,-1,0,1] | |
mapRotY = [0,1,1,1,0,-1,-1,-1] | |
speed1 = 0.75 | |
speed1MoveFrames = 30 | |
speed1StopFrames = 10 | |
speed1RevFrames = 5 | |
speed2 = 1.5 | |
class Car: | |
x = 0.0 | |
y = 0.0 | |
rotate = 0 | |
gas = 0 | |
allowBoost = False | |
speedShift = 0.0 | |
speed = 0 | |
trailCounter = 0 | |
trailNext = 0 | |
trailDecay = [] | |
sprTrails = [] | |
def __init__(self, bmpCars, bmpsCars): | |
self.sprCar = thumby.Sprite(8, 8, [bmpCars, bmpsCars], key=0) | |
for i in range(trailSpriteCount): | |
self.sprTrails.append(thumby.Sprite(6, 6, [bmpTrails, bmpsTrails], key=0)) | |
for i in range(trailSpriteCount): | |
self.trailDecay.append(0) | |
def physicsUpdate(self): | |
if (self.gas > 0): | |
if (self.speedShift >= 0.0): | |
self.speedShift += 1.0 / speed1MoveFrames | |
else: | |
self.speedShift += 1.0 / speed1RevFrames | |
if (self.speedShift > 1.0): | |
self.speedShift = 1.0 | |
elif (self.gas < 0): | |
if (self.speedShift >= 0.0): | |
self.speedShift -= 1.0 / speed1RevFrames | |
else: | |
self.speedShift -= 1.0 / speed1RevFrames | |
if (self.speedShift < -1.0): | |
self.speedShift = -1.0 | |
else: | |
if (self.speedShift > 0.0): | |
self.speedShift -= 1.0 / speed1StopFrames | |
if (self.speedShift < 0.0): | |
self.speedShift = 0.0 | |
elif (self.speedShift < 0.0): | |
self.speedShift += 1.0 / speed1RevFrames | |
if (self.speedShift > 0.0): | |
self.speedShift = 0.0 | |
if (self.allowBoost and self.speedShift >= 1.0): | |
self.speed = speed2 | |
elif (self.speedShift > 0.0): | |
self.speed = speed1 | |
elif (self.speedShift < 0.0): | |
self.speed = -speed1 | |
else: | |
self.speed = 0 | |
self.trailCounter += 1 | |
self.x += self.speed * math.cos(self.rotate*math.pi/4.0) | |
self.y += self.speed * math.sin(self.rotate*math.pi/4.0) | |
if (self.x < 3): | |
self.x = 3 | |
if (self.x > 69): | |
self.x = 69 | |
if (self.y < 3): | |
self.y = 3 | |
if (self.y > 37): | |
self.y = 37 | |
def draw(self): | |
if (self.allowBoost and self.speedShift >= 1.0 and ((self.trailCounter % trailInterval) == 0)): | |
self.sprTrails[self.trailNext].x = round(self.x) - 3 | |
self.sprTrails[self.trailNext].y = round(self.y) - 3 | |
self.trailDecay[self.trailNext] = trailDuration | |
self.trailNext = (self.trailNext + 1) % trailSpriteCount | |
for i in range(trailSpriteCount): | |
if (self.trailDecay[i] > 0): | |
self.trailDecay[i] -= 1 | |
self.sprTrails[i].setFrame((trailFrameCount * self.trailDecay[i]) // trailDuration) | |
thumby.display.drawSprite(self.sprTrails[i]) | |
if ((self.rotate % 2) == 1): | |
self.sprCar.setFrame(1) | |
elif ((self.rotate % 4) < 2): | |
self.sprCar.setFrame(2) | |
else: | |
self.sprCar.setFrame(0) | |
self.sprCar.mirrorX = 1 if (((self.rotate+5)%8)<3) else 0 #3,4,5 | |
self.sprCar.mirrorY = 1 if (((self.rotate+7)%8)<3) else 0 #1,2,3 | |
self.sprCar.x = round(self.x) - 4 | |
self.sprCar.y = round(self.y) - 4 | |
thumby.display.drawSprite(self.sprCar) | |
ballDrag = 0.9 | |
class Ball: | |
x = 0.0 | |
y = 0.0 | |
velX = 0.0 | |
velY = 0.0 | |
player = Car(bmpCarPlayer, bmpsCarPlayer) | |
if (gmLinked): | |
opponent = Car(bmpCarPlayer, bmpsCarPlayer) | |
else: | |
opponent = Car(bmpCarOpponent, bmpsCarOpponent) | |
ball = Ball() | |
if (gmLinked and link.mode == 1): | |
car1 = opponent | |
car2 = player | |
else: | |
car1 = player | |
car2 = opponent | |
scoreLeft = 0 | |
scoreRight = 0 | |
aiRotateInterval = [30,20,10][sdAi] if gmCPU else 0 | |
aiCanBoost = [False,True,True][sdAi] if gmCPU else True | |
aiCanReverse = [False,False,True][sdAi] if gmCPU else True | |
aiNextRotate = 0 | |
player.allowBoost = True | |
opponent.allowBoost = aiCanBoost | |
countdownFrames = 0 | |
countdownEnd = 20 * 3 + 10 | |
goal = False | |
goalFrame = 0 | |
goalEnd = 40 | |
#[idle,slow,fast,boost] | |
sfxCarHzVariation = 0.25 | |
sfxCarHzRange = [5.0,10.0,15.0,20.0] | |
sfxCarFreqRange = [150,150,200,250] | |
sfxCarDuration = 40 | |
sfxCarNextPlay = 0.0 | |
#[frequency,duration] | |
sfxCountdownBeep = [600,200] | |
sfxCarBump = [200,50] | |
sfxBallKick = [300,100] | |
sfxGoalBeep = [800,500] | |
scoreTabWait = 0 | |
scoreTabLerp = 1.0 | |
turnAssistRateSlow = 10 | |
turnAssistRateFast = 5 | |
turnAssistFrame = 0 | |
turnAssistL = False | |
turnAssistR = False | |
if (gmCredits): | |
creditNames = [ | |
"Demod", | |
"acedent", | |
"Adrian 2 Cool", | |
"AyreGuitar", | |
"hemlockmay", | |
"JasonTC", | |
"Laver:na", | |
"Mason W", | |
"Oliver2402", | |
"speedyink", | |
"SunnyChow", | |
"TacoTormentor", | |
"Timendus", | |
"transistortester", | |
";;; TurtleMoon ;;;", | |
"Unimatrix0", | |
"Vali", | |
"Windows Vista", | |
"Xyvir", | |
"THANK YOU FOR PLAYING ;"] | |
for i in range(1,len(creditNames)-2): | |
j = random.randrange(i+1,len(creditNames)-1) | |
creditNames[i], creditNames[j] = creditNames[j], creditNames[i] | |
# BITMAP: width: 50, height: 64 (5x7(8) letters - 0 to z) | |
bmpCreditLetters = bytearray([62,81,73,69,62,0,66,127,64,0,66,97,81,73,70,33,65,69,75,49,24,20,18,127,16,47,73,73,73,49,60,74,73,73,48,3,113,9,5,3,54,73,73,73,54,6,73,73,41,30, | |
56,84,86,85,24,12,18,36,18,12,0,0,12,12,0,0,2,0,4,0,2,32,0,8,0,0,4,0,18,0,0,48,52,0,0,124,18,17,18,124,127,73,73,73,54,62,65,65,65,34, | |
127,65,65,34,28,127,73,73,73,65,127,9,9,9,1,62,65,73,73,122,127,8,8,8,127,0,65,127,65,0,32,64,65,63,1,127,8,20,34,65,127,64,64,64,64,127,2,12,2,127, | |
127,4,8,16,127,62,65,65,65,62,127,9,9,9,6,62,65,81,33,94,127,9,25,41,70,70,73,73,73,49,1,1,127,1,1,63,64,64,64,63,31,32,64,32,31,63,64,56,64,63, | |
99,20,8,20,99,7,8,112,8,7,97,81,73,69,67,0,0,12,8,126,127,8,12,0,0,0,8,20,54,21,21,54,20,8,0,30,18,12,18,12,12,18,12,18,30,32,84,84,84,120, | |
127,72,68,68,56,56,68,68,68,40,56,68,68,72,127,56,84,84,84,24,8,126,9,1,2,8,84,84,84,60,127,8,4,4,120,0,68,125,64,0,32,64,68,61,0,0,127,16,40,68, | |
0,65,127,64,0,124,4,24,4,120,124,8,4,4,120,56,68,68,68,56,124,20,20,20,8,8,20,20,24,124,124,8,4,4,8,72,84,84,84,32,4,63,68,64,32,60,64,64,32,124, | |
28,32,64,32,28,60,64,32,64,60,68,40,16,40,68,12,80,80,80,60,68,100,84,76,68,0,0,1,0,0,0,0,64,8,0,1,0,0,0,4,0,24,28,28,0,64,0,0,1,0]) | |
# BITMAP: width: 30, height: 32 (3x4 letters - 0 to z) | |
bmpCreditLettersTiny = bytearray([239,249,143,113,239,112,253,153,251,249,157,255,247,148,255,251,153,245,255,157,253,241,81,255,255,189,111,251,155,159, | |
255,25,246,255,155,249,255,85,113,127,217,125,255,82,191,185,159,217,28,248,31,255,130,253,127,136,120,255,195,255, | |
253,162,237,227,174,163,237,175,251,239,233,143,239,89,15,143,233,239,255,41,239,15,217,15,143,217,15,244,74,174, | |
96,143,96,238,198,238,174,66,174,46,202,46,46,229,130,242,149,254,240,158,242,248,158,242,255,154,240,254,152,254]) | |
creditDebrisLetters = [12,13,14,15,16,75,76,77,78,79] | |
creditSpecialLetters = [43,45,47] | |
sprCreditDebris = [] | |
sprCreditLetters = [] | |
creditDebrisChance = 20 | |
creditSpecialChance = 5000 | |
creditsLettersX = 100 | |
creditsLettersY = 20 | |
creditsCarMinX = 26.0 | |
creditsCarMaxX = 46.0 | |
bmpCreditDebris = bytearray(5) | |
creditPosition = 0 | |
creditNextDebrisCheck = 0 | |
creditRevealedCount = 0 | |
creditFrame = 0 | |
def resetField(): | |
global countdownFrames | |
if (gmLinked): | |
random.seed(link.syncFrames) | |
if (gmTraining): | |
ball.x = 40.0 + 16*random.random() | |
ball.y = 8.0 + 32*random.random() | |
ball.velX = 0.0 | |
ball.velY = 0.0 | |
car1.x = 32.0 - 16*random.random() | |
car1.y = 8.0 + 32*random.random() | |
car1.rotate = 0 | |
car1.speedShift = 0.0 | |
countdownFrames = countdownEnd | |
elif (gmCredits): | |
car1.x = 36.0 | |
car1.y = 20.0 | |
car1.rotate = 0 | |
car1.speedShift = 0.0 | |
countdownFrames = 0 | |
else: # CPU or Linked | |
ball.x = 36.0 | |
ball.y = 20.0 | |
ball.velX = 0.0 | |
ball.velY = 0.0 | |
car1.x = 10.0 | |
car1.y = 20.0 + (random.random()-0.5) | |
car1.rotate = 0 | |
car1.speedShift = 0.0 | |
car2.x = 61.0 | |
car2.y = 20.0 + (random.random()-0.5) | |
car2.rotate = 4 | |
car2.speedShift = 0.0 | |
countdownFrames = 0 | |
resetField() | |
thumby.display.setFont("/lib/font5x7.bin", 5, 7, 1) | |
while(1): | |
### INPUTS | |
if (sdTurnAssist > 0): | |
if (sdTurnAssist == 1): | |
turnAssistRate = turnAssistRateSlow | |
elif (sdTurnAssist == 2): | |
turnAssistRate = turnAssistRateFast | |
if (thumby.buttonL.pressed()): | |
turnAssistL = (turnAssistFrame % turnAssistRate) == 0 | |
turnAssistR = False | |
turnAssistFrame += 1 | |
elif (thumby.buttonR.pressed()): | |
turnAssistR = (turnAssistFrame % turnAssistRate) == 0 | |
turnAssistL = False | |
turnAssistFrame += 1 | |
else: | |
turnAssistR = False | |
turnAssistL = False | |
turnAssistFrame = 0 | |
inputPacket = 0 | |
if (thumby.buttonL.justPressed() or turnAssistL): | |
inputPacket = inputPacket | 0x1 | |
player.rotate = (player.rotate + 7) % 8 | |
if (thumby.buttonR.justPressed() or turnAssistR): | |
inputPacket = inputPacket | 0x2 | |
player.rotate = (player.rotate + 1) % 8 | |
if (thumby.buttonA.pressed()): | |
inputPacket = inputPacket | 0x4 | |
player.gas = 1 | |
elif (thumby.buttonB.pressed()): | |
inputPacket = inputPacket | 0x8 | |
player.gas = -1 | |
else: | |
player.gas = 0 | |
if (gmLinked): | |
linkedInput = link.sync(inputPacket) | |
if (linkedInput&0x1): #buttonL just pressed | |
opponent.rotate = (opponent.rotate + 7) % 8 | |
if (linkedInput&0x2): #buttonR just pressed | |
opponent.rotate = (opponent.rotate + 1) % 8 | |
if (linkedInput&0x4): #buttonA | |
opponent.gas = 1 | |
elif (linkedInput&0x8): #buttonB | |
opponent.gas = -1 | |
else: | |
opponent.gas = 0 | |
### OPPONENT AI | |
if (gmCPU): | |
# Find target | |
if (ball.x < 36): | |
ballGoalDir = math.atan2(20-ball.y,-4-ball.x) | |
aiTargetX = ball.x - 5 * math.cos(ballGoalDir) | |
aiTargetY = ball.y - 5 * math.sin(ballGoalDir) | |
else: | |
if (opponent.x < ball.x): | |
if (opponent.y < 20): | |
aiTargetX = ball.x + 4 | |
aiTargetY = ball.y + 8 | |
else: | |
aiTargetX = ball.x + 4 | |
aiTargetY = ball.y - 8 | |
else: | |
ballGoalDir = math.atan2(20-ball.y,75-ball.x) | |
aiTargetX = ball.x + 5 * math.cos(ballGoalDir) | |
aiTargetY = ball.y + 5 * math.sin(ballGoalDir) | |
aiTargetDir = math.atan2(aiTargetY-opponent.y,aiTargetX-opponent.x) | |
aiTargetRotate = round((4 * aiTargetDir) / math.pi) | |
aiRotateDelta = (aiTargetRotate - opponent.rotate) | |
if (aiRotateDelta > 4): | |
aiRotateDelta -= 8 | |
if (aiRotateDelta < -4): | |
aiRotateDelta += 8 | |
# Rotate towards target | |
if (aiNextRotate > 0): | |
aiNextRotate -= 1 | |
else: | |
aiNextRotate = aiRotateInterval | |
if (aiRotateDelta > 0): | |
opponent.rotate = (opponent.rotate + 1) % 8 | |
if (aiRotateDelta < 0): | |
opponent.rotate = (opponent.rotate + 7) % 8 | |
# Move if we are mostly aligned with target | |
if (abs(aiRotateDelta) < 2): | |
opponent.gas = 1 | |
elif (aiCanReverse and abs(aiRotateDelta) > 2): | |
opponent.gas = -1 | |
else: | |
opponent.gas = 0 | |
if (countdownFrames < countdownEnd): | |
player.gas = 0 | |
opponent.gas = 0 | |
### PHYSICS | |
if (oppExist): | |
# Car Collision | |
carCarDist = math.sqrt((car2.y-car1.y)**2+(car2.x-car1.x)**2) | |
if (carCarDist<7): | |
carCarDir = math.atan2(car2.y-car1.y,car2.x-car1.x) | |
carCarDx = (7-carCarDist) * math.cos(carCarDir) | |
carCarDy = (7-carCarDist) * math.sin(carCarDir) | |
car1.x -= carCarDx | |
car1.y -= carCarDy | |
car2.x += carCarDx | |
car2.y += carCarDy | |
if (not goal): | |
thumbyAudio.audio.play(sfxCarBump[0],sfxCarBump[1]) | |
car1.physicsUpdate() | |
if (oppExist): | |
car2.physicsUpdate() | |
if (ballExist): | |
ballKick = False | |
ballPlayerDist = math.sqrt((ball.y-car1.y)**2+(ball.x-car1.x)**2) | |
if (ballPlayerDist < 6): | |
ballKick = True | |
ballKickX = car1.x | |
ballKickY = car1.y | |
ballKickSpeed = car1.speed * 1.5 | |
ballPlayerDir = math.atan2(car1.y-ball.y,car1.x-ball.x) | |
car1.x += min(1,(6-ballPlayerDist)) * math.cos(ballPlayerDir) | |
car1.y += min(1,(6-ballPlayerDist)) * math.sin(ballPlayerDir) | |
if (oppExist): | |
ballOpponentDist = math.sqrt((ball.y-car2.y)**2+(ball.x-car2.x)**2) | |
if (ballOpponentDist < 6): | |
if (ballKick): | |
ballKickX = (ballKickX + car2.x)/2 | |
ballKickY = (ballKickY + car2.y)/2 | |
ballKickSpeed = max(ballKickSpeed, car2.speed * 1.5) | |
else: | |
ballKick = True | |
ballKickX = car2.x | |
ballKickY = car2.y | |
ballKickSpeed = car2.speed * 1.5 | |
ballOpponentDir = math.atan2(car2.y-ball.y,car2.x-ball.x) | |
car2.x += min(1,(6-ballOpponentDist)) * math.cos(ballOpponentDir) | |
car2.y += min(1,(6-ballOpponentDist)) * math.sin(ballOpponentDir) | |
if (ballKick): | |
kickDir = math.atan2(ball.y-ballKickY,ball.x-ballKickX) | |
ball.velX = ballKickSpeed * math.cos(kickDir) | |
ball.velY = ballKickSpeed * math.sin(kickDir) | |
if (not goal): | |
thumbyAudio.audio.play(sfxBallKick[0],sfxBallKick[1]) | |
if (ball.y < 12 or ball.y > 28): | |
if (ball.x < 4): | |
ball.x = 4 | |
ball.velX = abs(ball.velX) | |
if (ball.x > 68): | |
ball.x = 68 | |
ball.velX = -abs(ball.velX) | |
if (ball.y < 4): | |
ball.y = 4 | |
ball.velY = abs(ball.velY) | |
if (ball.y > 36): | |
ball.y = 36 | |
ball.velY = -abs(ball.velY) | |
else: | |
if (math.sqrt((ball.y-12)**2+(ball.x)**2) < 4): | |
ball.velX = abs(ball.velX) | |
ball.velY = abs(ball.velY) | |
if (math.sqrt((ball.y-28)**2+(ball.y)**2) < 4): | |
ball.velX = abs(ball.velX) | |
ball.velY = -abs(ball.velY) | |
if (math.sqrt((ball.y-12)**2+(ball.x-71)**2) < 4): | |
ball.velX = -abs(ball.velX) | |
ball.velY = abs(ball.velY) | |
if (math.sqrt((ball.y-28)**2+(ball.x-71)**2) < 4): | |
ball.velX = -abs(ball.velX) | |
ball.velY = -abs(ball.velY) | |
if (ball.x <= -1): | |
ball.x = -1 | |
ball.velX = 0 | |
if (ball.x >= 72): | |
ball.x = 72 | |
ball.velX = 0 | |
ball.x += ball.velX | |
ball.y += ball.velY | |
ball.velX *= ballDrag | |
ball.velY *= ballDrag | |
### CREDITS | |
if (gmCredits): | |
creditFrame += 1 | |
creditsShift = 0 | |
if (player.x < creditsCarMinX and creditPosition > 0): | |
creditsShift = round(creditsCarMinX - player.x) | |
if (player.x > creditsCarMaxX and creditPosition < len(creditNames)*200 + 100): | |
creditsShift = round(creditsCarMaxX - player.x) | |
creditPosition -= creditsShift | |
player.x += creditsShift | |
for spr in sprCreditDebris: | |
spr.x += creditsShift | |
for spr in sprCreditLetters: | |
spr.x += creditsShift | |
for spr in player.sprTrails: | |
spr.x += creditsShift | |
for spr in sprCreditLetters: | |
if (not spr.revealed and max(abs(player.x-(spr.x+1)),abs(player.y-(spr.y+1))) < 7): | |
spr.revealed = True | |
spr.x -= 1 | |
spr.y -= 1 | |
spr.width = 5 | |
spr.height = 7 | |
spr.bitmap = spr.revealBitmap | |
creditRevealedCount += 1 | |
if (creditRevealedCount == len(sprCreditLetters)): | |
for spr2 in sprCreditLetters: | |
spr2.origY = spr2.y | |
if (creditRevealedCount == len(sprCreditLetters)): | |
spr.y = round(spr.origY + 2*math.sin((creditPosition + creditFrame + spr.x)/10.0)) | |
if (len(sprCreditDebris) > 0): | |
creditNextDebrisCheck = (creditNextDebrisCheck + 1) % len(sprCreditDebris) | |
spr = sprCreditDebris[creditNextDebrisCheck] | |
if (spr.x + spr.width < -1 or spr.x > 73): | |
sprCreditDebris.remove(spr) | |
if (creditsShift > 0): #Car moving left | |
for i in range(creditsShift): | |
spawnX = -5 + i | |
if (random.randrange(creditDebrisChance) == 0): | |
pick = random.choice(creditDebrisLetters) | |
bmp = bmpCreditLetters[pick*5:pick*5+5] | |
if (sdGrayscale): | |
bmp = [bmpCreditDebris,bmp] | |
sprCreditDebris.append(thumby.Sprite(5,7,bmp, | |
x=spawnX, | |
y=random.randrange(33), | |
key=0, | |
mirrorX=random.getrandbits(1), | |
mirrorY=random.getrandbits(1))) | |
spawnX = -10 + i | |
if (random.randrange(creditSpecialChance) == 0): | |
pick = random.choice(creditDebrisLetters) | |
bmp = bmpCreditLetters[pick*5:pick*5+10] | |
if (sdGrayscale): | |
bmp = [bmp,bmp] | |
sprCreditDebris.append(thumby.Sprite(10,7,bmp, | |
x=spawnX, | |
y=random.randrange(33), | |
key=0)) | |
if (creditsShift < 0): #Car moving right | |
for i in range(-creditsShift): | |
if ((creditPosition-i) % 200 == 100): | |
creditNameIndex = (creditPosition-i) // 200 | |
if (creditNameIndex >= 0 and creditNameIndex < len(creditNames)): | |
sprCreditLetters.clear() | |
creditRevealedCount = 0 | |
creditName = creditNames[creditNameIndex] | |
offset = 0 | |
nameY = 5 + random.randrange(33-10) | |
if (creditNameIndex == 0 or creditNameIndex == len(creditNames)-1): | |
nameY = 20 - 3 | |
for letter in creditName: | |
if (letter.isspace()): | |
offset += 6 | |
continue | |
pick = ord(letter) - 48 | |
col = pick % 10 | |
row = pick // 20 | |
bank = (pick % 20) // 10 | |
bmpOffset = (row * 10 + col) * 3 | |
bmpTiny = bmpCreditLettersTiny[bmpOffset:bmpOffset+3] | |
for j in range(len(bmpTiny)): | |
if (bank == 1): | |
bmpTiny[j] = bmpTiny[j] >> 4 | |
bmp = bmpCreditLetters[pick*5:pick*5+5] | |
spr = thumby.Sprite(3,4,bmpTiny, | |
x=72+offset+1,y=nameY+1, | |
key=0) | |
spr.revealBitmap = bmp | |
spr.revealed = False | |
sprCreditLetters.append(spr) | |
offset += 6 | |
spawnX = 72 - i | |
if (random.randrange(creditDebrisChance) == 0): | |
pick = random.choice(creditDebrisLetters) | |
bmp = bmpCreditLetters[pick*5:pick*5+5] | |
if (sdGrayscale): | |
bmp = [bmpCreditDebris,bmp] | |
sprCreditDebris.append(thumby.Sprite(5,7,bmp, | |
x=spawnX, | |
y=random.randrange(33), | |
key=0, | |
mirrorX=random.getrandbits(1), | |
mirrorY=random.getrandbits(1))) | |
if (random.randrange(creditSpecialChance) == 0): | |
pick = random.choice(creditSpecialLetters) | |
bmp = bmpCreditLetters[pick*5:pick*5+10] | |
if (sdGrayscale): | |
bmp = [bmp,bmp] | |
sprCreditDebris.append(thumby.Sprite(10,7,bmp, | |
x=spawnX, | |
y=random.randrange(33), | |
key=0)) | |
### SCORING | |
if (ballExist and not goal): | |
if (ball.x <= -1 or ball.x >= 72): | |
if (ball.x < 36): | |
scoreRight += 1 | |
goalDirection = 1 | |
if (gmCPU): | |
goalText = "CPU Scored!" | |
else: | |
goalText = "P2 Scored!" | |
else: | |
scoreLeft += 1 | |
goalDirection = -1 | |
goalText = "P1 Scored!" | |
if (gmTraining): | |
resetField() | |
else: | |
goal = True | |
goalFrame = 0 | |
thumbyAudio.audio.play(sfxGoalBeep[0],sfxGoalBeep[1]) | |
### SOUND | |
if (sdSound): | |
if (player.speedShift == 0.0): #Idle | |
sfxCarHz = sfxCarHzRange[0] | |
sfxCarFreq = sfxCarFreqRange[0] | |
elif (player.speedShift == 1.0): #Boost | |
sfxCarHz = sfxCarHzRange[3] | |
sfxCarFreq = sfxCarFreqRange[3] | |
else: | |
sfxCarLerp = player.speedShift | |
sfxCarHz = round(lerp(sfxCarHzRange[1],sfxCarHzRange[2],sfxCarLerp)) | |
sfxCarFreq = round(lerp(sfxCarFreqRange[1],sfxCarFreqRange[2],sfxCarLerp)) | |
if (thumbyAudio.audio.pwm.duty_u16() == 0): #Not playing anything | |
if (sfxCarNextPlay <= 0.0): | |
variation = random.uniform(1.0-sfxCarHzVariation,1.0+sfxCarHzVariation) | |
thumbyAudio.audio.play(round(variation * sfxCarFreq), sfxCarDuration) | |
sfxCarNextPlay += variation / sfxCarHz | |
sfxCarNextPlay -= 0.0333 # 1/30 | |
### DISPLAY | |
thumby.display.fill(0) | |
if (ballExist): | |
ballRoundX = round(ball.x) | |
ballRoundY = round(ball.y) | |
sprBallTex.x = ballRoundX - 9 + (ballRoundX % 6) | |
sprBallTex.y = ballRoundY - 9 + (ballRoundY % 6) | |
thumby.display.drawSprite(sprBallTex) | |
sprBallVoid.x = ballRoundX - 9 | |
sprBallVoid.y = ballRoundY - 9 | |
thumby.display.drawSprite(sprBallVoid) | |
sprBallOutline.x = ballRoundX - 4 | |
sprBallOutline.y = ballRoundY - 4 | |
thumby.display.drawSprite(sprBallOutline) | |
#thumby.display.blit(bmpTrails[1], round(aiTargetX)-2,round(aiTargetY)-2,4,4,0,0,0); | |
if (gmCredits): | |
for spr in sprCreditDebris: | |
thumby.display.drawSprite(spr) | |
for spr in sprCreditLetters: | |
thumby.display.drawSprite(spr) | |
if (oppExist): | |
car2.draw() | |
car1.draw() | |
if (goalExist): | |
sprGoal.x = 0 | |
sprGoal.y = 12 | |
sprGoal.mirrorX = 0 | |
thumby.display.drawSprite(sprGoal) | |
sprGoal.x = 68 | |
sprGoal.y = 12 | |
sprGoal.mirrorX = 1 | |
thumby.display.drawSprite(sprGoal) | |
if (scoreExist): | |
if (oppExist): | |
minCarBallY = min(car1.y,car2.y,ball.y) | |
else: | |
minCarBallY = min(car1.y,ball.y) | |
if (minCarBallY < 10): | |
scoreTabWait = 30 | |
scoreTabLerp = max(0,scoreTabLerp-0.1) | |
else: | |
if (scoreTabWait > 0): | |
scoreTabWait -= 1 | |
else: | |
scoreTabLerp = min(1,scoreTabLerp+0.1) | |
if (sdGrayscale): | |
scoreTabY = round(lerp(-7,0,scoreTabLerp)) | |
else: | |
scoreTabY = round(lerp(-8,0,scoreTabLerp)) | |
thumby.display.blit(bmpScoreTab,14,scoreTabY,15,8,0,0,0) | |
thumby.display.blit(bmpScoreTab,43,scoreTabY,15,8,0,0,0) | |
sprDigit.y = scoreTabY + 1 | |
sprDigit.x = 18 | |
sprDigit.bitmap = bmpDigits[(scoreLeft//10) % 10] | |
thumby.display.drawSprite(sprDigit) | |
sprDigit.x = 22 | |
sprDigit.bitmap = bmpDigits[scoreLeft % 10] | |
thumby.display.drawSprite(sprDigit) | |
sprDigit.x = 47 | |
sprDigit.bitmap = bmpDigits[(scoreRight//10) % 10] | |
thumby.display.drawSprite(sprDigit) | |
sprDigit.x = 51 | |
sprDigit.bitmap = bmpDigits[scoreRight % 10] | |
thumby.display.drawSprite(sprDigit) | |
if (goal): | |
x = round(20*math.tan(goalDirection*((goalFrame+1)/(goalEnd+2) - 0.5)*math.pi)) | |
thumby.display.drawFilledRectangle(round(x*1.5),20-5,72,10,2) | |
thumby.display.drawText(goalText,x+37-3*len(goalText),20-3,0) | |
thumby.display.drawText(goalText,x+36-3*len(goalText),20-4,1) | |
if (goalFrame == goalEnd//2): | |
resetField() | |
if (goalFrame < goalEnd): | |
goalFrame += 1 | |
else: | |
goal = False | |
if (countdownFrames < countdownEnd - 10): | |
bmpCountdownAnim[2] = bmpCountdownNumber[countdownFrames//20] | |
bmpCountdownAnim[3] = bmpCountdownAnim[2] | |
if (countdownFrames < 10): | |
countdownY = -16 + 2*countdownFrames | |
elif (countdownFrames >= 75): | |
countdownY = 0 - (2*countdownFrames-75) | |
else: | |
countdownY = 0 | |
if ((countdownFrames % 20)==9): | |
thumbyAudio.audio.play(sfxCountdownBeep[0],sfxCountdownBeep[1]) | |
thumby.display.blit(bmpCountdownAnim[(countdownFrames//4)%5], 28, countdownY, 16, 16, 0, 0, 0) | |
if (oppExist and (countdownFrames % 10) < 5): | |
thumby.display.blit(bmpCountdownArrow, round(player.x) - 4, round(player.y) - 12, 8, 8, 0, 0, 0) | |
if (countdownFrames < countdownEnd): | |
countdownFrames += 1 | |
thumby.display.update() | |
except Exception as e: | |
f = open("/crash.log", "w") | |
f.write(str(e)) | |
f.close() | |
raise e |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# Thumby grayscale library | |
# https://github.com/Timendus/thumby-grayscale | |
# | |
# This program is free software: you can redistribute it and/or modify | |
# it under the terms of the GNU General Public License as published by | |
# the Free Software Foundation, either version 3 of the License, or | |
# (at your option) any later version. | |
# | |
# This program is distributed in the hope that it will be useful, | |
# but WITHOUT ANY WARRANTY; without even the implied warranty of | |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
# GNU General Public License for more details. | |
# | |
# You should have received a copy of the GNU General Public License | |
# along with this program. If not, see <https://www.gnu.org/licenses/>. | |
from utime import sleep_ms, ticks_diff, ticks_ms, sleep_us | |
from machine import Pin, SPI, idle, mem32 | |
import _thread | |
from os import stat | |
from math import sqrt, floor | |
from array import array | |
from thumbyButton import buttonA, buttonB, buttonU, buttonD, buttonL, buttonR | |
from sys import modules | |
__version__ = '3.0.0' | |
emulator = None | |
try: | |
import emulator | |
except ImportError: | |
pass | |
class Sprite: | |
@micropython.native | |
def __init__(self, width, height, bitmapData, x = 0, y=0, key=-1, mirrorX=False, mirrorY=False): | |
self.width = width | |
self.height = height | |
self.bitmapSource = bitmapData | |
self.bitmapByteCount = width*(height//8) | |
if(height%8): | |
self.bitmapByteCount+=width | |
self.frameCount = 1 | |
self.currentFrame = 0 | |
self._shaded = False | |
self._usesFile = False | |
if isinstance(bitmapData, (tuple, list)): | |
if (len(bitmapData) != 2) or (type(bitmapData[0]) != type(bitmapData[1])): | |
raise ValueError('bitmapData must be a bytearray, string, or tuple of two bytearrays or strings') | |
self._shaded = True | |
if isinstance(bitmapData[0], str): | |
self._usesFile = True | |
if stat(bitmapData[0])[6] != stat(bitmapData[1])[6]: | |
raise ValueError('Sprite files must match in size') | |
self.bitmap = (bytearray(self.bitmapByteCount), bytearray(self.bitmapByteCount)) | |
self.files = (open(bitmapData[0],'rb'),open(bitmapData[1],'rb')) | |
self.files[0].readinto(self.bitmap[0]) | |
self.files[1].readinto(self.bitmap[1]) | |
self.frameCount = stat(bitmapData[0])[6] // self.bitmapByteCount | |
elif isinstance(bitmapData[0], bytearray): | |
if len(bitmapData[0]) != len(bitmapData[1]): | |
raise ValueError('Sprite bitplanes must match in size') | |
self.frameCount = len(bitmapData[0]) // self.bitmapByteCount | |
self.bitmap = [ | |
memoryview(bitmapData[0])[0:self.bitmapByteCount], | |
memoryview(bitmapData[1])[0:self.bitmapByteCount] | |
] | |
else: | |
raise ValueError('bitmapData must be a bytearray, string, or tuple of two bytearrays or strings') | |
elif isinstance(bitmapData, str): | |
self._usesFile = True | |
self.bitmap = bytearray(self.bitmapByteCount) | |
self.file = open(bitmapData,'rb') | |
self.file.readinto(self.bitmap) | |
self.frameCount = stat(bitmapData)[6] // self.bitmapByteCount | |
elif isinstance(bitmapData, bytearray): | |
self.bitmap = memoryview(bitmapData)[0:self.bitmapByteCount] | |
self.frameCount = len(bitmapData) // self.bitmapByteCount | |
else: | |
raise ValueError('bitmapData must be a bytearray, string, or tuple of two bytearrays or strings') | |
self.x = x | |
self.y = y | |
self.key = key | |
self.mirrorX = mirrorX | |
self.mirrorY = mirrorY | |
@micropython.native | |
def getFrame(self): | |
return self.currentFrame | |
@micropython.native | |
def setFrame(self, frame): | |
if(frame >= 0 and (self.currentFrame is not frame % (self.frameCount))): | |
self.currentFrame = frame % (self.frameCount) | |
offset=self.bitmapByteCount*self.currentFrame | |
if self._shaded: | |
if self._usesFile: | |
self.files[0].seek(offset) | |
self.files[1].seek(offset) | |
self.files[0].readinto(self.bitmap[0]) | |
self.files[1].readinto(self.bitmap[1]) | |
else: | |
self.bitmap[0] = memoryview(self.bitmapSource[0])[offset:offset+self.bitmapByteCount] | |
self.bitmap[1] = memoryview(self.bitmapSource[1])[offset:offset+self.bitmapByteCount] | |
else: | |
if self._usesFile: | |
self.file.seek(offset) | |
self.file.readinto(self.bitmap) | |
else: | |
self.bitmap = memoryview(self.bitmapSource)[offset:offset+self.bitmapByteCount] | |
# The times below are calculated using phase 1 and phase 2 pre-charge | |
# periods of 1 clock. | |
# Note that although the SSD1306 datasheet doesn't state it, the 50 | |
# clocks period per row _is_ a constant (datasheets for similar | |
# controllers from the same manufacturer state this). | |
# 530kHz is taken to be the highest nominal clock frequency. The | |
# calculations shown provide the value in seconds, which can be | |
# multiplied by 1e6 to provide a microsecond value. | |
_PRE_FRAME_TIME_US = const( 883) # 9 rows: ( 9*(1+1+50)) / 530e3 seconds | |
_FRAME_TIME_US = const(4709) # 48 rows: (49*(1+1+50)) / 530e3 seconds | |
# Thread state variables for managing the Grayscale Thread | |
_THREAD_STOPPED = const(0) | |
_THREAD_STARTING = const(1) | |
_THREAD_RUNNING = const(2) | |
_THREAD_STOPPING = const(3) | |
# Indexes into the multipurpose state array, accessing a particular status | |
_ST_THREAD = const(0) | |
_ST_COPY_BUFFS = const(1) | |
_ST_PENDING_CMD = const(2) | |
_ST_CONTRAST = const(3) | |
_ST_INVERT = const(4) | |
# Screen display size constants | |
_WIDTH = const(72) | |
_HEIGHT = const(40) | |
_BUFF_SIZE = const((_HEIGHT // 8) * _WIDTH) | |
_BUFF_INT_SIZE = const(_BUFF_SIZE // 4) | |
class Grayscale: | |
# BLACK and WHITE is 0 and 1 to be compatible with the standard Thumby API | |
BLACK = 0 | |
WHITE = 1 | |
DARKGRAY = 2 | |
LIGHTGRAY = 3 | |
def __init__(self): | |
self._spi = SPI(0, sck=Pin(18), mosi=Pin(19)) | |
self._dc = Pin(17) | |
self._cs = Pin(16) | |
self._res = Pin(20) | |
self._spi.init(baudrate=100 * 1000 * 1000, polarity=0, phase=0) | |
self._res.init(Pin.OUT, value=1) | |
self._dc.init(Pin.OUT, value=0) | |
self._cs.init(Pin.OUT, value=1) | |
self._display_initialised = False | |
self.display = self # This acts as both the GraphicsClass and SSD1306 | |
self.width = _WIDTH | |
self.height = _HEIGHT | |
self.max_x = _WIDTH - 1 | |
self.max_y = _HEIGHT - 1 | |
self.pages = self.height // 8 | |
# Draw buffers. | |
# This comprises of two full buffer lengths. | |
# The first section contains black and white compatible | |
# with the display buffer from the standard Thumby API, | |
# and the second contains the shading to create | |
# offwhite (lightgray) or offblack (darkgray). | |
self.drawBuffer = bytearray(_BUFF_SIZE*2) | |
# The base "buffer" matches compatibility with the std Thumby API. | |
self.buffer = memoryview(self.drawBuffer)[:_BUFF_SIZE] | |
# The "shading" buffer adds the grayscale | |
self.shading = memoryview(self.drawBuffer)[_BUFF_SIZE:] | |
self._subframes = array('O', [bytearray(_BUFF_SIZE), | |
bytearray(_BUFF_SIZE), bytearray(_BUFF_SIZE)]) | |
if 'thumbyGraphics' in modules: | |
self.buffer[:] = modules['thumbyGraphics'].display.display.buffer | |
# The method used to create reduced flicker greyscale using the SSD1306 | |
# uses certain assumptions about the internal behaviour of the | |
# controller. Even though the behaviour seems to back up those | |
# assumptions, it is possible that the assumptions are incorrect but the | |
# desired result is achieved anyway. To simplify things, the following | |
# comments are written as if the assumptions _are_ correct. | |
# We will keep the display synchronised by resetting the row counter | |
# before each frame and then outputting a frame of 57 rows. This is 17 | |
# rows past the 40 of the actual display. | |
# Prior to loading in the frame we park the row counter at row 0 and | |
# wait for the nominal time for 8 rows to be output. This (hopefully) | |
# provides enough time for the row counter to reach row 0 before it | |
# sticks there. (Note: recent test indicate that perhaps the current row | |
# actually jumps before parking) | |
# The 'parking' is done by setting the number of rows (aka 'multiplex | |
# ratio') to 1 row. This is an invalid setting according to the datasheet | |
# but seems to still have the desired effect. | |
# 0xa8,0 Set multiplex ratio to 1 | |
# 0xd3,52 Set display offset to 52 | |
self._preFrameCmds = bytearray([0xa8,0, 0xd3,52]) | |
# Once the frame has been loaded into the display controller's GDRAM, we | |
# set the controller to output 57 rows, and then delay for the nominal | |
# time for 48 rows to be output. | |
# Considering the 17 row 'buffer space' after the real 40 rows, that puts | |
# us around halfway between the end of the display, and the row at which | |
# it would wrap around. | |
# By having 8.5 rows either side of the nominal timing, we can absorb any | |
# variation in the frequency of the display controller's RC oscillator as | |
# well as any timing offsets introduced by the Python code. | |
# 0xd3,x Set display offset. Since rows are scanned in reverse, the | |
# calculation must work backwards from the last controller row. | |
# 0xa8,57-1 Set multiplex ratio to 57 | |
self._postFrameCmds = bytearray([0xd3,_HEIGHT+(64-57), 0xa8,57-1]) | |
# We enhance the greys by modulating the contrast. | |
# 0x81,<val> Set Bank0 contrast value to <val> | |
# Use setting from thumby.cfg | |
self._brightness = 127 | |
try: | |
with open("thumby.cfg", "r") as fh: | |
_, _, conf = fh.read().partition("brightness,") | |
b = int(conf.split(',')[0]) | |
# Set to the relevant brightness level | |
if b == 0: self._brightness = 0 | |
if b == 1: self._brightness = 28 | |
# Otherwise, leave it at 127 | |
except (OSError, ValueError): | |
pass | |
self._postFrameAdj = array('O', [bytearray([0x81,0]) for _ in range(3)]) | |
self._postFrameAdjSrc = bytearray(3) | |
# It's better to avoid using regular variables for thread sychronisation. | |
# Instead, elements of an array/bytearray should be used. | |
# We're using a uint32 array here, as that should hopefully further ensure | |
# the atomicity of any element accesses. | |
# [thread_state, buff_copy_gate, pending_cmd_gate, constrast_change, inverted] | |
self._state = array('I', [_THREAD_STOPPED,0,0,0,0]) | |
self._pendingCmds = bytearray(8) | |
self.setFont('lib/font5x7.bin', 5, 7, 1) | |
self.lastUpdateEnd = 0 | |
self.frameRate = 0 | |
self.brightness(self._brightness) | |
self._initEmuScreen() | |
_thread.stack_size(2048) # minimum stack size for RP2040 micropython port | |
# allow use of 'with' | |
def __enter__(self): | |
self.enableGrayscale() | |
return self | |
def __exit__(self, type, value, traceback): | |
self.disableGrayscale() | |
@micropython.viper | |
def _initEmuScreen(self): | |
if not emulator: | |
return | |
# Register draw buffer with emulator | |
Pin(2, Pin.OUT) # Ready display handshake pin | |
emulator.screen_breakpoint(ptr16(self.drawBuffer)) | |
self._clearEmuFunctions() | |
def _clearEmuFunctions(self): | |
# Disable device controller functions | |
def _disabled(*arg, **kwdarg): | |
pass | |
self.invert = _disabled | |
self.reset = _disabled | |
self.poweron = _disabled | |
self.poweroff = _disabled | |
self.init_display = _disabled | |
self.write_cmd = _disabled | |
def reset(self): | |
self._res(1) | |
sleep_ms(1) | |
self._res(0) | |
sleep_ms(10) | |
self._res(1) | |
sleep_ms(10) | |
def init_display(self): | |
self._dc(0) | |
if self._display_initialised: | |
if self._state[_ST_THREAD] == _THREAD_STOPPED: | |
# (Re)Initialise the display for monocrhome timings | |
# 0xa8,0 Set multiplex ratio to 0 (pausing updates) | |
# 0xd3,52 Set display offset to 52 | |
self._spi.write(bytearray([0xa8,0, 0xd3,52])) | |
sleep_us(_FRAME_TIME_US*3) | |
# 0xa8,39 Set multiplex ratio to height (releasing updates) | |
# 0xd3,0 Set display offset to 0 | |
self._spi.write(bytearray([0xa8,_HEIGHT-1,0xd3,0])) | |
if self._state[_ST_INVERT]: | |
self._spi.write(bytearray([0xa6 | 1])) # Resume device color inversion | |
else: | |
# Initialise the display for grayscale timings | |
# 0xae Display Off | |
# 0xa8,0 Set multiplex ratio to 0 (will be changed later) | |
# 0xd3,0 Set display offset to 0 (will be changed later) | |
# 0xaf Set display on | |
self._spi.write(bytearray([0xae, 0xa8,0, 0xd3,0, 0xaf])) | |
return | |
self.reset() | |
self._cs(0) | |
# initialise as usual, except with shortest pre-charge | |
# periods and highest clock frequency | |
# 0xae Display Off | |
# 0x20,0x00 Set horizontal addressing mode | |
# 0x40 Set display start line to 0 | |
# 0xa1 Set segment remap mode 1 | |
# 0xa8,63 Set multiplex ratio to 64 (will be changed later) | |
# 0xc8 Set COM output scan direction 1 | |
# 0xd3,54 Set display offset to 0 (will be changed later) | |
# 0xda,0x12 Set COM pins hardware configuration: alternative config, | |
# disable left/right remap | |
# 0xd5,0xf0 Set clk div ratio = 1, and osc freq = ~370kHz | |
# 0xd9,0x11 Set pre-charge periods: phase 1 = 1 , phase 2 = 1 | |
# 0xdb,0x20 Set Vcomh deselect level = 0.77 x Vcc | |
# 0x81,0x7f Set Bank0 contrast to 127 (will be changed later) | |
# 0xa4 Do not enable entire display (i.e. use GDRAM) | |
# 0xa6 Normal (not inverse) display | |
# 0x8d,0x14 Charge bump setting: enable charge pump during display on | |
# 0xad,0x30 Select internal 30uA Iref (max Iseg=240uA) during display on | |
# 0xf Set display on | |
self._spi.write(bytearray([ | |
0xae, 0x20,0x00, 0x40, 0xa1, 0xa8,63, 0xc8, 0xd3,0, 0xda,0x12, | |
0xd5,0xf0, 0xd9,0x11, 0xdb,0x20, 0x81,0x7f, | |
0xa4, 0xa6, 0x8d,0x14, 0xad,0x30, 0xaf])) | |
self._dc(1) | |
# clear the entire GDRAM | |
zero32 = bytearray([0] * 32) | |
for _ in range(32): | |
self._spi.write(zero32) | |
self._dc(0) | |
# set the GDRAM window | |
# 0x21,28,99 Set column start (28) and end (99) addresses | |
# 0x22,0,4 Set page start (0) and end (4) addresses0 | |
self._spi.write(bytearray([0x21,28,99, 0x22,0,4])) | |
self._display_initialised = True | |
def enableGrayscale(self): | |
if emulator: | |
# Activate grayscale emulation | |
emulator.screen_breakpoint(1) | |
self.show() | |
return | |
if self._state[_ST_THREAD] == _THREAD_RUNNING: | |
return | |
self._state[_ST_THREAD] = _THREAD_STARTING | |
self.init_display() | |
_thread.start_new_thread(self._display_thread, ()) | |
# Wait for the thread to successfully settle into a running state | |
while self._state[_ST_THREAD] != _THREAD_RUNNING: | |
idle() | |
def disableGrayscale(self): | |
if emulator: | |
# Disable grayscale emulation | |
emulator.screen_breakpoint(0) | |
self.show() | |
return | |
if self._state[_ST_THREAD] != _THREAD_RUNNING: | |
return | |
self._state[_ST_THREAD] = _THREAD_STOPPING | |
while self._state[_ST_THREAD] != _THREAD_STOPPED: | |
idle() | |
# Refresh the image to the B/W form | |
self.init_display() | |
self.show() | |
# Change back to the original (unmodulated) brightness setting | |
self.brightness(self._brightness) | |
@micropython.native | |
def write_cmd(self, cmd): | |
if isinstance(cmd, list): | |
cmd = bytearray(cmd) | |
elif not isinstance(cmd, bytearray): | |
cmd = bytearray([cmd]) | |
if self._state[_ST_THREAD] == _THREAD_RUNNING: | |
pendingCmds = self._pendingCmds | |
if len(cmd) > len(pendingCmds): | |
# We can't just break up the longer list of commands automatically, as we | |
# might end up separating a command and its parameter(s). | |
raise ValueError('Cannot send more than %u bytes using write_cmd()' % len(pendingCmds)) | |
i = 0 | |
while i < len(cmd): | |
pendingCmds[i] = cmd[i] | |
i += 1 | |
# Fill the rest of the bytearray with display controller NOPs | |
# This is probably better than having to create slice or a memoryview in the GPU thread | |
while i < len(pendingCmds): | |
pendingCmds[i] = 0x3e | |
i += 1 | |
self._state[_ST_PENDING_CMD] = 1 | |
while self._state[_ST_PENDING_CMD]: | |
idle() | |
else: | |
self._dc(0) | |
self._spi.write(cmd) | |
def poweroff(self): | |
self.write_cmd(0xae) | |
def poweron(self): | |
self.write_cmd(0xaf) | |
@micropython.viper | |
def invert(self, invert:int): | |
state = ptr32(self._state) | |
invert = 1 if invert else 0 | |
state[_ST_INVERT] = invert | |
state[_ST_COPY_BUFFS] = 1 | |
if state[_ST_THREAD] != _THREAD_RUNNING: | |
self.write_cmd(0xa6 | invert) | |
@micropython.viper | |
def show(self): | |
state = ptr32(self._state) | |
if state[_ST_THREAD] == _THREAD_RUNNING: | |
state[_ST_COPY_BUFFS] = 1 | |
while state[_ST_COPY_BUFFS] != 0: | |
idle() | |
elif emulator: | |
mem32[0xD0000000+0x01C] = 1 << 2 | |
else: | |
self._dc(1) | |
self._spi.write(self.buffer) | |
@micropython.native | |
def show_async(self): | |
state = ptr32(self._state) | |
if state[_ST_THREAD] == _THREAD_RUNNING: | |
state[_ST_COPY_BUFFS] = 1 | |
else: | |
self.show() | |
@micropython.native | |
def setFPS(self, newFrameRate): | |
self.frameRate = newFrameRate | |
@micropython.native | |
def update(self): | |
self.show() | |
if self.frameRate > 0: | |
frameTimeMs = 1000 // self.frameRate | |
lastUpdateEnd = self.lastUpdateEnd | |
frameTimeRemaining = frameTimeMs - ticks_diff(ticks_ms(), lastUpdateEnd) | |
while frameTimeRemaining > 1: | |
buttonA.update() | |
buttonB.update() | |
buttonU.update() | |
buttonD.update() | |
buttonL.update() | |
buttonR.update() | |
sleep_ms(1) | |
frameTimeRemaining = frameTimeMs - ticks_diff(ticks_ms(), lastUpdateEnd) | |
while frameTimeRemaining > 0: | |
frameTimeRemaining = frameTimeMs - ticks_diff(ticks_ms(), lastUpdateEnd) | |
self.lastUpdateEnd = ticks_ms() | |
@micropython.viper | |
def brightness(self, c:int): | |
if c < 0: c = 0 | |
if c > 127: c = 127 | |
state = ptr32(self._state) | |
postFrameAdj = self._postFrameAdj | |
postFrameAdjSrc = ptr8(self._postFrameAdjSrc) | |
# Provide 3 different subframe levels for the GPU | |
# Low (0): 0, 5, 15 | |
# Mid (28): 4, 42, 173 | |
# High (127): 9, 84, 255 | |
cc = int(floor(sqrt(c<<17))) | |
postFrameAdjSrc[0] = (cc*30>>12)+6 | |
postFrameAdjSrc[1] = (cc*72>>12)+14 | |
c3 = (cc*340>>12)+20 | |
postFrameAdjSrc[2] = c3 if c3 < 255 else 255 | |
# Apply to display, GPU, and emulator | |
if state[_ST_THREAD] == _THREAD_RUNNING: | |
state[_ST_CONTRAST] = 1 | |
else: | |
# Copy in the new contrast adjustments for when the GPU starts | |
postFrameAdj[0][1] = postFrameAdjSrc[0] | |
postFrameAdj[1][1] = postFrameAdjSrc[1] | |
postFrameAdj[2][1] = postFrameAdjSrc[2] | |
# Apply the contrast directly to the display or emulator | |
if emulator: | |
emulator.brightness_breakpoint(c) | |
else: | |
self.write_cmd([0x81, c]) | |
setattr(self, '_brightness', c) | |
# GPU (Gray Processing Unit) thread function | |
@micropython.viper | |
def _display_thread(self): | |
# cache various instance variables and buffers | |
postFrameAdjSrc = ptr8(self._postFrameAdjSrc) | |
state = ptr32(self._state) | |
preFrameCmds:ptr8 = ptr8(self._preFrameCmds) | |
postFrameCmds:ptr8 = ptr8(self._postFrameCmds) | |
pendingCmds:ptr8 = ptr8(self._pendingCmds) | |
# local object arrays for display framebuffers and post-frame commands | |
subframes:ptr32 = ptr32(array('L', [ptr8(self._subframes[0]), ptr8(self._subframes[1]), ptr8(self._subframes[2])])) | |
postFrameAdj:ptr32 = ptr32(array('L', [ptr8(self._postFrameAdj[0]), ptr8(self._postFrameAdj[1]), ptr8(self._postFrameAdj[2])])) | |
# hardware register access | |
spi0:ptr32 = ptr32(0x4003c000) | |
tmr:ptr32 = ptr32(0x40054000) | |
sio:ptr32 = ptr32(0xd0000000) | |
# we want ptr32 vars for fast buffer copying | |
bb = ptr32(self.buffer) ; bs = ptr32(self.shading) | |
b1 = ptr32(self._subframes[0]); b2 = ptr32(self._subframes[1]); b3 = ptr32(self._subframes[2]) | |
state[_ST_THREAD] = _THREAD_RUNNING | |
while state[_ST_THREAD] == _THREAD_RUNNING: | |
# this is the main GPU loop. We cycle through each of the 3 display | |
# framebuffers, sending the framebuffer data and various commands. | |
fn = 0 | |
while fn < 3: | |
time_out = tmr[10] + _PRE_FRAME_TIME_US | |
# the 'dc' output is used to switch the controller to receive | |
# commands (0) or frame data (1) | |
sio[6] = 1 << 17 # dc(0) | |
# send the pre-frame commands to 'park' the row counter | |
# spi_write(preFrameCmds) | |
i = 0 | |
while i < 4: | |
while (spi0[3] & 2) == 0: pass # while !(SPI0->SR & SPI_SSPSR_TNF_BITS): pass | |
spi0[2] = preFrameCmds[i] # SPI0->DR = buff[i] | |
i += 1 | |
while (spi0[3] & 4) == 4: i = spi0[2] # while SPI0->SR & SPI_SSPSR_RNE_BITS: read SPI0->DR | |
while (spi0[3] & 0x10) == 0x10: pass # while SPI0->SR & SPI_SSPSR_BSY_BITS: pass | |
while (spi0[3] & 4) == 4: i = spi0[2] # while SPI0->SR & SPI_SSPSR_RNE_BITS: read SPI0->DR | |
sio[5] = 1 << 17 # dc(1) | |
# and then send the frame | |
#spi_write(subframes[fn]) | |
i = 0 | |
spibuff:ptr8 = ptr8(subframes[fn]) | |
while i < 360: | |
while (spi0[3] & 2) == 0: pass | |
spi0[2] = spibuff[i] | |
i += 1 | |
while (spi0[3] & 4) == 4: i = spi0[2] | |
while (spi0[3] & 0x10) == 0x10: pass | |
while (spi0[3] & 4) == 4: i = spi0[2] | |
sio[6] = 1 << 17 # dc(0) | |
# send the first instance of the contrast adjust command | |
#spi_write(postFrameAdj[fn]) | |
i = 0 | |
spibuff:ptr8 = ptr8(postFrameAdj[fn]) | |
while i < 2: | |
while (spi0[3] & 2) == 0: pass | |
spi0[2] = spibuff[i] | |
i += 1 | |
while (spi0[3] & 4) == 4: i = spi0[2] | |
while (spi0[3] & 0x10) == 0x10: pass | |
while (spi0[3] & 4) == 4: i = spi0[2] | |
# wait for the pre-frame time to complete | |
while (tmr[10] - time_out) < 0: | |
pass | |
time_out = tmr[10] + _FRAME_TIME_US | |
# now send the post-frame commands to display the frame | |
#spi_write(postFrameCmds) | |
i = 0 | |
while i < 4: | |
while (spi0[3] & 2) == 0: pass | |
spi0[2] = postFrameCmds[i] | |
i += 1 | |
# and adjust the contrast for the specific frame number again. | |
# If we do not do this twice, the screen can glitch. | |
#spi_write(postFrameAdj[fn]) | |
i = 0 | |
spibuff:ptr8 = ptr8(postFrameAdj[fn]) | |
while i < 2: | |
while (spi0[3] & 2) == 0: pass | |
spi0[2] = spibuff[i] | |
i += 1 | |
while (spi0[3] & 4) == 4: i = spi0[2] | |
while (spi0[3] & 0x10) == 0x10: pass | |
while (spi0[3] & 4) == 4: i = spi0[2] | |
if fn == 2: | |
# check if there's a pending frame copy required | |
# we only copy the paint framebuffers to the display framebuffers on | |
# the last frame to avoid screen-tearing artefacts | |
if state[_ST_COPY_BUFFS] != 0: | |
i = 0 | |
inv = -1 if state[_ST_INVERT] else 0 | |
# fast copy loop. By using using ptr32 vars we copy 3 bytes at a time. | |
while i < _BUFF_INT_SIZE: | |
v1 = bb[i] ^ inv | |
v2 = bs[i] | |
# this isn't a straight copy. Instead we are mapping: | |
# in out colour | |
# 0 (0b00) 0 (0b000) black | |
# 1 (0b01) 5 (0b101) dark gray | |
# 2 (0b10) 7 (0b111) white | |
# 3 (0b11) 6 (0b110) light gray | |
b1[i] = v1 | v2 | |
b2[i] = v1 | |
b3[i] = v1 & (v1 ^ v2) | |
i += 1 | |
state[_ST_COPY_BUFFS] = 0 | |
# check if there's a pending contrast/brightness value change | |
if state[_ST_CONTRAST] != 0: | |
# Copy in the new contrast adjustments | |
ptr8(postFrameAdj[0])[1] = postFrameAdjSrc[0] | |
ptr8(postFrameAdj[1])[1] = postFrameAdjSrc[1] | |
ptr8(postFrameAdj[2])[1] = postFrameAdjSrc[2] | |
state[_ST_CONTRAST] = 0 | |
# check if there are pending commands | |
elif state[_ST_PENDING_CMD] != 0: | |
#spi_write(pending_cmds) | |
i = 0 | |
while i < 8: | |
while (spi0[3] & 2) == 0: pass | |
spi0[2] = pendingCmds[i] | |
i += 1 | |
while (spi0[3] & 4) == 4: i = spi0[2] | |
while (spi0[3] & 0x10) == 0x10: pass | |
while (spi0[3] & 4) == 4: i = spi0[2] | |
state[_ST_PENDING_CMD] = 0 | |
# wait for frame time to complete | |
while (tmr[10] - time_out) < 0: | |
pass | |
fn += 1 | |
# mark that we've stopped | |
state[_ST_THREAD] = _THREAD_STOPPED | |
@micropython.viper | |
def fill(self, colour:int): | |
buffer = ptr32(self.buffer) | |
shading = ptr32(self.shading) | |
f1 = -1 if colour & 1 else 0 | |
f2 = -1 if colour & 2 else 0 | |
i = 0 | |
while i < _BUFF_INT_SIZE: | |
buffer[i] = f1 | |
shading[i] = f2 | |
i += 1 | |
@micropython.viper | |
def drawFilledRectangle(self, x:int, y:int, width:int, height:int, colour:int): | |
if x >= _WIDTH or y >= _HEIGHT: return | |
if width <= 0 or height <= 0: return | |
if x < 0: | |
width += x | |
x = 0 | |
if y < 0: | |
height += y | |
y = 0 | |
x2 = x + width | |
y2 = y + height | |
if x2 > _WIDTH: | |
x2 = _WIDTH | |
width = _WIDTH - x | |
if y2 > _HEIGHT: | |
y2 = _HEIGHT | |
height = _HEIGHT - y | |
buffer = ptr8(self.buffer) | |
shading = ptr8(self.shading) | |
o = (y >> 3) * _WIDTH | |
oe = o + x2 | |
o += x | |
strd = _WIDTH - width | |
c1 = colour & 1 | |
c2 = colour & 2 | |
v1 = 0xff if c1 else 0 | |
v2 = 0xff if c2 else 0 | |
yb = y & 7 | |
ybh = 8 - yb | |
if height <= ybh: | |
m = ((1 << height) - 1) << yb | |
else: | |
m = 0xff << yb | |
im = 255-m | |
while o < oe: | |
if c1: | |
buffer[o] |= m | |
else: | |
buffer[o] &= im | |
if c2: | |
shading[o] |= m | |
else: | |
shading[o] &= im | |
o += 1 | |
height -= ybh | |
while height >= 8: | |
o += strd | |
oe += _WIDTH | |
while o < oe: | |
buffer[o] = v1 | |
shading[o] = v2 | |
o += 1 | |
height -= 8 | |
if height > 0: | |
o += strd | |
oe += _WIDTH | |
m = (1 << height) - 1 | |
im = 255-m | |
while o < oe: | |
if c1: | |
buffer[o] |= m | |
else: | |
buffer[o] &= im | |
if c2: | |
shading[o] |= m | |
else: | |
shading[o] &= im | |
o += 1 | |
@micropython.viper | |
def drawRectangle(self, x:int, y:int, width:int, height:int, colour:int): | |
dfr = self.drawFilledRectangle | |
dfr(x, y, width, 1, colour) | |
dfr(x, y, 1, height, colour) | |
dfr(x, y+height-1, width, 1, colour) | |
dfr(x+width-1, y, 1, height, colour) | |
@micropython.viper | |
def setPixel(self, x:int, y:int, colour:int): | |
if x < 0 or x >= _WIDTH or y < 0 or y >= _HEIGHT: | |
return | |
o = (y >> 3) * _WIDTH + x | |
m = 1 << (y & 7) | |
im = 255-m | |
buffer = ptr8(self.buffer) | |
shading = ptr8(self.shading) | |
if colour & 1: | |
buffer[o] |= m | |
else: | |
buffer[o] &= im | |
if colour & 2: | |
shading[o] |= m | |
else: | |
shading[o] &= im | |
@micropython.viper | |
def getPixel(self, x:int, y:int) -> int: | |
if x < 0 or x >= _WIDTH or y < 0 or y >= _HEIGHT: | |
return 0 | |
o = (y >> 3) * _WIDTH + x | |
m = 1 << (y & 7) | |
buffer = ptr8(self.buffer) | |
shading = ptr8(self.shading) | |
colour = 0 | |
if buffer[o] & m: | |
colour = 1 | |
if shading[o] & m: | |
colour |= 2 | |
return colour | |
@micropython.viper | |
def drawLine(self, x0:int, y0:int, x1:int, y1:int, colour:int): | |
if x0 == x1: | |
self.drawFilledRectangle(x0, y0, 1, y1 - y0, colour) | |
return | |
if y0 == y1: | |
self.drawFilledRectangle(x0, y0, x1 - x0, 1, colour) | |
return | |
dx = x1 - x0 | |
dy = y1 - y0 | |
sx = 1 | |
# y increment is always 1 | |
if dy < 0: | |
x0,x1 = x1,x0 | |
y0,y1 = y1,y0 | |
dy = 0 - dy | |
dx = 0 - dx | |
if dx < 0: | |
dx = 0 - dx | |
sx = -1 | |
x = x0 | |
y = y0 | |
buffer = ptr8(self.buffer) | |
shading = ptr8(self.shading) | |
o = (y >> 3) * _WIDTH + x | |
m = 1 << (y & 7) | |
im = 255-m | |
c1 = colour & 1 | |
c2 = colour & 2 | |
if dx > dy: | |
err = dx >> 1 | |
x1 += 1 | |
while x != x1: | |
if 0 <= x < _WIDTH and 0 <= y < _HEIGHT: | |
if c1: | |
buffer[o] |= m | |
else: | |
buffer[o] &= im | |
if c2: | |
shading[o] |= m | |
else: | |
shading[o] &= im | |
err -= dy | |
if err < 0: | |
y += 1 | |
m <<= 1 | |
if m & 0x100: | |
o += _WIDTH | |
m = 1 | |
im = 0xfe | |
else: | |
im = 255-m | |
err += dx | |
x += sx | |
o += sx | |
else: | |
err = dy >> 1 | |
y1 += 1 | |
while y != y1: | |
if 0 <= x < _WIDTH and 0 <= y < _HEIGHT: | |
if c1: | |
buffer[o] |= m | |
else: | |
buffer[o] &= im | |
if c2: | |
shading[o] |= m | |
else: | |
shading[o] &= im | |
err -= dx | |
if err < 0: | |
x += sx | |
o += sx | |
err += dy | |
y += 1 | |
m <<= 1 | |
if m & 0x100: | |
o += _WIDTH | |
m = 1 | |
im = 0xfe | |
else: | |
im = 255-m | |
def setFont(self, fontFile, width, height, space): | |
sz = stat(fontFile)[6] | |
self.font_bmap = bytearray(sz) | |
with open(fontFile, 'rb') as fh: | |
fh.readinto(self.font_bmap) | |
self.font_width = width | |
self.font_height = height | |
self.font_space = space | |
self.font_glyphcnt = sz // width | |
@micropython.viper | |
def drawText(self, stringToPrint, x:int, y:int, colour:int): | |
buffer = ptr8(self.buffer) | |
shading = ptr8(self.shading) | |
font_bmap = ptr8(self.font_bmap) | |
font_width = int(self.font_width) | |
font_space = int(self.font_space) | |
font_glyphcnt = int(self.font_glyphcnt) | |
sm1o = 0xff if colour & 1 else 0 | |
sm1a = 255 - sm1o | |
sm2o = 0xff if colour & 2 else 0 | |
sm2a = 255 - sm2o | |
ou = (y >> 3) * _WIDTH + x | |
ol = ou + _WIDTH | |
shu = y & 7 | |
shl = 8 - shu | |
for c in memoryview(stringToPrint): | |
if isinstance(c, str): | |
co = int(ord(c)) - 0x20 | |
else: | |
co = int(c) - 0x20 | |
if co < font_glyphcnt: | |
gi = co * font_width | |
gx = 0 | |
while gx < font_width: | |
if 0 <= x < _WIDTH: | |
gb = font_bmap[gi + gx] | |
gbu = gb << shu | |
gbl = gb >> shl | |
if 0 <= ou < _BUFF_SIZE: | |
# paint upper byte | |
buffer[ou] = (buffer[ou] | (gbu & sm1o)) & 255-(gbu & sm1a) | |
shading[ou] = (shading[ou] | (gbu & sm2o)) & 255-(gbu & sm2a) | |
if (shl != 8) and (0 <= ol < _BUFF_SIZE): | |
# paint lower byte | |
buffer[ol] = (buffer[ol] | (gbl & sm1o)) & 255-(gbl & sm1a) | |
shading[ol] = (shading[ol] | (gbl & sm2o)) & 255-(gbl & sm2a) | |
ou += 1 | |
ol += 1 | |
x += 1 | |
gx += 1 | |
ou += font_space | |
ol += font_space | |
x += font_space | |
@micropython.viper | |
def blit(self, src, x:int, y:int, width:int, height:int, key:int, mirrorX:int, mirrorY:int): | |
if x+width < 0 or x >= _WIDTH: | |
return | |
if y+height < 0 or y >= _HEIGHT: | |
return | |
buffer = ptr8(self.buffer) | |
shading = ptr8(self.shading) | |
if isinstance(src, (tuple, list)): | |
shd = 1 | |
src1 = ptr8(src[0]) | |
src2 = ptr8(src[1]) | |
else: | |
shd = 0 | |
src1 = ptr8(src) | |
src2 = ptr8(0) | |
stride = width | |
srcx = 0 ; srcy = 0 | |
dstx = x ; dsty = y | |
sdx = 1 | |
if mirrorX: | |
sdx = -1 | |
srcx += width - 1 | |
if dstx < 0: | |
srcx += dstx | |
width += dstx | |
dstx = 0 | |
else: | |
if dstx < 0: | |
srcx = 0 - dstx | |
width += dstx | |
dstx = 0 | |
if dstx+width > _WIDTH: | |
width = _WIDTH - dstx | |
if mirrorY: | |
srcy = height - 1 | |
if dsty < 0: | |
srcy += dsty | |
height += dsty | |
dsty = 0 | |
else: | |
if dsty < 0: | |
srcy = 0 - dsty | |
height += dsty | |
dsty = 0 | |
if dsty+height > _HEIGHT: | |
height = _HEIGHT - dsty | |
srco = (srcy >> 3) * stride + srcx | |
srcm = 1 << (srcy & 7) | |
dsto = (dsty >> 3) * _WIDTH + dstx | |
dstm = 1 << (dsty & 7) | |
dstim = 255 - dstm | |
while height != 0: | |
srcco = srco | |
dstco = dsto | |
i = width | |
while i != 0: | |
v = 0 | |
if src1[srcco] & srcm: | |
v = 1 | |
if shd and (src2[srcco] & srcm): | |
v |= 2 | |
if (key == -1) or (v != key): | |
if v & 1: | |
buffer[dstco] |= dstm | |
else: | |
buffer[dstco] &= dstim | |
if v & 2: | |
shading[dstco] |= dstm | |
else: | |
shading[dstco] &= dstim | |
srcco += sdx | |
dstco += 1 | |
i -= 1 | |
dstm <<= 1 | |
if dstm & 0x100: | |
dsto += _WIDTH | |
dstm = 1 | |
dstim = 0xfe | |
else: | |
dstim = 255 - dstm | |
if mirrorY: | |
srcm >>= 1 | |
if srcm == 0: | |
srco -= stride | |
srcm = 0x80 | |
else: | |
srcm <<= 1 | |
if srcm & 0x100: | |
srco += stride | |
srcm = 1 | |
height -= 1 | |
@micropython.native | |
def drawSprite(self, s): | |
self.blit(s.bitmap, s.x, s.y, s.width, s.height, s.key, s.mirrorX, s.mirrorY) | |
@micropython.viper | |
def blitWithMask(self, src, x:int, y:int, width:int, height:int, key:int, mirrorX:int, mirrorY:int, mask): | |
if x+width < 0 or x >= _WIDTH: | |
return | |
if y+height < 0 or y >= _HEIGHT: | |
return | |
buffer = ptr8(self.buffer) | |
shading = ptr8(self.shading) | |
if isinstance(src, (tuple, list)): | |
shd = 1 | |
src1 = ptr8(src[0]) | |
src2 = ptr8(src[1]) | |
else: | |
shd = 0 | |
src1 = ptr8(src) | |
src2 = ptr8(0) | |
if isinstance(mask, (tuple, list)): | |
maskp = ptr8(mask[0]) | |
else: | |
maskp = ptr8(mask) | |
stride = width | |
srcx = 0 ; srcy = 0 | |
dstx = x ; dsty = y | |
sdx = 1 | |
if mirrorX: | |
sdx = -1 | |
srcx += width - 1 | |
if dstx < 0: | |
srcx += dstx | |
width += dstx | |
dstx = 0 | |
else: | |
if dstx < 0: | |
srcx = 0 - dstx | |
width += dstx | |
dstx = 0 | |
if dstx+width > _WIDTH: | |
width = _WIDTH - dstx | |
if mirrorY: | |
srcy = height - 1 | |
if dsty < 0: | |
srcy += dsty | |
height += dsty | |
dsty = 0 | |
else: | |
if dsty < 0: | |
srcy = 0 - dsty | |
height += dsty | |
dsty = 0 | |
if dsty+height > _HEIGHT: | |
height = _HEIGHT - dsty | |
srco = (srcy >> 3) * stride + srcx | |
srcm = 1 << (srcy & 7) | |
dsto = (dsty >> 3) * _WIDTH + dstx | |
dstm = 1 << (dsty & 7) | |
dstim = 255 - dstm | |
while height != 0: | |
srcco = srco | |
dstco = dsto | |
i = width | |
while i != 0: | |
if (maskp[srcco] & srcm) == 0: | |
if src1[srcco] & srcm: | |
buffer[dstco] |= dstm | |
else: | |
buffer[dstco] &= dstim | |
if shd and (src2[srcco] & srcm): | |
shading[dstco] |= dstm | |
else: | |
shading[dstco] &= dstim | |
srcco += sdx | |
dstco += 1 | |
i -= 1 | |
dstm <<= 1 | |
if dstm & 0x100: | |
dsto += _WIDTH | |
dstm = 1 | |
dstim = 0xfe | |
else: | |
dstim = 255 - dstm | |
if mirrorY: | |
srcm >>= 1 | |
if srcm == 0: | |
srco -= stride | |
srcm = 0x80 | |
else: | |
srcm <<= 1 | |
if srcm & 0x100: | |
srco += stride | |
srcm = 1 | |
height -= 1 | |
@micropython.native | |
def drawSpriteWithMask(self, s, m): | |
self.blitWithMask(s.bitmap, s.x, s.y, s.width, s.height, s.key, s.mirrorX, s.mirrorY, m.bitmap) | |
display = Grayscale() | |
display.enableGrayscale() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment