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.