01 / STATE
The world matrix W
Row first, column second. Choose a number to trace its change. Values stay between 0 and 9.
MICRO:BIT / A WORLD YOU CAN HOLD
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.
A subtracts one from every value. B adds one. Shake restores the original gradient.
01 / STATE
Row first, column second. Choose a number to trace its change. Values stay between 0 and 9.
02 / TRANSFORMATION
03 / DISPLAY
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?
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.
Keep values in a five-row matrix. Predict a cell, a row, then the whole field.
Compute a new state from the old state. Add, subtract, shift, reflect or restore.
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
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
EXPERIMENT 01 / BRIGHTNESS
Store brightness values from 0 to 9. Start with a gradient, then apply the same arithmetic rule everywhere. A decreases; B increases; shake restores.
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)// 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
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.
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)// 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
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.
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)// 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
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.
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)// 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
.py source. These are MicroPython programs, not MakeCode Python.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.