예제 #1
0
def main():
    app = App(framerate=float("inf"))

    colors = [Color.rgb(r,g,b) for r in (0, 1) for g in (0, 1) for b in (0, 1)]
    pixels = [PixelData(char="▄", fg=col1, bg=col2) for col1 in colors for col2 in colors]

    t0 = perf_counter()
    n_frames = 0
    n_updates = 0
    update_time = 0

    @app.on("frame")
    def on_frame():
        nonlocal n_frames, n_updates, update_time
        for x in range(app.screen.w):
            for y in range(app.screen.h):
                app.screen[x,y].set(choice(pixels))
        app.screen.update()
        n_frames += 1
        n_updates += app.screen._update_count
        update_time += app.screen._update_duration

    app.run()

    updates_per_ms = n_updates / update_time / 1000
    updates_per_ms_real = n_updates / (perf_counter() - t0) / 1000
    print("Updates per ms (updating): {:.2f}".format(updates_per_ms))
    print("Updates per ms (real time): {:.2f}".format(updates_per_ms_real))
    print("Average update time ms: {:.2f}".format(update_time / n_frames * 1000))
    print("Average potential FPS: {:.2f}".format(n_frames / update_time))
예제 #2
0
def main():
    app = App(mouse=True, framerate=120)
    buf = Buffer(0, 0)

    dirty = True
    dragging = False
    drag_start = None
    mouse_pos = None

    def do_box(buffer):
        if drag_start is None or mouse_pos is None:
            return
        x, y, w, h = corners_to_box(*drag_start, *mouse_pos)
        # draw_box(buffer, x, y, w, h, chars=BOX_CHARS_DOUBLE, fg=Color.hsl(perf_counter(), 1.0, 0.5))
        draw_frame(buffer,
                   x,
                   y,
                   w,
                   h,
                   chars=FRAME_CHARS_DOUBLE,
                   fg=Color.hsl(perf_counter(), 1.0, 0.5))

    @app.on("start")
    @app.on("resize")
    def on_resize():
        buf.resize(app.screen.w, app.screen.h)
        for x in range(buf.w):
            for y in range(buf.h):
                col = Color.rgb(0, 0, 0) if (x + y) % 2 else Color.rgb(
                    0.1, 0.1, 0.1)
                buf.put_char(" ", x, y, bg=col)

    @app.on("mouse")
    def on_mouse(mouse):
        nonlocal dirty, dragging, drag_start, mouse_pos
        if mouse.left:
            if mouse.down:
                dragging = True
                drag_start = (mouse.x, mouse.y)
            if mouse.up:
                do_box(buf)
                dragging = False
            dirty = True
            mouse_pos = (mouse.x, mouse.y)
        else:
            dragging = False

    @app.on("frame")
    def on_frame():
        app.screen.clear()
        app.screen.blit(buf)
        if dragging:
            do_box(app.screen)
        app.screen.update()

    app.run()
예제 #3
0
def main():
    app = App(framerate=float("inf"))
    app.t0 = perf_counter()
    app.frames = 0
    app.pos = 0

    @app.on("frame")
    def on_frame():
        app.pos = (app.pos + 1) % (app.screen.w * app.screen.h)
        x = app.pos % app.screen.w
        y = app.pos // app.screen.w
        app.screen.clear()
        app.screen.print(" " * 5, x, y, bg=AQUA)
        app.screen.update()
        app.frames += 1
        app.t1 = perf_counter()

    @app.on("after_stop")
    def on_after_stop():
        tpf = (app.t1 - app.t0) / app.frames
        print("Terminal: {} ({}x{})\n".format(app.backend.terminal_name,
                                              app.screen.w, app.screen.h))
        print("Avg time per frame: {:.4f}\n".format(tpf))
        print("Avg framerate: {:.2f}\n".format(1 / tpf))

    app.start()
    app.await_stop()
예제 #4
0
def main():
    app = App(mouse=True, framerate=60)
    app.mouse_x = 0
    app.mouse_y = 0
    app.mouse_px = 0
    app.mouse_py = 0
    app.particles = []

    @app.on("mouse")
    def on_mouse(m):
        app.mouse_x = m.x
        app.mouse_y = m.y * 2
    
    @app.on("frame")
    def on_frame():
        w = app.screen.w
        h = app.screen.h
        colormap = [Color.rgb(0,0,0) for x in range(w) for y in range(h * 2)]

        dx = app.mouse_x - app.mouse_px
        dy = app.mouse_y - app.mouse_py
        l = math.sqrt(dx ** 2 + dy ** 2)
        d = math.atan2(dy, dx)

        j = min(25, int(l)) * 6
        for i in range(j):
            f = i / j
            ll = l* random.uniform(0.25, 0.5)
            dd = d + random.uniform(-0.25, 0.25)
            vx = ll * math.cos(dd)
            vy = ll * math.sin(dd)
            app.particles.append(Particle(app.mouse_px + dx * f, app.mouse_py + dy * f, vx, vy))

        app.screen.clear()
        for i, p in enumerate(app.particles):
            col = Color.hsl(i/len(app.particles), 1.0, 0.5)
            steps = math.hypot(p.vx, p.vy)
            for step in range(int(steps + 1)):
                f = step / steps
                px = int(p.x - p.vx * f)
                py = int(p.y - p.vy * f)
                if px >= 0 and py >= 0 and px < w and py < h * 2:
                    colormap[py * w + px] += RED
            p.update()
            if p.x < 0 or p.y < 0 or p.x >= w or p.y >= h * 2:
                app.particles.remove(p)
        draw_colormap_2x(app.screen, colormap, 0, 0, w=w, h=h * 2)
        app.screen.update()

        app.mouse_px = app.mouse_x
        app.mouse_py = app.mouse_y

    app.run()
예제 #5
0
def main():
    app = App(mouse=True)
    x = 0
    y = 0
    left = False
    middle = False
    right = False
    scroll_total = 0

    @app.on("mouse")
    def mouse(mouse: Mouse):
        nonlocal x, y, left, middle, right, scroll_total

        if mouse.scrollup:
            scroll_total += 1
        elif mouse.scrolldown:
            scroll_total -= 1

        if mouse.down:
            if mouse.left:
                left = True
            if mouse.middle:
                middle = True
            if mouse.right:
                right = True
        elif mouse.up:
            if mouse.left:
                left = False
            if mouse.middle:
                middle = False
            if mouse.right:
                right = False

        x = mouse.x
        y = mouse.y

    @app.on("frame")
    def frame():
        app.screen.clear()
        app.screen.print(
            "".join("#" if b else "_" for b in [left, middle, right]), 1, 1)
        app.screen.print("LMR", 1, 2)
        app.screen.print(f"scroll: {scroll_total}", 7, 2)
        app.screen.show_cursor = True
        app.screen.cursor_pos = (x, y)
        app.screen.update()

    app.run()
예제 #6
0
def main():
    app = App(mouse=True)

    @app.on("start")
    def on_start():
        app.mouse = None
        app.key = None
        app.dirty = False
        redraw(app)

    @app.on("resize")
    def on_resize():
        redraw(app)

    @app.on("key")
    def on_key(k):
        app.key = k
        app.dirty = True

    @app.on("mouse")
    def on_mouse(m):
        app.mouse = m
        app.dirty = True

    @app.on("frame")
    def on_frame():
        if app.dirty:
            redraw(app)
            app.dirty = False

    app.start()
    app.await_stop()
예제 #7
0
def main():
    app = App()

    @app.on("frame")  # run this function every frame
    def on_frame():
        app.screen.clear()  # remove everything from the screen
        text = "Hello world, from termpixels!"

        for i, c in enumerate(text):
            f = i / len(text)
            color = Color.hsl(f + time(), 1,
                              0.5)  # create a color from a hue value
            x = app.screen.w // 2 - len(
                text) // 2  # horizontally center the text
            offset = sin(time() * 3 + f * 5) * 2  # some arbitrary math
            y = round(app.screen.h / 2 +
                      offset)  # vertical center with an offset
            app.screen.print(c, x + i, y,
                             fg=color)  # draw the text to the screen buffer

        app.screen.update()  # commit the changes to the screen

    app.run()  # block here until the app exits (press Escape!)
예제 #8
0
def main():
    app = App(mouse=True)
    frame_num = 0
    size_text = ""

    @app.on("start")
    def start():
        app.screen.show_cursor = True
        app.screen.print("App start, waiting 1 second.", 0, 0)
        app.screen.update()
        sleep(1)
    
    @app.on("frame")
    def frame():
        nonlocal frame_num
        app.screen.clear()
        app.screen.print(f"Frame: {frame_num}", 0, 0)
        app.screen.print("Press 'q' to quit.", 0, 1)
        app.screen.print(size_text, 0, 2)
        app.screen.update()
        frame_num += 1

    @app.on("resize")
    def resize():
        nonlocal size_text
        size_text = f"Resized to {app.screen.w}x{app.screen.h}"

    @app.on("key")
    def key(k):
        if k == "q":
            app.stop()
    
    @app.on("mouse")
    def mouse(m):
        app.screen.cursor_pos = (m.x, m.y)
    
    @app.on("before_stop")
    def before_stop():
        # Terminal is not reset; we can use the screen.
        app.screen.clear()
        app.screen.print("Before stop", 0, 0)
        app.screen.update()
        sleep(0.5)
    
    @app.on("after_stop")
    def after_stop():
        # Terminal is reset; we can print normally.
        print("After stop")
    
    # the following two method calls are equivalent to calling app.run()
    app.start() # this does not block
    # you could run code concurrently with the App here, but you should not
    # interact with its attributes outside of event listeners due to potential
    # concurrency issues.
    app.await_stop() # this blocks
    print("Exited")
예제 #9
0
def main():
    app = App()

    @app.on("key")
    def on_key(k):
        app.screen.clear()
        app.screen.print(repr(k), 1, 1)
        app.screen.update()

    app.start()
    app.await_stop()
예제 #10
0
def main():
    app = App()

    @app.on("frame")
    def on_frame():
        app.screen.print("Goodbye", 0,
                         0)  # this should be overwritten completely
        app.screen.print("こんにちは世界", 0, 0)  # double-width characters
        app.screen.print(
            " (Hello World)")  # this should follow the previous printout
        app.screen.update()

    app.start()
    app.await_stop()
예제 #11
0
def main():
    app = App()

    @app.on("frame")
    def on_frame():
        clock_time = time.strftime("%H:%M:%S", time.gmtime())
        app.screen.print("Clock: " + clock_time, 1, 1)
        try:
            app.backend.window_title = clock_time
        except:
            app.screen.print("Terminal does not support setting window title", 1, 2, fg=Color.rgb(1,0,0))
        app.screen.update()
    
    app.start()
    app.await_stop()
예제 #12
0
def main():
    app = App()
    run_count = 0

    @app.on("start")
    def start():
        nonlocal run_count
        run_count += 1
        app.screen.print("Run {}".format(run_count), 0, 0)
        app.screen.print("Press any key to restart...", 0, 1)
        app.screen.update()

    @app.on("key")
    def key(k):
        app.stop()

    app.run()
    app.run()
    app.run()
예제 #13
0
def main():
    app = App(mouse=True)

    in_buffer = []

    @app.input.on("raw_input")
    def on_raw(data):
        in_buffer.append(data)

    def print_escape(line, x, y):
        app.screen.print_pos = (x, y)
        for ch in line:
            fg = COL_FG
            bg = None

            cat = category(ch)
            if "C" in cat:
                fg = COL_CTRL
            elif "N" in cat:
                fg = COL_NUM
            elif "P" in cat:
                fg = COL_PUNC
            elif "Z" in cat:
                bg = COL_SEP
            app.screen.print(repr(ch).lstrip("'").rstrip("'"), fg=fg, bg=bg)

    @app.on("frame")
    @app.on("resize")
    def update():
        max_lines = app.screen.h
        n_lines = min(max_lines, len(in_buffer))
        lines = in_buffer[-n_lines:]

        app.screen.clear()
        for i, line in enumerate(lines):
            print_escape(line, 0, i)

        app.screen.update()

    app.start()
    app.await_stop()
예제 #14
0
def main():
    app = App()

    @app.on("start")
    def on_start():
        app.screen.print("Resize the terminal.", 1, 1, fg=Color.rgb(0, 1, 0))
        app.screen.update()

    @app.on("resize")
    def on_resize():
        app.screen.clear()
        app.screen.fill(0, 0, app.screen.w, app.screen.h, bg=Color(0, 255, 0))
        app.screen.fill(1,
                        1,
                        app.screen.w - 2,
                        app.screen.h - 2,
                        bg=Color(0, 0, 0))
        app.screen.print("{} x {}".format(app.screen.w, app.screen.h), 2, 2)
        app.screen.update()

    app.start()
    app.await_stop()
예제 #15
0
def main():
    app = App(framerate=60)

    @app.on("start")
    def on_start():
        app.buffer = Buffer(16, 4)
        app.buffer.clear(bg=Color.rgb(0.2, 0, 0), fg=Color.rgb(1, 0.5, 0.5))
        app.buffer.print("Hello world")

    @app.on("frame")
    def on_frame():
        t = time()
        app.screen.clear()
        app.screen.blit(
            app.buffer,
            round(app.screen.w / 2 + sin(t * 3) * 16) - app.buffer.w // 2,
            round(app.screen.h / 2 + cos(t * 1) * 4) - app.buffer.h // 2)
        app.screen.print(
            "Update time: {:.2f}ms".format(app.screen._update_duration * 1000),
            0, 0)
        app.screen.update()

    app.start()
    app.await_stop()
예제 #16
0
def main():
    backend = App().backend
    backend.enter_alt_buffer()
    colors = [
        Color.rgb(1, 0, 0),
        Color.rgb(1, 1, 0),
        Color.rgb(0, 1, 0),
        Color.rgb(0, 1, 1),
        Color.rgb(0, 0, 1),
        Color.rgb(1, 0, 1)
    ]

    t0 = perf_counter()
    frames = 0
    pos = 0

    while frames < 10000:
        backend.bg = colors[frames % len(colors)]
        backend.write(" ")
        backend.flush()
        frames += 1
        t1 = perf_counter()

    backend.exit_alt_buffer()
    backend.flush()

    tpf = (t1 - t0) / frames
    print("Terminal: {}\n".format(backend.terminal_name))
    print("Avg time per frame: {:.4f}\n".format(tpf))
    print("Avg framerate: {:.2f}\n".format(1 / tpf))
예제 #17
0
from datetime import datetime
from time import time
from termpixels import App, Color, SparseBuffer
from termpixels.util import splitlines_print

a = App()
b = SparseBuffer(0, 0)
b.extend_to(10, 10)

scroll_y = 0
autoscroll = False


def log(s):
    global scroll_y
    for line in splitlines_print(s):
        b.print(line, fg=Color.hsl(time() * 0.2, 0.5, 0.4))
        b.print_pos = (0, b.print_pos[1] + 1)
        b.extend_to(0, b.print_pos[1] + 1)
    if autoscroll:
        scroll_y = b.print_pos[1] - a.screen.h


@a.on("start")
@a.on("resize")
def resize():
    b.resize(a.screen.w, b.h)
    log("Use up/down arrow keys to scroll!")


@a.on("key")
from termpixels import App, Color

if __name__ == "__main__":
    # it's possible to not "start" an App and just use its terminal backend.
    term = App().backend
    term.fg = Color.rgb(1, 0, 1)
    term.write("Hello world\n")
    term.flush()

    term.fg = Color.rgb(1, 1, 0)
    term.flush()
    print("print() works too; just flush first.")
예제 #19
0
def main():
    app = App()
    inner_buffer = Buffer(0, 0)

    outer_pad = 1
    inner_w = 0
    inner_h = 0

    @app.on("start")
    @app.on("resize")
    @app.on("frame")
    def update():
        nonlocal inner_w, inner_h
        app.screen.clear()
        inner_buffer.clear()

        # compute inner dimensions of box
        inner_w = app.screen.w - outer_pad * 2 - 2
        inner_h = app.screen.h - outer_pad * 2 - 2
        inner_buffer.resize(inner_w, inner_h)

        # draw a box with a title
        draw_box(app.screen, outer_pad, outer_pad, inner_w + 2, inner_h + 2, chars=BOX_CHARS_LIGHT_DOUBLE_TOP)
        title = "Hello drawing"
        app.screen.print(title, app.screen.w // 2 - len(title) // 2, outer_pad)

        # draw spinners
        inner_buffer.print("Inner header ", 0, 0)
        draw_spinner(inner_buffer, *inner_buffer.print_pos, freq=2, fg=GREEN)
        inner_buffer.print(" ")
        draw_spinner(inner_buffer, *inner_buffer.print_pos, freq=2, fg=GREY, frames=SPINNER_PIPE)
        inner_buffer.print(" ")
        draw_spinner(inner_buffer, *inner_buffer.print_pos, freq=2, fg=GREY, frames=SPINNER_CLOCK)
        inner_buffer.print(" ")
        draw_spinner(inner_buffer, *inner_buffer.print_pos, freq=2, t=math.sin(time.perf_counter()), fg=YELLOW, frames=SPINNER_MOON)
        inner_buffer.print(" ")
        draw_spinner(inner_buffer, *inner_buffer.print_pos, freq=1, fg=WHITE, frames=SPINNER_DOTS)

        # draw a horizontal line along x=2 within the box
        draw_hline(inner_buffer, 2, fg=GREY)

        # print some word-wrapped text inside the box
        inner_buffer.print(wrap_text(LIPSUM, inner_w), 0, 3, fg=YELLOW)

        # progress bar
        draw_progress(inner_buffer, 0, inner_buffer.print_pos[1] + 2,
                      w=inner_buffer.w // 3,
                      progress=math.sin(time.perf_counter()) * 0.5 + 0.5,
                      fg=BRIGHT_YELLOW,
                      bg=GREY,
                      **PROGRESS_SMOOTH)

        # draw a color bitmap at 2x vertical resolution
        draw_colormap_2x(inner_buffer, SMILEY, 2, inner_buffer.h - 3 - 2, w=7, h=7)

        # copy box contents to screen
        app.screen.blit(inner_buffer, outer_pad + 1, outer_pad + 1)
        app.screen.update()
    
    app.start()
    app.await_stop()
예제 #20
0
from termpixels import App, Color, Buffer, SparseBuffer

a = App()
b = Buffer(16, 8)
sb = SparseBuffer(16, 8)

b.clear(bg=Color.rgb(0.5, 0, 0))
b.print("Hello Buffer!")
sb.clear(bg=Color.rgb(0, 0.5, 0))
sb.print("Hello Sparse!")


@a.on("frame")
def frame():
    a.screen.clear()
    a.screen.blit(b, 1, 1)
    a.screen.blit(sb, 1 + b.w + 1, 1)
    a.screen.print_pos = (1, 1 + b.h + 1)
    a.screen.print(f"Pixels in buffer: {b.w * b.h}\n")
    a.screen.print(f"Pixels in sparse buffer: {sb._pixel_count}\n")
    a.screen.update()


if __name__ == "__main__":
    a.run()
예제 #21
0
def main():
    app = App(mouse=True, framerate=60)
    app.mouse_x = 0
    app.mouse_y = 0
    app.mouse_px = 0
    app.mouse_py = 0
    app.particles = []

    @app.on("mouse")
    def on_mouse(m):
        app.mouse_x = m.x
        app.mouse_y = m.y

    @app.on("frame")
    def on_frame():
        dx = app.mouse_x - app.mouse_px
        dy = app.mouse_y - app.mouse_py
        l = math.sqrt(dx**2 + dy**2)
        d = math.atan2(dy, dx)

        j = min(45, int(l)) * 2
        for i in range(j):
            f = i / j
            ll = l * random.uniform(0.25, 0.5)
            dd = d + random.uniform(-0.2, 0.2)
            vx = ll * math.cos(dd)
            vy = ll * math.sin(dd)
            app.particles.append(
                Particle(app.mouse_px + dx * f, app.mouse_py + dy * f, vx, vy))

        app.screen.clear()
        for i, p in enumerate(app.particles):
            col = Color.hsl(i / len(app.particles), 1.0, 0.5)
            app.screen.print(" ", int(p.x), int(p.y), bg=col)
            p.update()
            if p.x < 0 or p.y < 0 or p.x >= app.screen.w or p.y >= app.screen.h:
                app.particles.remove(p)
        app.screen.update()

        app.mouse_px = app.mouse_x
        app.mouse_py = app.mouse_y

    app.start()
    app.await_stop()
예제 #22
0
def main():
    app = App(mouse=True, framerate=60)
    app.screen.show_cursor = True

    w = 0
    h = 0
    buf = None
    c_x = 0
    c_y = 0
    show_help = True

    @app.on("start")
    @app.on("resize")
    def make_buffers():
        nonlocal w, h, buf
        w = app.screen.w
        h = app.screen.h * 2
        buf = [P(**props["air"]) for y in range(w) for x in range(h)]

    @app.on("frame")
    def on_frame():
        app.screen.clear()
        app.screen.cursor_pos = (c_x, c_y)
        P.update(buf, w, h)
        P.draw(app.screen, buf, w, h)
        if show_help:
            app.screen.print(helptext, 1, 1, fg=Color.rgb(1, 1, 1))
        app.screen.update()

    def interact(place):
        nonlocal show_help
        show_help = False

        x = c_x
        y = c_y * 2
        if x >= 0 and x < w and y >= 0 and y < h:
            if place:
                buf[y * w + x] = P(color=Color.hsl(perf_counter() * 0.25, 1,
                                                   0.5),
                                   **props["sand"])
            else:
                buf[y * w + x] = P(**props["air"])

    @app.on("mouse")
    def on_mouse(m):
        nonlocal c_x, c_y

        c_x = m.x
        c_y = m.y

        if m.left:
            interact(True)
        if m.right:
            interact(False)

    @app.on("key")
    def on_key(k):
        nonlocal c_x, c_y

        if k == "left":
            c_x = max(0, c_x - 1)
        if k == "right":
            c_x = min(w - 1, c_x + 1)
        if k == "up":
            c_y = max(0, c_y - 1)
        if k == "down":
            c_y = min(w - 1, c_y + 1)
        if k == "z":
            interact(True)
        if k == "x":
            interact(False)

    app.start()
    app.await_stop()
예제 #23
0
SPEED_MIN = 0.5
SPEED_MAX = 1.5
LENGTH_MIN = 3
LENGTH_MAX = 8
HUE = 0.3 * 360


class Particle:
    def __init__(self):
        self.pos = 0
        self.speed = uniform(SPEED_MIN, SPEED_MAX)
        self.length = uniform(LENGTH_MIN, LENGTH_MAX)


a = App()
cols = []


@a.on("start")
@a.on("resize")
def start():
    global cols
    cols = [Particle() for x in range(a.screen.w)]
    a.screen.clear(fg=Color.rgb(0, 0, 0))


@a.on("frame")
def on_frame():
    for x, p in enumerate(cols):
        # update colors
예제 #24
0
def main():
    app = App(framerate=20)

    ui_height = 3
    game_buffer = Buffer(0,0)

    score = 0
    hiscore = load_hiscore()
    mode = None
    worm = None
    food_x = 0
    food_y = 0
    control_x = 0
    control_y = 0

    def move_food():
        nonlocal food_x, food_y
        w = game_buffer.w // 2
        h = game_buffer.h
        positions = [(x,y) for x in range(w) for y in range(h)
                     if not worm.intersecting(x, y)]
        food_x, food_y = choice(positions)

    @app.on("start")
    def start():
        nonlocal score, mode, worm
        score = 0
        worm = Worm(randint(0, app.screen.w - 1), randint(0, app.screen.h - 1))
        mode = MODE_GAME
    
    @app.on("start")
    @app.on("resize")
    def resize():
        game_buffer.resize(app.screen.w, app.screen.h - ui_height)
        move_food()

    @app.on("key")
    def key(k):
        nonlocal control_x, control_y
        if mode == MODE_GAME:
            if k == K_UP:
                control_y = -1
            if k == K_DOWN:
                control_y = 1
            if k == K_LEFT:
                control_x = -1
            if k == K_RIGHT:
                control_x = 1
        elif mode == MODE_GAMEOVER:
            if k == "\n":
                app.emit("start")

    def frame_game():
        nonlocal control_x, control_y
        nonlocal score, hiscore
        nonlocal mode
        
        # handle input
        if control_x != 0 and not worm.intersecting(worm.x + control_x, worm.y):
            worm.vx = control_x
            worm.vy = 0
            control_x = 0
        if control_y != 0 and not worm.intersecting(worm.x, worm.y + control_y):
            worm.vy = control_y
            worm.vx = 0
            control_y = 0

        if worm.intersecting(food_x, food_y):
            score += 1
            worm.length += 1
            hiscore = max(hiscore, score)
            save_hiscore(hiscore)
            move_food()
        
        game_buffer.clear()
        game_buffer.print("░░", food_x * 2, food_y)

        try:
            worm.update(game_buffer)
        except GameOver:
            mode = MODE_GAMEOVER
            multiply_buffer(game_buffer, 0.4)
            return
        
        # draw
        app.screen.clear()
        app.screen.print("~TermWorm~", 0, 0)
        app.screen.print("Score: {}".format(score), 0, 1, fg=Color.rgb(.7,.7,.7))
        app.screen.print("  Hi: {}".format(hiscore), fg=Color.rgb(.7,.7,0))
        draw_box(app.screen, 0, 2, app.screen.w, 1, chars=BOX_CHARS_DOUBLE)
        app.screen.blit(game_buffer, 0, ui_height)
        app.screen.update()
    
    def frame_gameover():
        dark = Color.rgb(0.2,0.2,0.2)
        app.screen.clear()
        app.screen.blit(game_buffer, 0, ui_height)
        print_hcenter(app.screen, "Game over!", y=3, fg=Color.rgb(1,1,0), bg=dark)
        print_hcenter(app.screen, "Press enter to restart", y=4, fg=Color.rgb(0.75,0.75,0.75), bg=dark)
        print_hcenter(app.screen, "Press escape to exit", y=5, fg=Color.rgb(0.75,0.75,0.75), bg=dark)
        app.screen.update()

    @app.on("frame")
    def frame():
        if mode == "game":
            frame_game()
        elif mode == "gameover":
            frame_gameover()
        else:
            raise Exception("Invalid mode: {}".format(mode))
    
    @app.on("after_stop")
    def after_stop():
        print("Thanks for playing TermWorm!")

    app.run()