Skip to content

Instantly share code, notes, and snippets.

@alufers
Last active August 18, 2023 00:58
Show Gist options
  • Save alufers/f0b12e27b8d81ee0de41119d3cdd1e61 to your computer and use it in GitHub Desktop.
Save alufers/f0b12e27b8d81ee0de41119d3cdd1e61 to your computer and use it in GitHub Desktop.
Vortex game backup
from ctx import Context
from st3m.reactor import Responder
from st3m.input import InputController, InputState
from st3m.utils import tau
from st3m.ui.view import View, ViewManager
from st3m.application import Application, ApplicationContext
import st3m.run
import math
import random
import gc
try:
from typing import List, Tuple, Optional, Dict, Any
except:
pass
SCREEN_RADIUS = 120
class PolarPos:
def __init__(self, angle: float, radius: float) -> None:
self.angle = angle
self.radius = radius
def __str__(self) -> str:
return f"({self.angle}, {self.radius})"
def __repr__(self) -> str:
return str(self)
def add(self, other: "PolarPos", delta: float = 1.0) -> "PolarPos":
self.angle = (self.angle + other.angle * delta) % tau
self.radius += other.radius * delta
return self
def to_cartesian(self) -> Tuple[float, float]:
return (self.radius * math.cos(self.angle), self.radius * math.sin(self.angle))
class Entity:
def __init__(self, vortex: "VortexGame") -> None:
self.vortex = vortex
def think(self, ins: InputState, delta_ms: int) -> None:
pass
def draw(self, ctx: Context) -> None:
pass
class Ball(Entity):
def __init__(self, vortex: "VortexGame") -> None:
super().__init__(vortex)
self.pos = PolarPos(0, 0)
self.velocity = PolarPos(0, 0)
self.bound_to_paddle_time = 2.0
def think(self, ins: InputState, delta_ms: int) -> None:
for entity in self.vortex.entities:
if isinstance(entity, Block):
entity.check_collision(self)
if self.bound_to_paddle_time > 0:
self.bound_to_paddle_time -= delta_ms / 1000
player_block = None
for entity in self.vortex.entities:
if isinstance(entity, PlayerBlock):
player_block = entity
center_angle = player_block.angle + player_block.width / 2
self.pos.angle = center_angle
self.pos.radius = player_block.radius - 10
if self.bound_to_paddle_time <= 0:
# yeet the ball towards the center
initial_speed = 30
self.velocity.angle = player_block.velocity * 0.2
self.velocity.radius = -initial_speed
return
self.pos.add(self.velocity, delta_ms / 1000)
if self.pos.radius > self.vortex.SCREEN_RADIUS:
self.vortex.kill_ball(self)
def draw(self, ctx: Context) -> None:
ctx.rgb(255, 255, 255)
ctx.begin_path()
x, y = self.pos.to_cartesian()
max_radius = 5
min_radius = 2
dist_to_center_ratio = self.pos.radius / self.vortex.SCREEN_RADIUS
radius = min_radius + (max_radius - min_radius) * dist_to_center_ratio
ctx.arc(x, y, radius, 0, tau, 0)
ctx.fill()
class Particle(Entity):
def __init__(self, vortex: "VortexGame", x: float, y: float, vx: float, vy: float) -> None:
super().__init__(vortex)
self.x = x
self.y = y
self.vx = vx
self.vy = vy
self.total_lifetime = 0.7
self.lifetime = self.total_lifetime
self.r = 1
self.g = 1
self.b = 1
def think(self, ins: InputState, delta_ms: int) -> None:
self.lifetime -= delta_ms / 1000
self.x += self.vx * delta_ms / 1000
self.y += self.vy * delta_ms / 1000
if self.lifetime <= 0:
self.vortex.entities.remove(self)
def draw(self, ctx: Context) -> None:
alpha = 1.0
if self.lifetime < 0.5:
alpha = self.lifetime / 0.5
ctx.rgba(self.r, self.g, self.b, alpha)
ctx.begin_path()
ctx.arc(self.x, self.y, 2, 0, tau, 0)
ctx.fill()
class Block(Entity):
def __init__(self, vortex: "VortexGame", angle: float, width: float, radius: float, thickness: float) -> None:
super().__init__(vortex)
self.angle = angle # position of the first edge of the block
self.velocity = 0 # angular velocity
self.width = width # angular size
self.radius = radius # distance from center
self.thickness = thickness # thickness of the block
self.filled = True
self.r = 1
self.g = 0
self.b = 0
self.alpha = 1
def draw(self, ctx: Context) -> None:
ctx.rgba(self.r, self.g, self.b, self.alpha)
self.draw_block(ctx, self.angle, self.width, self.radius, self.thickness)
if self.filled:
ctx.fill()
else:
ctx.stroke()
def draw_block(self, ctx: Context, angle: float, width: float, radius: float, thickness: float) -> None:
angle = angle % tau
p1_x = (radius) * math.cos(angle + width)
p1_y = (radius) * math.sin(angle + width)
p2_x = (radius) * math.cos(angle)
p2_y = (radius) * math.sin(angle)
p3_x = (radius + thickness) * math.cos(angle)
p3_y = (radius + thickness) * math.sin(angle)
p4_x = (radius + thickness) * math.cos(angle + width)
p4_y = (radius + thickness) * math.sin(angle + width)
ctx.begin_path()
# ctx.move_to(p1_x, p1_y)
# ctx.arc(0, 0, radius, angle, angle + width, 0)
self.arc_tesselate(ctx, 0, 0, radius, angle, angle + width, True)
# ctx.line_to(p2_x, p2_y)
ctx.line_to(p3_x, p3_y)
# ctx.rgb(0, 0, 255)
# ctx.line_to(p3_x, p3_y)
# ctx.rgb(0, 255, 0)
self.arc_tesselate(ctx, 0, 0, radius + thickness, angle, angle + width, False)
ctx.close_path()
def arc_tesselate(self, ctx: Context, x: float, y:float, radius: float, start_angle: float, end_angle: float, reverse: bool) -> None:
STEPS = 5
# ctx.move_to(radius * math.cos(start_angle), radius * math.sin(start_angle))
for i in range(STEPS):
if reverse:
i = STEPS - i - 1
angle = start_angle + (end_angle - start_angle) * i / (STEPS - 1)
ctx.line_to(radius * math.cos(angle), radius * math.sin(angle))
def think(self, ins: InputState, delta_ms: int) -> None:
self.angle += self.velocity * delta_ms / 1000
self.angle = self.angle % tau
def get_linear_velocity(self) -> Tuple[float, float]:
return (self.radius * -self.velocity * math.cos(self.angle), self.radius * -self.velocity * math.sin(self.angle))
def check_collision(self, ball: Ball) -> None:
ball_angle = ball.pos.angle #math.atan2(ball.y, ball.x) % tau
start_angle = self.angle % tau
end_angle = (self.angle + self.width) % tau
dist_from_center = ball.pos.radius
if not (self.radius <= dist_from_center and dist_from_center <= self.radius + self.thickness):
return
angle_ok = False
if start_angle < end_angle:
angle_ok = start_angle <= ball_angle and ball_angle <= end_angle
else:
angle_ok = start_angle <= ball_angle or ball_angle <= end_angle
if angle_ok:
self.handle_collision(ball)
def handle_collision(self, ball: Ball) -> None:
ball.velocity.radius *= -1
angular_velocity_diff = abs(self.velocity - ball.velocity.angle)
angular_velocity_diff *= 0.3
if self.velocity > ball.velocity.angle:
ball.velocity.angle += angular_velocity_diff
else:
ball.velocity.angle -= angular_velocity_diff
# make sure the ball is outside the block
dist_from_inner = math.fabs(ball.pos.radius - self.radius)
dist_from_outer = math.fabs(ball.pos.radius - (self.radius + self.thickness))
margin = 1
if dist_from_inner < dist_from_outer:
ball.pos.radius = self.radius - margin
else:
ball.pos.radius = self.radius + self.thickness + margin
class PlayerBlock(Block):
def __init__(self, vortex: "VortexGame", angle: float, width: float, radius: float, thickness: float) -> None:
super().__init__(vortex, angle, width, radius, thickness)
self.r = 0.7
self.g = 0.7
self.b = 0.7
def draw(self, ctx: Context) -> None:
super().draw(ctx)
ctx.rgb(0.4, 0.4, 0.4)
self.draw_block(ctx, self.angle + self.width*0.1, self.width * 0.8, self.radius, self.thickness)
ctx.fill()
class CenterBlock(Block):
def __init__(self, vortex: "VortexGame", angle: float, width: float, radius: float, thickness: float) -> None:
super().__init__(vortex, angle, width, radius, thickness)
self.r = 0.4
self.g = 0.4
self.b = 0.4
def draw(self, ctx: Context) -> None:
ctx.rgba(self.r, self.g, self.b, self.alpha)
ctx.arc(0, 0, self.radius, 0, tau, 0)
ctx.fill()
def check_collision(self, ball: Ball) -> None:
if ball.pos.radius <= self.radius:
ball.pos.radius = self.radius + 2
ball.velocity.radius *= -1
class DestroyableBlock(Block):
def __init__(self, vortex: "VortexGame", angle: float, width: float, radius: float, thickness: float) -> None:
super().__init__(vortex, angle, width, radius, thickness)
self._initial_health = 3.0
self._health = self._initial_health
def handle_collision(self, ball: Ball) -> None:
self._health -= 1.0
self.alpha = self._health / self._initial_health
super().handle_collision(ball)
if self._health <= 0:
self.vortex.entities.remove(self)
self.vortex.score += int(self._initial_health)
# particles
for i in range(0, 10):
dist_center = self.radius + self.thickness / 2
angle = self.angle + random.random() * self.width
x = dist_center * math.cos(angle)
y = dist_center * math.sin(angle)
vx, vy = self.get_linear_velocity()
vx += random.random() * 30 - 15
vy += random.random() * 30 - 15
part = Particle(self.vortex, x, y, vx, vy)
part.r = self.r
part.g = self.g
part.b = self.b
self.vortex.entities.append(part)
# check if there are any blocks left
if len([e for e in self.vortex.entities if isinstance(e, DestroyableBlock)]) == 0:
self.vortex.animations.append(WaitEffect(self.vortex, 0.2))
self.vortex.animations.append(DropDownEffect(self.vortex, 0.5))
self.vortex.animations.append(WaitEffect(self.vortex, 0.1))
self.vortex.animations.append(RiseUpEffect(self.vortex, 0.5))
self.vortex.animations.append(self.vortex.next_level)
class Animation:
def __init__(self, vortex: "VortexGame", duration: float) -> None:
self.vortex = vortex
self.duration = duration
self.time = 0
def before_draw(self, ctx: Context) -> None:
pass
def after_draw(self, ctx: Context) -> None:
pass
def after_draw2(self, ctx: Context) -> None:
pass
class DropDownEffect(Animation):
def before_draw(self, ctx: Context) -> None:
progress = self.time / self.duration
self.vortex.zoom_scale = 1 + progress * 5
class RiseUpEffect(Animation):
def before_draw(self, ctx: Context) -> None:
progress = 1 - (self.time / self.duration)
self.vortex.zoom_scale = 1 + progress * 5
class WaitEffect(Animation):
def before_draw(self, ctx: Context) -> None:
pass
def after_draw(self, ctx: Context) -> None:
pass
class FadeToBlackEffect(Animation):
def before_draw(self, ctx: Context) -> None:
pass
def after_draw2(self, ctx: Context) -> None:
progress = self.time / self.duration
ctx.rgba(0, 0, 0, progress)
ctx.rectangle(-120, -120, 240, 240)
ctx.fill()
class GameOverEffect(Animation):
def before_draw(self, ctx: Context) -> None:
pass
def after_draw2(self, ctx: Context) -> None:
ctx.rgba(0, 0, 0, 1)
ctx.rectangle(-120, -120, 240, 240)
ctx.fill()
ctx.rgb(1, 1, 1)
ctx.move_to(0, 0)
ctx.font_size = 30
ctx.text_align = ctx.CENTER
ctx.text_baseline = ctx.MIDDLE
ctx.text("Game Over")
class VortexGame(Application):
def __init__(self, app_ctx: ApplicationContext) -> None:
super().__init__(app_ctx)
self.input = InputController()
self.SCREEN_RADIUS = 120
self._time_until_ball_spawn = None
self.entities = []
self.animations = []
self.current_animation = None
self.player_speed = 1.5
self.demo_mode = True
self.score = 0
self.zoom_scale = 1.0
self.current_level = 0
self.load_level(0)
def load_level(self, level: int) -> None:
self.entities = []
self.entities.append(Ball(self))
self.entities.append(PlayerBlock(self, 0, 0.1 * tau, self.SCREEN_RADIUS - 10, 10))
CENTER_RADIUS = 20
self.entities.append(CenterBlock(self, 0, 0.1 * tau, CENTER_RADIUS, 10))
self.spare_balls = 3
self.max_spare_balls = 3
self.current_level = level
if level == 0:
for i in range(0, 10):
if i % 2 == 0:
continue
red_block = DestroyableBlock(self, i * tau / 10, 0.09 * tau, CENTER_RADIUS + 5, 10)
red_block.r = 1
red_block.g = 0
red_block.b = 0
red_block._initial_health = 1
red_block._health = 1
self.entities.append(red_block)
for i in range(0, 5):
db = DestroyableBlock(self, i * tau / 10, 0.09 * tau, CENTER_RADIUS + 20, 10)
db.velocity = 0.5
db.r = 0
db.g = 0.5
db.b = 0.5
self.entities.append(db)
elif level == 1:
for i in range(0, 10):
if i % 2 == 0:
continue
db = DestroyableBlock(self, i * tau / 10, 0.09 * tau, CENTER_RADIUS + 20, 10)
db.velocity = 0.5
db.r = 0
db.g = 0.5
db.b = 0.5
self.entities.append(db)
for i in range(0, 10):
if i % 2 == 1:
continue
db = DestroyableBlock(self, i * tau / 10, 0.09 * tau, CENTER_RADIUS + 35, 10)
db.velocity = -0.5
db.r = 1
db.g = 0
db.b = 0
db._initial_health = 1
db._health = 1
self.entities.append(db)
for i in range(0, 5):
if i % 2 == 1:
continue
db = DestroyableBlock(self, i * tau / 5, 0.09 * tau, CENTER_RADIUS + 50, 10)
db.velocity = 0.7
db.r = 0
db.g = 1
db.b = 0
db._initial_health = 2
db._health = 2
self.entities.append(db)
def next_level(self, xD) -> None:
self.load_level(self.current_level + 1)
self.zoom_scale = 1.0
def draw(self, ctx: Context) -> None:
# Paint the background black
ctx.rgb(0.1, 0.1, 0.1).rectangle(-120, -120, 240, 240).fill()
# # Paint a red square in the middle of the display
# if self._draw_rectangle:
# ctx.rgb(255, 0, 0).rectangle(self._x, -20, 40, 40).fill()
# else:
ctx.save()
ctx.scale(self.zoom_scale, self.zoom_scale)
if self.current_animation is not None:
ctx.move_to(0,-40)
ctx.text(str(self.current_animation.time))
self.current_animation.before_draw(ctx)
self.draw_background(ctx)
for entity in self.entities:
entity.draw(ctx)
if self.demo_mode:
ctx.move_to(0,-30)
ctx.rgb(1, 1, 1)
ctx.text_align = ctx.CENTER
ctx.font_size = 20
ctx.text("VORTEX")
ctx.move_to(0, 30)
ctx.font_size = 10
ctx.text("left shoulder to start")
if self.current_animation is None and len(self.animations) == 0:
self.zoom_scale = 1.0
elif self.current_animation is None and len(self.animations) == 0:
self.zoom_scale = 1.0
ctx.move_to(0,0)
ctx.rgb(1, 1, 1)
ctx.font_size = 17
ctx.text_align = ctx.CENTER
ctx.text(str(self.score))
for i in range(0, self.max_spare_balls):
min_angle = tau/2 - tau/5
max_angle = tau/2 + tau/5
angle = tau/2 + tau/3 + min_angle + (max_angle - min_angle) * i / self.max_spare_balls
radius = 15
x = radius * math.cos(angle)
y = radius * math.sin(angle)
ctx.move_to(x, y)
if self.spare_balls > i:
ctx.rgb(1, 1, 1)
else:
ctx.rgb(0.2, 0.2, 0.2)
ctx.begin_path()
ctx.arc(x, y, 3, 0, tau, 0)
ctx.fill()
if self.current_animation is not None:
self.current_animation.after_draw(ctx)
ctx.restore()
if self.current_animation is not None:
self.current_animation.after_draw2(ctx)
def draw_background(self, ctx: Context) -> None:
SEGMENTS = 7
ctx.rgb(0, 0, 0.2)
ctx.begin_path()
for i in range(0, SEGMENTS):
angle = i * tau / SEGMENTS
ctx.move_to(0, 0)
ctx.line_to(self.SCREEN_RADIUS * math.cos(angle), self.SCREEN_RADIUS * math.sin(angle))
ctx.stroke()
def draw_pkt(self, ctx, x: float, y: float, r: int, g: int, b: int) -> None:
ctx.rgb(r, g, b)
ctx.begin_path()
ctx.arc(x, y, 3, 0, tau, 0)
ctx.fill()
def think(self, ins: InputState, delta_ms: int) -> None:
if self.current_animation is not None:
self.current_animation.time += delta_ms / 1000
if self.current_animation.time > self.current_animation.duration:
self.current_animation = None
if self.current_animation is None:
for anim in self.animations:
# check if is function, if yes call it and remove it from the list
if callable(anim):
anim(self)
self.animations.remove(anim)
else:
self.current_animation = anim
self.animations.remove(self.current_animation)
break
self.input.think(ins, delta_ms) # let the input controller to its magic
# if self.input.buttons.app.middle.pressed:
# self._draw_rectangle = not self._draw_rectangle
direction = ins.buttons.app
player_block = None
for entity in self.entities:
if isinstance(entity, PlayerBlock):
player_block = entity
if self.demo_mode:
# ai code
ball = None
for entity in self.entities:
if isinstance(entity, Ball):
ball = entity
if ball is not None:
player_angle = player_block.angle + player_block.width / 2
ball_angle = ball.pos.angle
if ball.bound_to_paddle_time > 0:
player_block.velocity = self.player_speed * (int(ball.bound_to_paddle_time * 3)) % 3 - 1
else:
player_block.angle = (ball_angle - player_block.width / 2) % tau
if self.input.buttons.app.left.pressed or self.input.buttons.app.right.pressed:
if len(self.animations) == 0:
# print("APPEND")
self.animations.append(DropDownEffect(self, 1))
self.animations.append(self.exit_demo_mode)
self.animations.append(RiseUpEffect(self, 1))
else:
# player control
if direction == ins.buttons.PRESSED_LEFT:
player_block.velocity = self.player_speed
elif direction == ins.buttons.PRESSED_RIGHT:
player_block.velocity = -self.player_speed
else:
player_block.velocity = 0
for entity in self.entities:
entity.think(ins, delta_ms)
# ball respawn
if self._time_until_ball_spawn is not None:
self._time_until_ball_spawn -= delta_ms / 1000
if self._time_until_ball_spawn <= 0:
gc.collect()
b = Ball(self)
b.think(ins, delta_ms) # make it think so it teleports to the paddle location
self.entities.append(b)
self._time_until_ball_spawn = None
def exit_demo_mode(self, xD):
self.demo_mode = False
self.score = 0
self.spare_balls = 0 # self.max_spare_balls
self.load_level(0)
def enter_demo_mode(self, xD):
self.demo_mode = True
self.score = 0
self.load_level(0)
self._time_until_ball_spawn = 1.0
def on_enter(self, vm: Optional[ViewManager]) -> None:
self._vm = vm
self.input._ignore_pressed()
def kill_ball(self, ball: Ball) -> None:
self.spare_balls -= 1
self.entities.remove(ball)
# generate 10 particles
for i in range(40):
vx = 130 * (2 * random.random() - 1)
vy = 130 * (2 * random.random() - 1)
x, y = ball.pos.to_cartesian()
self.entities.append(Particle(self, x, y, vx, vy))
if self.spare_balls < 0 and not self.demo_mode:
self.animations.append(WaitEffect(self, 0.2))
self.animations.append(DropDownEffect(self, 1))
self.animations.append(FadeToBlackEffect(self, 0.5))
self.animations.append(GameOverEffect(self, 1))
self.animations.append(RiseUpEffect(self, 1))
self.animations.append(self.enter_demo_mode)
return
self._time_until_ball_spawn = 1.0
if __name__ == '__main__':
# Continue to make runnable via mpremote run.
st3m.run.run_view(VortexGame(ApplicationContext()))
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment