Rozdział 12: Budujemy Nyan Cata! — Część 4: Platformy i podwójny skok 🪜

Nowe komendy terminala!

ps — lista procesów

ps aux

Pokazuje wszystkie działające procesy (programy) na komputerze! Dużo tego, nie? Przefiltrujmy:

ps aux | grep python

To pokaże tylko procesy związane z Pythonem. Pipe + grep — klasyka!

kill — zakończ proces

Jeśli Twoja gra się zawiesi (zdarza się!), możesz ją zabić:

ps aux | grep nyan_cat

Zobaczysz numer procesu (PID) — to pierwsza liczba. Potem:

kill NUMER_PID

Albo brutalniej: kill -9 NUMER_PID (siłowe zabicie). Ale zwykle Ctrl+C w terminalu wystarczy!

top — monitor systemu na żywo

top

To jak menedżer zadań w Windowsie! Widzisz procesy posortowane po zużyciu CPU. Naciśnij q żeby wyjść. (Pamiętasz htop? To ładniejsza wersja.)

Dodajemy platformy do gry!

cd ~/projekty/nyan-cat-runner
source venv/bin/activate
cp src/nyan_cat.py src/nyan_cat_v03.py
nano src/nyan_cat.py

Zamień cały plik:

#!/usr/bin/env python3
"""
NYAN CAT RUNNER v0.4
Platformy, trudność rośnie, high score!
"""
import curses
import time
import random

# --- STAŁE ---
GRAVITY = 1
JUMP_FORCE = -4
MAX_JUMPS = 2
BASE_SPEED = 0.08

CAT_FRAME_1 = [
    "  /\\_/\\  ",
    " ( o.o ) ",
    "  > ^ <  ",
    " /|   |\\ ",
    "(_|   |_)",
]

CAT_FRAME_2 = [
    "  /\\_/\\  ",
    " ( o.o ) ",
    "  > ^ <  ",
    "  |/  \\| ",
    " (_)  (_)",
]

CAT_DEAD = [
    "  /\\_/\\  ",
    " ( x.x ) ",
    "  > ^ <  ",
    "  /| |\\  ",
    " (_| |_) ",
]

RAINBOW_COLORS = "~=~=~=~=~="

OBSTACLE_CACTUS = [" ||| ", "/|||\\", " ||| ", " ||| "]
OBSTACLE_ROCK = [" @@@ ", "@@@@@", "@@@@@"]
OBSTACLE_BIRD = ["\\  /", " \\/ ", " /\\ ", "/  \\"]


def create_star(width, floor_y):
    return {
        "x": width + random.randint(5, 40),
        "y": random.randint(3, floor_y - 6),
        "symbol": random.choice(["★", "*", "✦"]),
        "collected": False,
    }


def create_obstacle(width, floor_y, difficulty):
    kinds = ["cactus", "rock"]
    if difficulty > 2:
        kinds.append("bird")

    kind = random.choice(kinds)

    if kind == "cactus":
        art = OBSTACLE_CACTUS
        y = floor_y - len(art)
    elif kind == "rock":
        art = OBSTACLE_ROCK
        y = floor_y - len(art)
    else:  # bird
        art = OBSTACLE_BIRD
        y = random.randint(3, floor_y - 10)

    return {
        "x": width + random.randint(20, 60),
        "y": y,
        "art": art,
        "width": max(len(line) for line in art),
        "kind": kind,
    }


def create_platform(width, floor_y):
    length = random.randint(6, 14)
    return {
        "x": width + random.randint(10, 50),
        "y": random.randint(floor_y - 15, floor_y - 7),
        "length": length,
        "symbol": "═" * length,
    }


def check_collision(y1, x1, h1, w1, y2, x2, h2, w2):
    return (x1 < x2 + w2 and x1 + w1 > x2 and
            y1 < y2 + h2 and y1 + h1 > y2)


def main(stdscr):
    curses.curs_set(0)
    stdscr.nodelay(True)
    stdscr.timeout(int(BASE_SPEED * 1000))

    curses.start_color()
    curses.use_default_colors()
    curses.init_pair(1, curses.COLOR_YELLOW, -1)    # gwiazdki
    curses.init_pair(2, curses.COLOR_RED, -1)        # przeszkody
    curses.init_pair(3, curses.COLOR_CYAN, -1)       # kot
    curses.init_pair(4, curses.COLOR_MAGENTA, -1)    # tęcza
    curses.init_pair(5, curses.COLOR_GREEN, -1)      # podłoga
    curses.init_pair(6, curses.COLOR_WHITE, -1)      # platformy
    curses.init_pair(7, curses.COLOR_RED, -1)        # game over

    height, width = stdscr.getmaxyx()

    # --- STAN GRY ---
    cat_x = 5
    floor_y = height - 3
    cat_y = float(floor_y - 5)
    velocity_y = 0.0
    jumps_left = MAX_JUMPS
    frame = 0
    score = 0
    high_score = 0
    scroll_x = 0
    lives = 3
    invincible = 0
    difficulty = 1  # rośnie z czasem!

    stars = [create_star(width, floor_y) for _ in range(5)]
    obstacles = [create_obstacle(width, floor_y, difficulty)]
    platforms = [create_platform(width, floor_y) for _ in range(2)]

    running = True
    game_over = False

    while running:
        # 1. KLAWISZE
        key = stdscr.getch()

        if game_over:
            if key == ord('r') or key == ord('R'):
                cat_y = float(floor_y - 5)
                velocity_y = 0.0
                jumps_left = MAX_JUMPS
                score = 0
                lives = 3
                difficulty = 1
                frame = 0
                stars = [create_star(width, floor_y) for _ in range(5)]
                obstacles = [create_obstacle(width, floor_y, difficulty)]
                platforms = [create_platform(width, floor_y) for _ in range(2)]
                game_over = False
                invincible = 0
            elif key == ord('q') or key == ord('Q'):
                running = False
            continue

        if key == ord('q') or key == ord('Q'):
            running = False
            continue

        if key == ord(' ') or key == curses.KEY_UP:
            if jumps_left > 0:
                velocity_y = JUMP_FORCE
                jumps_left -= 1

        # 2. FIZYKA
        velocity_y += GRAVITY
        cat_y += velocity_y

        # Lądowanie na podłodze
        on_ground = False
        if cat_y >= floor_y - 5:
            cat_y = floor_y - 5
            velocity_y = 0
            jumps_left = MAX_JUMPS
            on_ground = True

        # Lądowanie na platformach (tylko gdy spada!)
        if velocity_y > 0 and not on_ground:
            for plat in platforms:
                cat_bottom = int(cat_y) + 5
                if (cat_bottom >= plat["y"] and cat_bottom <= plat["y"] + 2 and
                    cat_x + 9 > plat["x"] and cat_x < plat["x"] + plat["length"]):
                    cat_y = float(plat["y"] - 5)
                    velocity_y = 0
                    jumps_left = MAX_JUMPS
                    on_ground = True
                    break

        if cat_y < 0:
            cat_y = 0
            velocity_y = 0

        # 3. AKTUALIZACJA
        frame += 1
        scroll_x += 1

        # Trudność rośnie!
        difficulty = 1 + score // 100

        # Prędkość gry rośnie z trudnością
        speed = max(0.04, BASE_SPEED - (difficulty - 1) * 0.005)
        stdscr.timeout(int(speed * 1000))

        if invincible > 0:
            invincible -= 1

        if frame % 5 == 0:
            score += 1

        # Przesuń obiekty
        move_speed = 1
        for star in stars:
            star["x"] -= move_speed
        for obs in obstacles:
            obs["x"] -= move_speed
        for plat in platforms:
            plat["x"] -= move_speed

        # Czyszczenie
        stars = [s for s in stars if s["x"] > -5 and not s["collected"]]
        obstacles = [o for o in obstacles if o["x"] > -10]
        platforms = [p for p in platforms if p["x"] > -20]

        # Nowe obiekty
        while len(stars) < 5:
            stars.append(create_star(width, floor_y))

        if len(obstacles) < 1 + difficulty and random.random() < 0.02 * difficulty:
            obstacles.append(create_obstacle(width, floor_y, difficulty))

        while len(platforms) < 2:
            platforms.append(create_platform(width, floor_y))

        # Kolizje z gwiazdkami
        for star in stars:
            if not star["collected"]:
                if abs(star["x"] - cat_x) < 8 and abs(star["y"] - int(cat_y) - 2) < 3:
                    star["collected"] = True
                    score += 10

        # Kolizje z przeszkodami
        if invincible == 0:
            for obs in obstacles:
                if check_collision(
                    int(cat_y), cat_x, 5, 9,
                    obs["y"], obs["x"], len(obs["art"]), obs["width"]
                ):
                    lives -= 1
                    invincible = 30
                    if lives <= 0:
                        game_over = True
                        if score > high_score:
                            high_score = score

        # 4. RYSOWANIE
        stdscr.erase()

        # Nagłówek
        diff_str = f"Lv.{difficulty}"
        header = f" NYAN CAT RUNNER | ★ {score} | ♥ {'❤' * lives}{'🖤' * (3-lives)} | {diff_str} | HI:{high_score} "
        stdscr.addstr(0, 0, header[:width-1], curses.A_BOLD)

        # Podłoga
        floor_line = ""
        for i in range(width - 1):
            if (i + scroll_x) % 4 == 0:
                floor_line += "█"
            else:
                floor_line += "▄"
        try:
            stdscr.addstr(floor_y, 0, floor_line, curses.color_pair(5))
        except curses.error:
            pass

        # Platformy
        for plat in platforms:
            if 0 < plat["x"] < width - 1:
                try:
                    display = plat["symbol"][:width - plat["x"] - 1]
                    stdscr.addstr(plat["y"], max(0, plat["x"]), display,
                                curses.color_pair(6) | curses.A_BOLD)
                except curses.error:
                    pass

        # Gwiazdki
        for star in stars:
            if not star["collected"] and 0 < star["x"] < width - 2:
                try:
                    stdscr.addstr(star["y"], star["x"], star["symbol"],
                                curses.color_pair(1) | curses.A_BOLD)
                except curses.error:
                    pass

        # Przeszkody
        for obs in obstacles:
            for i, line in enumerate(obs["art"]):
                y = obs["y"] + i
                x = obs["x"]
                if 0 <= y < height - 1 and 0 < x < width - len(line):
                    try:
                        stdscr.addstr(y, x, line, curses.color_pair(2))
                    except curses.error:
                        pass

        # Tęcza
        rainbow_y = int(cat_y) + 2
        if cat_x > len(RAINBOW_COLORS) and 0 <= rainbow_y < height - 1:
            try:
                stdscr.addstr(rainbow_y, cat_x - len(RAINBOW_COLORS),
                            RAINBOW_COLORS, curses.color_pair(4) | curses.A_BOLD)
            except curses.error:
                pass

        # Kot
        if game_over:
            current_cat = CAT_DEAD
            color = curses.color_pair(2)  # czerwony
        elif invincible == 0 or frame % 2 == 0:
            current_cat = CAT_FRAME_1 if (frame // 3) % 2 == 0 else CAT_FRAME_2
            color = curses.color_pair(3)
        else:
            current_cat = None

        if current_cat:
            for i, line in enumerate(current_cat):
                y_pos = int(cat_y) + i
                if 0 <= y_pos < height - 1:
                    try:
                        stdscr.addstr(y_pos, cat_x, line, color)
                    except curses.error:
                        pass

        # Game Over
        if game_over:
            box_w = 36
            box_h = 7
            by = height // 2 - box_h // 2
            bx = width // 2 - box_w // 2

            border = "╔" + "═" * (box_w - 2) + "╗"
            empty = "║" + " " * (box_w - 2) + "║"
            bottom = "╚" + "═" * (box_w - 2) + "╝"

            msg1 = "GAME OVER!"
            msg2 = f"Wynik: {score} punktów"
            msg3 = "[R] Jeszcze raz  [Q] Wyjście"

            try:
                stdscr.addstr(by, bx, border, curses.color_pair(7) | curses.A_BOLD)
                stdscr.addstr(by+1, bx, empty, curses.color_pair(7))
                stdscr.addstr(by+2, bx, f"║{msg1:^{box_w-2}}║", curses.color_pair(7) | curses.A_BOLD)
                stdscr.addstr(by+3, bx, f"║{msg2:^{box_w-2}}║", curses.color_pair(7))
                stdscr.addstr(by+4, bx, empty, curses.color_pair(7))
                stdscr.addstr(by+5, bx, f"║{msg3:^{box_w-2}}║", curses.color_pair(7))
                stdscr.addstr(by+6, bx, bottom, curses.color_pair(7) | curses.A_BOLD)
            except curses.error:
                pass

        # Pomoc
        help_text = "[SPACJA/↑]=Skok  [Q]=Wyjście"
        try:
            stdscr.addstr(height - 1, 0, help_text[:width-1])
        except curses.error:
            pass

        stdscr.refresh()

curses.wrapper(main)

Uruchom!

python3 src/nyan_cat.py

Nowości!

🏆 Wyzwanie

  1. Użyj diff src/nyan_cat_v03.py src/nyan_cat.py | wc -l — ile linii się zmieniło?
  2. Użyj ps aux | grep python podczas gdy gra działa (w drugim terminalu!)
  3. Zmień rozmiar platform (stała length w create_platform)
  4. Dodaj własną przeszkodę ASCII!