[Microbit-Python] Game API objects I want

Larry Hastings larry at hastings.org
Wed Oct 7 10:26:44 CEST 2015



Having written two games on the micro:bit--one of which actually 
works!--and having some previous experience writing video games, I'd 
like to get up on my soap box and hold forth.

There are a couple of technologies that /nearly //every//game/ will 
use.  (One that /literally every game/ will use!)  And given that 
they're mildly complicated to do properly, and can consume a good chunk 
of the micro:bit's precious RAM to implement, I strongly suggest they be 
written in C++ and provided with MicroPython on the micro:bit.  I don't 
know if I'm up to writing (and debugging!) them on the micro:bit 
itself.  Maybe I could make a first pass in normal C++ on Linux, and 
someone else could port them to the micro:bit?

I can name three such technologies, as follows.


*1: A debounced accelerometer scaling object*

My slalom game simply reads the current accelerometer value, restricts 
it to a specific range, then scales it down to the range I want to use 
and uses the resulting value, like so:

    [-1024                    1023]  <- accelerometer
    [  0  |  1  |  2  |  3  |  4  ]  <- slalom column

Values lower than -1024 are ceilinged to -1024, values greater than 1023 
are floored to 1023.  I then add 1024 to the value, multiply by 5, 
divide by 2048, and call int().  I get a number from 0 to 4 
(inclusive).  Simple, done.

The problem is that it kind of sucks.  If you hold the micro:bit /just 
so/, on the border between two values (say 2 and 3) it quickly and 
seemingly-randomly bounces back and forth between the two.  This can 
make playing slalom kind of tricky.

Most of the code in the scrolly image demo and whack-a-mole game went 
into writing a debounced accelerometer position reader class. The idea 
is simple enough: instead of simply taking the range of the 
accelerometer you're interested in, and scaling it to the range you 
want, you also establish "dead zones" between regions like so:

    [-512                      511]  <- accelerometer
    [  0 |a| 1 |b| 2 |c| 3 |d| 4  ]  <- range with dead zones

Here I've labeled the dead zones with lowercase letters (a, b, c, d).  
Dead zones magnified for illustrative purposes; in practice they're much 
smaller.

The idea is, the value reported when you're in a "dead zone" depends on 
the previous value.  The dead zones establish dynamic low-water and 
high-water marks for how far you'd have to go to cross over into another 
zone.

For example, if on frame N you were clearly in zone 1:

    [  0 |a| 1 |b| 2 |c| 3 |d| 4  ]
                  *

And on frame N+1 you were in zone b:

    [  0 |a| 1 |b| 2 |c| 3 |d| 4  ]
                 *

the debounced reader would then report that you're still in zone 1. You 
have to force it all the way past the dead zone for it to report you as 
being in zone 2.  If, on a subsequent frame P, you were definitely in 
zone 2:

    [  0 |a| 1 |b| 2 |c| 3 |d| 4  ]
                          *

And on frame P+1 you were in zone b:

    [  0 |a| 1 |b| 2 |c| 3 |d| 4  ]
                 *

the debounced reader would then report that you're (still) in zone 2.


One way to think of it is that the dead zones prefer the zone nearest to 
the previous accelerometer reading.  So if the accelerometer is 
currently in the spot marked with an asterisk, we can pretend that the 
zones map out as follows:

        *
    [  0   | 1   | 2   | 3   | 4  ]

              *
    [  0 |   1   | 2   | 3   | 4  ]

                    *
    [  0 |   1 |   2   | 3   | 4  ]

                          *
    [  0 |   1 |   2 |   3   | 4  ]

                                *
    [  0 |   1 |   2 |   3 |   4  ]


Long story short, the reason the scrolly image demo is so pleasant to 
use is because of this debounced accelerometer class.  If I didn't have 
that, it'd have the same bouncing-between-two-values problem that the 
slalom game has.  I suggest that most games on the micro:bit will use 
the accelerometer, and if they simply scale it they'll have the bouncing 
problem.

Of course, my debounced accelerometer class appears to consume most of 
the RAM of the micro:bit.  And that's /after/ I hard-coded it to some of 
the behavior I wanted.  A general-purpose debounced accelerometer class 
might not even fit!

I could write a simple class that does this in C++, and let somebody 
else who understands MicroPython could port it to the runtime and get it 
working on the micro:bit.

I think the API would look something like this:

    class AccelerometerScalingDebouncer:
         def __init__(self, fn, input_min, input_max, output_min,
    output_max, dead_zone=0.08):
             ...
         def read(self):
             ...

fn would be the function to call to do a reading (like 
microbit.accelerometer.get_x).  input_min and input_max would establish 
the range we'd clip the input to.  output_min and output_max would 
establish the range of values returned (inclusive).  dead_zone would 
establish how big the dead zone around each value would be, and we could 
hide this from the students (or handwave it away, "don't worry about it, 
it does the right thing for you").

read() would read the current value, clip it, debounce it, scale it, and 
return the resulting value.

Again, you can see a working example of a class like this in my "scrolly 
image" demo.  I've attached it here so you don't have to go hunting for 
it.  It runs on the micro:bit.


*2: A debounced reader for all the buttons*

A moment's thought suggests: we want a debounced button class, too. Both 
my games used the buttons, and I had to write debouncing code for both.

The basic idea: if the user presses the button, you often want to 
process the button press exactly once.  So code like this doesn't work:

    next_frame = 0
    while True:
         delta = next_frame - microbit.time()
         if delta > 0:
             microbit.sleep(delta)
             continue
         next_frame = microbit.time() + 50
         if microbit.button_a.is_pressed():
            do_thing()

The problem: if the user holds down the button, you'll call do_thing() 
every 50ms.

I've seen people mention that there's actually an event queue in the 
DAL.  Maybe just exposing this somehow is sufficient?

I think what I want is an object like this:

    class ButtonDebouncer:
         def __init__(self, a=True, b=True, both=False, delay=100):
             ...
         def poll(self):
             ...
         def a_pressed(self):
             ...
         def b_pressed(self):
             ...
         def both_pressed(self):
             ...

You pass in to the constructor booleans indicating which buttons you 
care about: a, b, or both.  If both=True and either a=True or b=True, 
"delay" indicates how long to wait after one button is pressed before 
giving up and assuming we're not getting both buttons pressed.

If you press and hold button A, after the next time you call poll(), 
a_pressed() would return True /once/.  After that first time it would 
return False until you released A, called poll(), then pressed A and 
called poll().//

The object would internally use microbit.time() for time.  __init__ 
would call poll().

I tried to write an example of this in Python for the micro:bit. However 
my first try hit a MemoryError.  (Argh!  Double-argh!)  So I've attached 
my attempt, which you can consider a non-working mockup.


*3: A simple scheduler*

/Every/ realtime game would want this.  And I doubt people will write 
non-realtime games on the micro:bit.

The basic idea: we want to do something at future time x.  And we also 
might want to do something every y milliseconds.

Python actually ships with a module to do this, "sched".  The 
sched.scheduler class is powerful and general-purpose.  But it's way 
more general than we need, and also it provides an inconvenient 
callback-based API.  ("I don't like callback-based APIs, I always get 
them wrong." --GvR)

How about this:

    class Scheduler:
         def __init__(self):
             ...
         def schedule(self, event, delta, repeat=False):
             ...
         def clear(self):
             ...
         def run(self):
             ...


schedule() would add an event to the scheduler.  "event" is any object, 
"delta" is how many ms in the future we want the event to happen, and if 
repeat is True the event will recur every "delta" ms thereafter.

clear() would clear the current events from the schedule.

run() would sleep until the next scheduled event.  It would return the 
"event" object associated with the event.  If you run() and there are no 
currently scheduled events, it throws an exception.

I mocked this up in normal Python, attached.  I also made it easy to 
port to the micro:bit by simulating the microbit module.  To run on the 
microbit, just return the first couple paragraphs of stuff (it's marked) 
and add "import microbit" to the top.  Works fine.


What do you think?  Anyone interested enough to work on the micro:bit 
implementations of these?


//arry/
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://mail.python.org/mailman/private/microbit/attachments/20151007/5a369e90/attachment-0001.html>
-------------- next part --------------
A non-text attachment was scrubbed...
Name: button.debouncer.mockup.py
Type: text/x-python
Size: 2794 bytes
Desc: not available
URL: <https://mail.python.org/mailman/private/microbit/attachments/20151007/5a369e90/attachment-0002.py>
-------------- next part --------------
A non-text attachment was scrubbed...
Name: simple.scheduler.py
Type: text/x-python
Size: 3657 bytes
Desc: not available
URL: <https://mail.python.org/mailman/private/microbit/attachments/20151007/5a369e90/attachment-0003.py>


More information about the Microbit mailing list