From 45630789648817d2d7428357c0f32a9fb28a6c07 Mon Sep 17 00:00:00 2001 From: richardlt Date: Sat, 22 Aug 2026 10:42:49 +0000 Subject: [PATCH] feat(rollupdice): include the rollup dice software Rollup dice was a separate repository built against the SDK as it was then: dep for its dependencies, urfave/cli.v1, and the protobuf messages passed by value. It no longer built against matrix, and the README pointed at it rather than shipping it. The logic is unchanged, and so are the logo and the pip font, so the dice look the same on the panel: two faces, pips drawn through the caracter driver, and a reroll on the A button. Three things differ from the original: - the faces are placed from a faceSize constant rather than from hardcoded coordinates, so both of them and their pips share the same arithmetic - the engine draws from the global math/rand source rather than one seeded from time.Now(). The board has no RTC, so a clock-seeded generator would deal the same sequence at every boot; the runtime seeds the global source whatever the clock says - it rerolls on the A release only, where before it re-rendered on every button event, including the ones it ignores Being in-tree, it travels with the releases: it is in the default component list of the .deb, of the Alpine card script and of the tarball, so it is on the console without a second binary to install. ArgsUsage is the list printed when a component name is rejected, and it was missing getout, animate and light. All four names are added, not just the new one, so the error stops calling a valid component invalid. --- README.md | 2 +- docs/install.md | 2 +- fonts/diceValues.json | 12 ++++++ images/rollupdice.json | 19 +++++++++ main.go | 5 ++- packaging/deb/default | 2 +- rollupdice/daemon.go | 57 +++++++++++++++++++++++++++ rollupdice/engine.go | 21 ++++++++++ rollupdice/engine_test.go | 27 +++++++++++++ rollupdice/renderer.go | 71 ++++++++++++++++++++++++++++++++++ scripts/prepare-alpine-card.sh | 2 +- 11 files changed, 215 insertions(+), 5 deletions(-) create mode 100644 fonts/diceValues.json create mode 100644 images/rollupdice.json create mode 100644 rollupdice/daemon.go create mode 100644 rollupdice/engine.go create mode 100644 rollupdice/engine_test.go create mode 100644 rollupdice/renderer.go diff --git a/README.md b/README.md index 69ad522..2a3d611 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,7 @@ Video game console operating system that displays on a 16*9 RGB LED matrix. | | Device | The Device software allows you to change the luminosity of the LEDs. | | | | Blocks | A puzzle game, score a maximum of points by clearing complete lines. | | | | Getout | A labyrinth game, try to get out if you can. | | -| | Rollup dice | Random dice generator (https://github.com/gwenker/matrix-rollup-dice). | | +| | Rollup dice | Roll two dice, press A for a new throw. | | | | Animate | Player for animations generated with Glediator (http://www.solderlab.de/index.php/software/glediator). | | | | Light | Simple software to generate mood light. | | diff --git a/docs/install.md b/docs/install.md index 2a0bca1..7623024 100644 --- a/docs/install.md +++ b/docs/install.md @@ -25,7 +25,7 @@ That puts a static ARMv7 binary at `/usr/bin/matrix`, the data it renders from u Choose what it runs in `/etc/default/matrix`: ```sh -MATRIX_COMPONENTS="core device gamepad emulator demo zigzag yumyum clock draw blocks getout animate light" +MATRIX_COMPONENTS="core device gamepad emulator demo zigzag yumyum clock draw blocks getout animate light rollupdice" MATRIX_GAMEPAD_PORT=80 MATRIX_LOG_LEVEL=info ``` diff --git a/fonts/diceValues.json b/fonts/diceValues.json new file mode 100644 index 0000000..dae2d06 --- /dev/null +++ b/fonts/diceValues.json @@ -0,0 +1,12 @@ +{ + "name": "DiceValues", + "height": 5, + "caracters": { + "1": {"width": 5, "mask": [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]}, + "2": {"width": 5, "mask": [0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0]}, + "3": {"width": 5, "mask": [0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0]}, + "4": {"width": 5, "mask": [1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1]}, + "5": {"width": 5, "mask": [1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1]}, + "6": {"width": 5, "mask": [1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1]} + } +} diff --git a/images/rollupdice.json b/images/rollupdice.json new file mode 100644 index 0000000..569c53b --- /dev/null +++ b/images/rollupdice.json @@ -0,0 +1,19 @@ +{ + "name": "rollupdice", + "height": 7, + "width": 12, + "colors": [ + { "r": 0, "g": 0, "b": 0, "a": 0 }, + { "r": 255, "g": 255, "b": 255, "a": 1 }, + { "r": 0, "g": 0, "b": 0, "a": 1 } + ], + "mask": [ + 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, + 1, 1, 1, 2, 1, 0, 0, 0, 0, 0, 0, 0, + 1, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 1, + 1, 2, 1, 1, 1, 0, 0, 1, 2, 1, 2, 1, + 1, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 1, + 0, 0, 0, 0, 0, 0, 0, 1, 2, 1, 2, 1, + 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1 + ] +} diff --git a/main.go b/main.go index 9303319..0fde98e 100644 --- a/main.go +++ b/main.go @@ -19,6 +19,7 @@ import ( "github.com/richardlt/matrix/getout" "github.com/richardlt/matrix/internal/errors" "github.com/richardlt/matrix/light" + "github.com/richardlt/matrix/rollupdice" "github.com/richardlt/matrix/yumyum" "github.com/richardlt/matrix/zigzag" ) @@ -51,7 +52,7 @@ func main() { Usage: "[panic fatal error warning info debug]", }, }, - ArgsUsage: "[core emulator gamepad device zigzag yumyum demo clock draw blocks]", + ArgsUsage: "[core emulator gamepad device zigzag yumyum demo clock draw blocks getout animate light rollupdice]", Action: startAction, }}, } @@ -112,6 +113,8 @@ func startAction(ctx context.Context, cmd *cli.Command) error { cs = append(cs, component(func() error { return animate.Start(cmd.String("core-uri")) })) case "light": cs = append(cs, component(func() error { return light.Start(cmd.String("core-uri")) })) + case "rollupdice": + cs = append(cs, component(func() error { return rollupdice.Start(cmd.String("core-uri")) })) default: return errors.Errorf("invalid component name %q, expected one of %s", arg, cmd.ArgsUsage) } diff --git a/packaging/deb/default b/packaging/deb/default index db3929b..9291779 100644 --- a/packaging/deb/default +++ b/packaging/deb/default @@ -6,7 +6,7 @@ # # Every component is a separate process-worth of goroutines and one gRPC connection, so # on a small board it is worth trimming this to what you actually play. -MATRIX_COMPONENTS="core device gamepad emulator demo zigzag yumyum clock draw blocks getout animate light" +MATRIX_COMPONENTS="core device gamepad emulator demo zigzag yumyum clock draw blocks getout animate light rollupdice" # Port for the gamepad web app, the virtual controller played from a phone. MATRIX_GAMEPAD_PORT=80 diff --git a/rollupdice/daemon.go b/rollupdice/daemon.go new file mode 100644 index 0000000..4b15ce9 --- /dev/null +++ b/rollupdice/daemon.go @@ -0,0 +1,57 @@ +package rollupdice + +import ( + "github.com/sirupsen/logrus" + + "github.com/richardlt/matrix/sdk-go/common" + "github.com/richardlt/matrix/sdk-go/software" +) + +// Start the rollup dice software. +func Start(uri string) error { + logrus.Infof("Start rollup dice for uri %s\n", uri) + + r := &rollupDice{} + + return software.Connect(uri, r, true) +} + +type rollupDice struct { + engine *engine + renderer *renderer +} + +func (r *rollupDice) Init(a software.API) (err error) { + logrus.Debug("Init rollup dice") + + r.renderer, err = newRenderer(a) + if err != nil { + return err + } + + if err := a.SetConfig(&software.ConnectRequest_SoftwareData_Config{ + Logo: a.GetImageFromLocal("rollupdice"), + MinPlayerCount: 1, + MaxPlayerCount: 1, + }); err != nil { + return err + } + + return a.Ready() +} + +func (r *rollupDice) Start(uint64) { + r.engine = newEngine() + r.engine.roll() + r.renderer.print(r.engine.values) +} + +func (r *rollupDice) Close() { r.renderer.clean() } + +func (r *rollupDice) ActionReceived(slot uint64, cmd common.Command) { + if cmd != common.Command_A_UP { + return + } + r.engine.roll() + r.renderer.print(r.engine.values) +} diff --git a/rollupdice/engine.go b/rollupdice/engine.go new file mode 100644 index 0000000..0aec432 --- /dev/null +++ b/rollupdice/engine.go @@ -0,0 +1,21 @@ +package rollupdice + +import "math/rand" + +// diceCount dice are rolled together, one per half of the display. +const diceCount = 2 + +func newEngine() *engine { return &engine{} } + +type engine struct { + values [diceCount]int +} + +// roll draws a new face, one to six, for every die. The global source is used rather than +// one seeded from the clock: the board this runs on has no clock to seed from, and the +// runtime seeds the global source itself. +func (e *engine) roll() { + for i := range e.values { + e.values[i] = rand.Intn(6) + 1 + } +} diff --git a/rollupdice/engine_test.go b/rollupdice/engine_test.go new file mode 100644 index 0000000..3348680 --- /dev/null +++ b/rollupdice/engine_test.go @@ -0,0 +1,27 @@ +package rollupdice + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +// Only the six faces of the DiceValues font can be rendered, so a value outside one to +// six would print an empty face rather than a die. +func TestRollStaysOnTheFaces(t *testing.T) { + assert := assert.New(t) + + e := newEngine() + seen := map[int]bool{} + + for i := 0; i < 1000; i++ { + e.roll() + for _, v := range e.values { + assert.GreaterOrEqual(v, 1) + assert.LessOrEqual(v, 6) + seen[v] = true + } + } + + assert.Len(seen, 6) +} diff --git a/rollupdice/renderer.go b/rollupdice/renderer.go new file mode 100644 index 0000000..a24b6d9 --- /dev/null +++ b/rollupdice/renderer.go @@ -0,0 +1,71 @@ +package rollupdice + +import ( + "github.com/richardlt/matrix/sdk-go/common" + "github.com/richardlt/matrix/sdk-go/software" +) + +// A face is a square of this many pixels, which is what fits two dice on the 16x9 +// display: a one pixel border all around and a two pixel gap down the middle. +const faceSize = 7 + +func newRenderer(a software.API) (*renderer, error) { + faceLayer, err := a.NewLayer() + if err != nil { + return nil, err + } + + pipLayer, err := a.NewLayer() + if err != nil { + return nil, err + } + + cd, err := pipLayer.NewCaracterDriver(a.GetFontFromLocal("DiceValues")) + if err != nil { + return nil, err + } + + return &renderer{ + api: a, + faceLayer: faceLayer, + pipLayer: pipLayer, + caracterDriver: cd, + faceColor: &common.Color{R: 255, G: 255, B: 255, A: 1}, + pipColor: &common.Color{A: 1}, + }, nil +} + +type renderer struct { + api software.API + faceLayer, pipLayer software.Layer + caracterDriver *software.CaracterDriver + faceColor, pipColor *common.Color +} + +func (r *renderer) clean() { + _ = r.faceLayer.Clean() + _ = r.pipLayer.Clean() +} + +func (r *renderer) print(values [diceCount]int) { + r.clean() + + for i, value := range values { + originX := i * (faceSize + 2) + + for x := originX; x < originX+faceSize; x++ { + for y := 1; y <= faceSize; y++ { + _ = r.faceLayer.SetWithCoord(&common.Coord{X: int64(x), Y: int64(y)}, r.faceColor) + } + } + + // The pips of a value are one caracter of the DiceValues font, five by five, and + // the driver places it around its center. The background is left transparent so + // the face below shows through. + _ = r.caracterDriver.Render(rune('0'+value), + &common.Coord{X: int64(originX + faceSize/2), Y: int64(1 + faceSize/2)}, + r.pipColor, &common.Color{}) + } + + _ = r.api.Print() +} diff --git a/scripts/prepare-alpine-card.sh b/scripts/prepare-alpine-card.sh index 0734849..83b54f9 100755 --- a/scripts/prepare-alpine-card.sh +++ b/scripts/prepare-alpine-card.sh @@ -46,7 +46,7 @@ DEBUG=0 # core and device, plus every game. gamepad and emulator are left out: gamepad is the web # controller, unreachable without a network, and emulator is a development tool. Trim this # with --components if memory gets tight. -COMPONENTS="core device demo zigzag yumyum clock draw blocks getout animate light" +COMPONENTS="core device demo zigzag yumyum clock draw blocks getout animate light rollupdice" # Go's collector shares the single core with rendering, and the frame path allocates on # every frame, so the default collection rate shows up as a periodic stutter. Trading # memory for fewer collections is the right way round on a board that does nothing else.