Skip to content

Instantly share code, notes, and snippets.

@demodude4u
Last active January 17, 2023 19:20
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save demodude4u/d387d94a7d7f08d3aad08b931508cfd6 to your computer and use it in GitHub Desktop.
Save demodude4u/d387d94a7d7f08d3aad08b931508cfd6 to your computer and use it in GitHub Desktop.
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
# 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