Last active
August 18, 2023 00:58
-
-
Save alufers/f0b12e27b8d81ee0de41119d3cdd1e61 to your computer and use it in GitHub Desktop.
Vortex game backup
This file contains hidden or 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 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