▦ MATRIX THINKING

MICRO:BIT / A WORLD YOU CAN HOLD

Twenty-five lights.
One matrix of state.

A number describes the brightness of a pixel. A matrix describes the whole display. Change the matrix with one shared rule, then draw the new world.

Change the brightness of the whole world.

A subtracts one from every value. B adds one. Shake restores the original gradient.

01 / STATE

The world matrix W

Row first, column second. Choose a number to trace its change. Values stay between 0 and 9.

02 / TRANSFORMATION

A rule for the whole field

03 / DISPLAY

The same state, as light

MATRIX THINKING · 5 × 5

0 is off; 9 is the brightest. This is an illustrative LED view, not a calibrated hardware simulator.

Trace row 2 · before → after

Open the MakeCode project to run its simulator, edit the blocks or download it to a micro:bit. You can share the project link directly.

Interactive JavaScript preview of the four examples. It does not run Python or connect to a board. The expressions above describe the transformation; the complete programs are below.

WHAT MAKES THIS MATRIX THINKING?

Describe the world.
Then describe its change.

The central object is the whole state W: all 25 brightness values together. The picture is a view of that state. An input chooses a transformation, and the display shows its result.

01 / Represent

Keep values in a five-row matrix. Predict a cell, a row, then the whole field.

02 / Transform

Compute a new state from the old state. Add, subtract, shift, reflect or restore.

03 / Draw

Render each value as light. Drawing reads the matrix; it does not define the rule.

A shared rule is the starting point. The brightness programs still loop over values. micro:bit does not provide NumPy here. MicroPython stores the field in an Image; MakeCode uses a flat list in row order, with index row * 5 + col. The logical matrix is the same.

MicroPython brightness uses 0–9. MakeCode's LED API uses 0–255, so its drawing function converts with round(value * 255 / 9). The stored state remains 0–9 in all four examples.

MATRICES WITHOUT NUMPY

Small board. Whole-world operations.

The standard micro:bit Python environment does not run NumPy. A matrix is still a useful way to represent its whole display: five rows of five brightness values. Our programs implement a few operations themselves: change every brightness, shift all rows, or reflect the whole field.

These are matrix transformations, implemented with explicit loops inside small functions. They introduce the same way of thinking we later use with NumPy: describe the whole state, choose a rule, then display the result. See the micro:bit MicroPython documentation for the board's supported environment.

A possible next step: run Python with NumPy on a computer, for example from PyCharm, and send the resulting 25 brightness values to a micro:bit over USB. That needs a separate receiver program on the board. The four examples below run on the board independently; they do not require a computer running NumPy.

FOUR EXPERIMENTS / TWO WAYS TO BUILD

Blocks or MicroPython.
The same predictions.

EXPERIMENT 01 / BRIGHTNESS

A different brightness in every cell.

Store brightness values from 0 to 9. Start with a gradient, then apply the same arithmetic rule everywhere. A decreases; B increases; shake restores.

  1. Predict the next top row after B. Does its zero change too?
  2. Press B until every value is 9. Then press A. Can subtraction recover the gradient?
  3. Shake to restore. Change the initial matrix in the code and predict again.
Read the complete MicroPython program
from microbit import Image, display, button_a, button_b, accelerometer, sleep

# The matrix stores brightness. Different cells start with different values.
initial = Image(
    "01234:"
    "12345:"
    "23456:"
    "34567:"
    "45678"
)

def change_brightness(source, amount):
    # Apply the SAME arithmetic rule everywhere; keep values within 0..9.
    result = Image(5, 5)
    for row in range(5):
        for col in range(5):
            value = source.get_pixel(col, row) + amount
            value = max(0, min(9, value))
            result.set_pixel(col, row, value)
    return result

world = initial.copy()
display.show(world)
print(world)

while True:
    a = button_a.was_pressed()
    b = button_b.was_pressed()

    if accelerometer.was_gesture("shake"):
        world = initial.copy()
        display.show(world)
        print(world)
    elif a and not b:
        world = change_brightness(world, -1)
        display.show(world)
        print(world)
    elif b and not a:
        world = change_brightness(world, 1)
        display.show(world)
        print(world)

    sleep(20)
Read the complete MakeCode TypeScript program
// One logical 5 x 5 brightness matrix, stored row by row.
let world: number[] = []

function drawWorld() {
    for (let row = 0; row <= 4; row++) {
        for (let col = 0; col <= 4; col++) {
            // Data uses 0..9; MakeCode LEDs use 0..255.
            led.plotBrightness(col, row, Math.round(world[row * 5 + col] * 255 / 9))
        }
    }
}

function resetWorld() {
    world = [
        0, 1, 2, 3, 4,
        1, 2, 3, 4, 5,
        2, 3, 4, 5, 6,
        3, 4, 5, 6, 7,
        4, 5, 6, 7, 8
    ]
    drawWorld()
}

function changeBrightness(amount: number) {
    let nextWorld: number[] = []
    for (let brightness of world) {
        nextWorld.push(Math.max(0, Math.min(9, brightness + amount)))
    }
    world = nextWorld
    drawWorld()
}

input.onButtonPressed(Button.A, function () {
    changeBrightness(-1)
})

input.onButtonPressed(Button.B, function () {
    changeBrightness(1)
})

input.onGesture(Gesture.Shake, function () {
    resetWorld()
})

resetWorld()

EXPERIMENT 02 / SHIFT

One button. The whole world moves.

The five rows describe one picture. A shifts the world down by one row; B restores it. New cells at the top are zero, and the bottom row leaves the world.

  1. Predict the picture after one A press, then compare the matrix and LEDs.
  2. Predict how many presses will make the field empty. Explain why.
  3. Reset with B. Change the starting pattern, or make your own left-shift version.
Read the complete MicroPython program
from microbit import Image, display, button_a, button_b, sleep

# Each row describes five LEDs. Values are brightness, from 0 to 9.
initial = Image(
    "09000:"
    "09900:"
    "00000:"
    "00000:"
    "00000"
)

world = initial.copy()
display.show(world)
print(world)

while True:
    # A: transform the whole world by one row. The new top row is zero.
    if button_a.was_pressed():
        world = world.shift_down(1)
        display.show(world)
        print(world)

    # B: restore the original world and try a different prediction.
    if button_b.was_pressed():
        world = initial.copy()
        display.show(world)
        print(world)

    sleep(20)
Read the complete MakeCode TypeScript program
// The five strings are rows of ONE world, not five animation frames.
let world = ["09000", "09900", "00000", "00000", "00000"]

function drawWorld() {
    // This loop only displays the state. It does not move individual lights.
    for (let row = 0; row <= 4; row++) {
        for (let col = 0; col <= 4; col++) {
            if (world[row].charAt(col) == "9") {
                led.plot(col, row)
            } else {
                led.unplot(col, row)
            }
        }
    }
}

input.onButtonPressed(Button.A, function () {
    // Read every source row from the old world, then replace the whole array.
    world = ["00000", world[0], world[1], world[2], world[3]]
    drawWorld()
})

input.onButtonPressed(Button.B, function () {
    world = ["09000", "09900", "00000", "00000", "00000"]
    drawWorld()
})

drawWorld()

EXPERIMENT 03 / MIRROR

A mirror for the whole world.

Begin with an asymmetric L-shaped picture. A reflects left to right; B reflects top to bottom; shake restores. Brightness values stay the same, but their positions change.

  1. Predict which side the vertical line will move to after A.
  2. Try A twice, then B twice. Why does the starting picture return?
  3. Compare A then B with B then A. Change the starting pattern and find a picture that is unchanged by one reflection.
Read the complete MicroPython program
from microbit import Image, display, button_a, button_b, accelerometer, sleep

# One asymmetric picture. Each number is a pixel's brightness, from 0 to 9.
initial = Image("90000:90000:90000:99990:00000")


def mirror_world(source, left_right):
    # Read from the old image. Build the complete next world before drawing it.
    result = Image(5, 5)
    for row in range(5):
        for col in range(5):
            if left_right:
                value = source.get_pixel(4 - col, row)
            else:
                value = source.get_pixel(col, 4 - row)
            result.set_pixel(col, row, value)
    return result


world = initial.copy()
display.show(world)
print(world)

while True:
    a = button_a.was_pressed()
    b = button_b.was_pressed()
    if accelerometer.was_gesture("shake"):
        world = initial.copy()
        display.show(world)
        print(world)
    elif a and not b:
        world = mirror_world(world, True)
        display.show(world)
        print(world)
    elif b and not a:
        world = mirror_world(world, False)
        display.show(world)
        print(world)
    sleep(20)
Read the complete MakeCode TypeScript program
// One 5 x 5 brightness matrix, stored row by row.
let world: number[] = []

function drawWorld() {
    for (let row = 0; row <= 4; row++) {
        for (let col = 0; col <= 4; col++) {
            led.plotBrightness(col, row, Math.round(world[row * 5 + col] * 255 / 9))
        }
    }
}

function resetWorld() {
    world = [
        9, 0, 0, 0, 0,
        9, 0, 0, 0, 0,
        9, 0, 0, 0, 0,
        9, 9, 9, 9, 0,
        0, 0, 0, 0, 0
    ]
    drawWorld()
}

function mirrorWorld(leftRight: boolean) {
    let nextWorld: number[] = []
    for (let row = 0; row <= 4; row++) {
        for (let col = 0; col <= 4; col++) {
            if (leftRight) {
                nextWorld.push(world[row * 5 + (4 - col)])
            } else {
                nextWorld.push(world[(4 - row) * 5 + col])
            }
        }
    }
    world = nextWorld
    drawWorld()
}

input.onButtonPressed(Button.A, function () {
    mirrorWorld(true)
})
input.onButtonPressed(Button.B, function () {
    mirrorWorld(false)
})
input.onGesture(Gesture.Shake, function () {
    resetWorld()
})
resetWorld()

EXPERIMENT 04 / DUCK

Tilt the board. Mirror the duck.

Keep one unchanged duck matrix. Tilt left to reverse columns; tilt right to restore them. Lower the top edge to reverse rows; lower the bottom edge to restore them. Combine both axes.

  1. Predict where the beak and foot will go before tilting.
  2. Hold a tilt. Why does the picture stay still? Return to flat: the last orientation is kept inside the neutral band.
  3. Hold the board flat and shake to restore the initial duck. Change the initial matrix and try another asymmetric picture.
Read the complete MicroPython program
from microbit import Image, display, accelerometer, sleep

# The same 5 x 5 duck, always kept unchanged.
initial = Image("00090:00999:99990:09990:00900")


def axis_state(reading, previous):
    # A wide neutral band keeps a held tilt from repeatedly toggling the image.
    if reading < -300:
        return True
    if reading > 300:
        return False
    return previous


def reflect_world(source, left_right, top_bottom):
    result = Image(5, 5)
    for row in range(5):
        for col in range(5):
            source_row = 4 - row if top_bottom else row
            source_col = 4 - col if left_right else col
            result.set_pixel(col, row, source.get_pixel(source_col, source_row))
    return result


left_right = False
top_bottom = False
world = initial.copy()
display.show(world)

while True:
    if accelerometer.was_gesture("shake"):
        left_right = False
        top_bottom = False
        world = initial.copy()
        display.show(world)
        sleep(300)
    else:
        next_left_right = axis_state(accelerometer.get_x(), left_right)
        next_top_bottom = axis_state(accelerometer.get_y(), top_bottom)
        if next_left_right != left_right or next_top_bottom != top_bottom:
            left_right = next_left_right
            top_bottom = next_top_bottom
            world = reflect_world(initial, left_right, top_bottom)
            display.show(world)
    sleep(40)
Read the complete MakeCode TypeScript program
// One unchanged duck matrix. Each tilt chooses how to read its rows and columns.
let initialWorld = [
    0, 0, 0, 9, 0,
    0, 0, 9, 9, 9,
    9, 9, 9, 9, 0,
    0, 9, 9, 9, 0,
    0, 0, 9, 0, 0
]
let world: number[] = []
let leftRight = false
let topBottom = false

function drawWorld() {
    for (let row = 0; row <= 4; row++) {
        for (let col = 0; col <= 4; col++) {
            led.plotBrightness(col, row, Math.round(world[row * 5 + col] * 255 / 9))
        }
    }
}

function reflectWorld() {
    let nextWorld: number[] = []
    for (let row = 0; row <= 4; row++) {
        for (let col = 0; col <= 4; col++) {
            let sourceRow = row
            let sourceCol = col
            if (topBottom) {
                sourceRow = 4 - row
            }
            if (leftRight) {
                sourceCol = 4 - col
            }
            nextWorld.push(initialWorld[sourceRow * 5 + sourceCol])
        }
    }
    world = nextWorld
    drawWorld()
}

function resetWorld() {
    leftRight = false
    topBottom = false
    reflectWorld()
}

function updateTilt() {
    let previousLeft = leftRight
    let previousTop = topBottom
    let x = input.acceleration(Dimension.X)
    let y = input.acceleration(Dimension.Y)
    if (x < -300) {
        leftRight = true
    } else if (x > 300) {
        leftRight = false
    }
    if (y < -300) {
        topBottom = true
    } else if (y > 300) {
        topBottom = false
    }
    if (previousLeft != leftRight || previousTop != topBottom) {
        reflectWorld()
    }
}

input.onGesture(Gesture.Shake, function () {
    resetWorld()
    basic.pause(300)
})
resetWorld()
basic.forever(function () {
    updateTilt()
    basic.pause(40)
})

TAKE IT TO THE BOARD

Start in the editor.
Then try real LEDs.

  1. MakeCode Blocks: click Open in MakeCode above to load any project directly. You can also open MakeCode, choose Import → Import URL and paste its GitHub project address. Alternatively, download its HEX and choose Import File. The projects contain editable blocks.
  2. MicroPython: open the official Python Editor and load or paste the corresponding .py source. These are MicroPython programs, not MakeCode Python.
  3. Run the editor preview where available, or download and transfer the program to a micro:bit. Predict before each button press, then compare the real display with the matrix.

MakeCode examples were checked in the official simulator. MicroPython logic was checked on a computer with an API adapter. Physical-board testing is still to come.

All sources, HEX exports, teacher notes and validation records on GitHub ↗

Continue with Game of Life to see shifts and Boolean masks update a larger world with NumPy-style array operations.