Skip to main content

Ralsina.Me — Roberto Alsina's website

CobraPy? CobraPew! Pew!

It has been a week and I had not re­al­ly touched Co­braPy my 80s-style python en­vi­ronem­nt at al­l. UN­TIL YES­TER­DAY.

So, how is it look­ing now? Like this, us­ing as­sets from the AWE­SOME ken­ney.nl:

I have added:

  • Sup­port for mu­sic (y­ou are lis­ten­ing to 80616-lunchip.xm)
  • Sup­port for sounds
  • Some im­prove­ments to the sprite en­gine (reusing tex­tures and such)
  • Bet­ter key­board sup­port, or at least one that's more use­ful for games
  • Fixed a bunch of bugs
  • Added col­li­sion de­tec­tion (did not make it to the video but it's there)

So let's go over the whole source code for that "game" and see how it work­s? Keep in mind that this code will look bizarre for your stan­dard Python pro­gram­mer be­cause it's de­lib­er­ate­ly done in a ... "ba­sic" style? Even in a BA­SIC style. Which means it aims to be more "struc­tured pro­gram­ming" than what you are used to see­ing.

Al­so, there are no guar­an­tees that any of the APIs ex­posed will even ex­ist to­mor­row, since this is still in a pro­to­typ­ing phase and things will change.

import time

load_sprite("player", "assets/PNG/playerShip3_orange.png")
x = 100
y = 500
step = 3
move_sprite("player", x, y)

We cre­at­ed the in­trepid play­er's ship, and put it "some­where". Sprites are iden­ti­fied by a name. All sprite op­er­a­tions will use that name to know what sprite should be af­fect­ed.

# A fleet of bad ships
fleet = [
    [f"enemy-1-{x}" for x in range(7)],
    [f"enemy-2-{x}" for x in range(7)],
    [f"enemy-3-{x}" for x in range(7)],
    [f"enemy-4-{x}" for x in range(7)],
]

for invader in fleet[0]:
    load_sprite(invader, "assets/PNG/Enemies/enemyBlack1.png")
for invader in fleet[1]:
    load_sprite(invader, "assets/PNG/Enemies/enemyBlue2.png")
for invader in fleet[2]:
    load_sprite(invader, "assets/PNG/Enemies/enemyGreen3.png")
for invader in fleet[3]:
    load_sprite(invader, "assets/PNG/Enemies/enemyRed4.png")

Again, just a few ships with dif­fer­ent look­s.


def move_fleet(x, y):
    """Move fleet so the left-top alien is in position x, y"""
    for inv_y, row in enumerate(fleet):
        for inv_x, invader in enumerate(row):
            move_sprite(invader, x + 90 * inv_x, y + 90 * inv_y)


fleet_x = 50
fleet_y = 50
fleet_step_x = 1
fleet_step_y = 5

A func­tion to move the fleet as a whole, and some in­fo about it, lo­ca­tion and speed.

load_sprite("laser", "assets/PNG/Lasers/laserBlue01.png")
load_sound("laser", "assets/Audio/laserRetro_004.ogg")
laser_last_shot = time.time()
laser_x = -100
laser_y = 0


def shoot():
    global laser_x, laser_y, x, y, laser_last_shot
    play_sound("laser")
    laser_x = x
    laser_y = y
    laser_last_shot = time.time()

Same for our lonely bullet, but we have a couple of new things. With load_sound we load into the program a sound (surprise!) and with play_sound it's played. Just like sprites, they are identified by name.

We keep track of when we shoot be­cause this is a ship, not a ma­chine­gun, Jim!

def check_hit():
    global laser_last_shot
    t1 = time.time()
    global laser_x
    for row in fleet:
        for invader in row:
            if check_collision("laser", invader):
                laser_x = -100
                laser_last_shot = 0

Col­li­sion de­tec­tion! If the bul­let hits an in­vad­er ... well, we can't make it ex­plode yet, but we move the bul­let out and al­low the us­er to shoot again, at least.

load_music_stream("background", "80616-lunchip.xm")
play_music_stream("background")

Just load and play a nice chip­tune! As usu­al, mu­sic is iden­ti­fied by a la­bel, so we can have more than one and play the one we wan­t.

def gameloop():
    global x, fleet_x, fleet_y, fleet_step_x, fleet_step_y, laser_y

The gameloop function is special. Whatever you put here CobraPy will try to run it 60 times a second. No faster, maybe slower if your gameloop takes too long. So this is where you do things like update the screen and interact with the user. You know ... a game loop.

    if is_pressed(114) and x < 720:  # right arrow
        x += step
    if is_pressed(113) and x > 0:  # left arrow
        x -= step
    if is_pressed(38): # a
        if time.time() - laser_last_shot > 1:
            shoot()

User interaction! Basically is_pressed takes a key identifier (nice constants coming soonish) and tells you if it's pressed or not. So, if right-arrow is pressed, increase x (within limits), if left-arrow is, decrease it (within limits). If "a" is pressed and you have not shot for a second, shoot.

    move_sprite("player", x, 500)

Well, that's why we in­creased / de­creased it, right?

    fleet_x += fleet_step_x
    move_fleet(fleet_x, fleet_y)
    if fleet_x < 50 or fleet_x > 150:
        fleet_step_x = -fleet_step_x
        fleet_y += fleet_step_y

The fleet moves to the right, then to the left, and at ev­ery turn, down. Does it re­mind you of any game you've seen?

    laser_y -= step
    move_sprite("laser", laser_x, laser_y)

Bul­let go up.

    check_hit()

En­e­my go boom. Even­tu­al­ly.

Audio! Navecitas!

Un­os min­u­tos eligien­do as­sets de ken­ney.n­l, un par de fun­ciones para que ten­ga so­porte de au­dio (sí, to­do el au­dio viene del código) ... ob­vi­a­mente no hay col­i­siones to­davía, y to­do es ... pre­cari­o.

El códi­go del jue­go: http://linkode.org/#E­Ja2EXWn­ToRI4zFr7vof15

The perfect Raspberry Pi setup

I am do­ing some semi-se­ri­ous Rasp­ber­ry Pi de­vel­op­men­t, so it was time I fig­ured out how to do it com­fort­ably.

My desk­top set­up is a two-­mon­i­tor con­fig­u­ra­tion, with my note­book on the ta­ble and a larg­er mon­i­tor above it. I like it, it's nice. The point­er nat­u­ral­ly goes from one screen to the oth­er in the ob­vi­ous way.

Desktop setup

Spe­cial­ly nice is that the lap­top's screen is touch and has an ac­tive pen, so I can use it nat­u­ral­ly.

But now, with the Rasp­ber­ry, I want to oc­ca­sion­al­ly show its dis­play. And that means switch­ing the mon­i­tor to it. Since I hate plug­ging and un­plug­ging things, I use one of the­se:

HDMI switch

It's a cheap tiny black plas­tic box that takes up to 5 HD­MI in­puts and switch­es be­tween them to its one out­put by click­ing a but­ton. It on­ly goes through the in­puts that have sig­nal, so since I on­ly have the lap­top's and the Pi's the but­ton tog­gles be­tween them.

If your mon­i­tor has more than one HD­MI in­put you can prob­a­bly just use that, but mine has just one.

But... what about key­board and mouse?

I could get a mul­ti­de­vice key­board and mouse, but I like the ones I have.

I could use a USB switch and tog­gle be­tween the two de­vices, but ... I don't have one.

So, I use bar­ri­er and con­fig­ure it in both the rasp­ber­ry pi and in the lap­top so that when my point­er goes "up" in­put goes to the Pi, and when it goes "down" in­put goes to the lap­top. That's ex­act­ly the same as with the du­al-dis­play se­tup, but with two com­put­er­s. Neat!

So, go ahead and con­fig­ure bar­ri­er. It's easy and there are tons of tu­to­ri­al­s.

Nex­t, make sure bar­ri­er starts when I lo­gin in­to both com­put­er­s. They way I pre­fer to do these things is us­ing sys­temd.

Put this in ~/.local/share/systemd/user/barrier.service in both machines:

[Unit]
Description=Barrier server
[Service]
Environment=DISPLAY=:0
Type=simple
TimeoutStartSec=0
ExecStart=/usr/bin/barrier
[Install]
WantedBy=default.target

Now you can make it start with systemctl --user start barrier or stop with systemctl --user stop barrier and make it start on every login with systemctl --user enable barrier

But while this is nice, it presents a prob­lem. When I am us­ing both dis­plays for the lap­top, I don't want bar­ri­er run­ning! Since I can't see the Pi's dis­play, it makes no sense.

So, I want to start bar­ri­er when the lap­top is us­ing one mon­i­tor, and stop it when it's us­ing two.

To do that, the trick is udev in the laptop. Put this (replacing my username with yours) in /etc/udev/rules.d/90-barrier.rules:

ACTION=="change", \
    KERNEL=="card0", \
    SUBSYSTEM=="drm", \
    ENV{DISPLAY}=":0", \
    ENV{XAUTHORITY}="/home/ralsina/.Xauthority", \
    ENV{XDG_RUNTIME_DIR}="/run/user/1000", \
    RUN+="/home/ralsina/bin/monitors-changed"

Basically that means "when there is a change in the configuration of the video card, run monitors-changed. Change the 1000 by your user ID, too.

The last piece of the puzzle is the monitors-changed script:

if `xrandr --listmonitors | grep -q HDMI`
then
    # The HDMI output is connected, stop barrier
    su ralsina -c '/usr/bin/systemctl stop --user barrier'
else
    # The Pi is using the monitor, start barrier
    su ralsina -c '/usr/bin/systemctl start --user barrier'
fi

And that's it!

This is the be­hav­iour now:

  • When the lap­­top is us­ing both dis­­­plays, they work nor­­mal­­ly in a "ex­­tend­ed dis­­­play" con­­fig­u­ra­­tion. They be­have like a sin­­gle large screen.

  • When I click on the HD­­MI switch and change the top dis­­­play to show the Pi's desk­­top, au­­to­­mat­i­­cal­­ly bar­ri­er starts in the lap­­top, and now the point­er and key­board change from one com­put­er to the oth­­er when the point­er moves from one mon­i­­tor to the nex­t.

  • If I click on the HD­­MI switch again, bar­ri­er stops on the lap­­top and I have a sin­­gle two-screen desk­­top again.

Ev­ery­thing be­haves per­fect­ly and I can switch be­tween com­put­ers by click­ing a but­ton.

Al­ter­na­tive­ly, we could start the bar­ri­er client when the rasp­ber­ry pi "get­s" the dis­play, and stops it when it goes away. The re­sult should be the same ex­cept for some cor­ner cas­es, but it has the added ben­e­fit of al­low­ing for a set­up with up to 5 de­vices :-)

The lost opportunity in test coverage

Dis­claimer: This is a bit of a rant, but it's a friend­ly rant :-)

When peo­ple look at code cov­er­age, they are read­ing it wrong.

Suppose you have a class, something stupid, like your own implementation of a stack, called Stack. Because you are not a total monster, you have tests in your code right? In fact, you are claiming that you are doing TDD (Test Driven Development), or at least you like TDD, or you would like the idea of TDD, or, let's be honest here, you just say you are doing TDD, but what you do is you sprinkle the tests you feel are needed, which is largely OK, I am not going to judge you, you freak.

And then you add test cov­er­age check­s, and it says: 80%

What most peo­ple feel when they see that is dread. They see that 80% and feel "OM­FG, my tests suck! I don't have enough! If even 100% cov­er­age is not enough then this 80% means my code is an un­sta­ble piece of garbage!"

Well, no.

Whether your code is good or not is in­de­pen­dent of test­s. Tests give you the abil­i­ty to know if your code is crap or not... some­times. What tests re­al­ly give you (if they are not to­tal garbage in them­selves) is the con­fi­dence that you can change your code with­out sig­nif­i­cant­ly af­fect­ing the be­hav­iours the tests are test­ing.

So, if your tests of Stack ensure that:

  • Stack.push puts the element at the top
  • Stack.pop gets the top element
  • Your stack can hold as many el­e­ments as your re­quire­ment de­fines (may be in­finite)

Then what you implemented is a stack. Period. It works. It's fine. It may be inefficient, it may be ugly, who knows, but tests are not going to give you good taste. All they are going to do is ensure that Stack is, indeed, a stack, and behaves like a stack, and that when you stick your mittens in it and change things inside it it stays a stack.

Yet, your cov­er­age is 80%.

Should you add more test­s?

No.

You should delete 20% of your code.

Since code is a li­a­bil­i­ty and the as­set is the code's be­haviour, then that's what the first D in TDD is for.

Test Driv­en De­vel­op­men­t.

Use the tests to de­fine the be­hav­iour you wan­t. Then add code to im­ple­ment that be­hav­iour.

Don't chase use­less stats like cov­er­age.

If cov­er­age is not 100%, con­sid­er your test­s.

Is there be­hav­iour you want that is not rep­re­sent­ed as a sce­nario in a test?

If yes: then add test­s.

If not: re­move code.

And us­ing "cov­er­age is low" as an op­por­tu­ni­ty to delete code in­stead of adding tests is some­thing a lot of de­vel­op­ers mis­s.


Contents © 2000-2024 Roberto Alsina