Skip to content

About

A scoresheet for e-ink tablets. Two to four players, rounds with running subtotals, an in-app keypad, and a per-series tally of who has won how many. Deliberately monochrome.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

7 Commits

Folders and files

Repository files navigation

Scorekeeper

A scoresheet for colour e-ink tablets — pencil-and-paper scoring for card and board games, for two to four players, with a running tally of who has won how many.

It knows nothing about any particular game. There are no rules, no scoring logic, no dealer. It replaces the back of an envelope, and nothing else.

Rivalries Scoresheet
Rivalries, each with its win tally The sheet — bars ruled under the addends
Finishing a game Standings in dark mode
Recording a winner Standings, in dark mode

Screenshots from a BOOX Note Air4 C, 10.3" Kaleido 3.

Using it

A rivalry is the same game played by the same people, more than once: a name, two to four players, and whether the highest or lowest score wins. Tapping a rivalry goes straight to a scoresheet — the game in progress if there is one, otherwise a new game started on the spot. Nobody opens a scorekeeper to admire a menu.

Each round is a row, and a subtotal follows it under a ruled addition bar — the pencil-and-paper convention, where the line is not part of any number but sits between the addends and their total. A ◆ marks whoever is ahead — all of them, if it's a tie.

Round 1 gets no subtotal, for the same reason you'd never rule a line under the first figure you wrote down: a bar needs two addends above it. From round 2 on, each total becomes the next round's addend, exactly as a running total works on paper.

Entry is free-form. Tap any cell, in any order, and type. Nothing insists you go round the table in player order, because people call out their scores in whatever order they finish counting. The only thing the app needs told is when a round is done, which is what the keypad's big key is for — and the sheet shows only rounds that exist, so there is no permanently blank row at the bottom carrying a subtotal that repeats the one above it.

That key does the whole flow, and changes to match what's in front of you:

  • the current round has scores in it → Finalize round, which closes it and adds the next
  • the current round is empty → you are evidently done, so it offers Finish game

It morphs rather than greying out, so it is never a dead key.

Finish records the winner. It proposes the leader, and insists you choose when the field is tied, because a scorekeeper that quietly picks one of two tied players has recorded a result nobody agreed to. Finishing a game starts the next one. A finished game can be reopened from the standings and corrected, figures and winner both; the tally follows.

The player names are pinned above the scroll. A long game runs the early rounds off the top of the screen, and a column of figures with no name over it is useless.

Designing for colour e-ink

The app is monochrome, and that is not minimalism. A Kaleido panel puts a colour filter over a monochrome screen: black renders at full resolution, colour at half, and every hue arrives desaturated and dimmed. A sheet of figures is sharper in black. The two projects beside this one needed colour to tell players apart; a scoresheet has columns for that.

What's left is greys, and they are chosen against two specific failure modes:

  • Every grey is an exact neutral — R, G and B identical, in both themes. E-ink devices commonly ship with colour enhancement on, which multiplies saturation; a conventional UI grey like #6F757C carries a blue cast that the boost amplifies into a visibly blue table. #B0B0B0 has nothing to amplify. This includes Material's surfaceContainer roles, set explicitly because the baseline is a purple-tinted neutral and it is what dropdown menus fill with.

    Dark mode was originally exempted from this rule, on the reasoning that dark mode is for a backlit screen. It isn't — the theme is a toggle in the app, on the same e-ink tablet, so it goes through the same enhancement. Its greys carried casts up to forty points and the panel rendered a navy page ruled in bright blue. Every value was replaced with the neutral of identical relative luminance, so nothing about the contrast changed and the colour simply left.

  • Nothing important is carried by a pale fill. The panel resolves roughly sixteen levels, so a very pale grey can snap to white and vanish. Subtotals were originally banded #E0E0E0, about two levels below white — a bet that only just paid. The addition bar retired it: the bar is ink, the strongest value in the palette, so the one distinction the sheet cannot afford to lose is now drawn in the one colour that cannot disappear.

ratio on paper notes
ink #101010 19.03:1 every figure, and the addition bar
muted #484848 9.15:1 row labels
rule #B0B0B0 2.17:1 the grid

Row types are distinguished four ways over: the bar, the bold total, the row label (Round 4 in ink against Subtotal in grey), and a shorter row. Losing any one of them still leaves the sheet readable.

Everything else follows from ghosting and refresh behaviour:

  • No ripples, and no press indication either. Suppressing ripples is the well-known half; Modifier.clickable takes its feedback from LocalIndication, whose default paints a flat 30% black wash over the whole component while held. Left in, every keypad key darkened an 85 dp rectangle on press and repainted it on release — two extra panel updates per digit, on the control tapped fastest.
  • Nothing animates. No smooth scrolling, no Material text-field label float, no tonal elevation. Dialogs are a 3 dp black outline on paper, because a tonal grey lifted off the background is either invisible or muddy.
  • The launch window is white, because that is what paints before Compose draws its first frame and a dark one flashes grey on every cold start.
  • Glyphs that aren't guaranteed are drawn instead. The leader diamond is four lines of Canvas, not ◆ — on a device with a trimmed font set a missing glyph renders as a tofu box or gets picked up by the emoji font and arrives in colour.

The keypad

In-app, fourteen fixed keys, always visible. The system keyboard was the obvious alternative and the wrong one: it is an alphabetic layout with a numeric strip, it animates in and out, and it repaints half the screen doing so — a visible flash on every score. Negative rounds are real in plenty of games, so ± is a first-class key.

The digits grey out when no cell is armed, rather than looking live and silently swallowing presses.

It scales on a tighter curve than the rest of the chrome. Chrome scale on a 10.3 inch panel works out at 1.74, and applied to the keypad that gave 80 dp keys in a 386 dp block — 29% of the screen, buying no legibility and costing a round of scoresheet.

Layout

Two scale factors. Chrome scales from whichever screen dimension is tighter — width alone over-scales a 4:3 tablet, and the taller rows then eat the space the sheet needs for rounds. Column widths come from the available width, with the label column clamped so it neither crowds the figures on a phone nor sprawls on a tablet.

The header row and every body row are built by one shared row function from the same two widths, so the names cannot drift out of alignment with the columns beneath them. An earlier mockup drew the header separately and it sat one padding value out — the sort of bug that looks like a rounding error and is actually two independent sources of truth.

Below 600 dp the Standings and Finish buttons collapse into an overflow menu; laid out flat they need more width than a phone has.

The column rules run the full height of the sheet, not just as far as the last round. A game three rounds into a tall screen otherwise left two thirds of the page blank under a table that stopped dead, which reads as truncated rather than as having room left. The empty rows are not ruled horizontally, though — a blank ruled row looks like something you can tap and isn't.

Persistence

Written to SharedPreferences after every change, as JSON. Rounds are a ragged list of lists of nullable integers, and hand-rolling a delimiter format for that is how you end up unable to tell an empty cell from a zero — org.json ships with the platform, so this costs no dependency.

A blank cell is not zero. Blank means "hasn't entered yet"; zero means "scored nothing". Conflating them makes the subtotals lie about who is still to play, and makes whoever hasn't played yet the runaway leader of every low-wins game.

Subtotals are not stored. They are sums of the rounds above them, and storing them would create a second source of truth that could disagree with the first. The one thing a scoresheet must never do is show a total that doesn't match the column.

An unreadable save is preserved, not discarded. load returns an empty state, and without care the next edit would serialise that empty state straight over the only copy of every rivalry; the raw string is copied aside first. A save written by a newer version is treated the same way rather than being read optimistically.

The keypad is flushed on ON_STOP, since digits typed into a cell only become a stored score when focus moves.

Build

Android Studio → Open, pick this folder, sync, Run. Pinned for Android Studio Hedgehog (2023.1.1): AGP 8.2.0-rc03, Kotlin 1.9.22, Gradle 8.2, compileSdk 34, Compose BOM 2024.02.00. Requires Android 8.0. No permissions, no network, no analytics.

There is no gradlew in the repo; the IDE reads the version from gradle/wrapper/gradle-wrapper.properties. Run gradle wrapper once if you want a command-line build.

Source layout

app/src/main/java/com/moira/scorekeeper/
  Model.kt           Rivalry/Game value types, derived totals and tally, JSON save/load
  ScoreViewModel.kt  screens and back stack, keypad buffer, rivalry editing, finishing
  Theme.kt           light (e-ink) and dark palettes, scale, ripple and indication suppression
  Sheet.kt           the scoresheet, the pinned header row, the keypad
  Screens.kt         rivalry list with the win tally, standings and game history
  Dialogs.kt         new rivalry, finish game, rename, players, delete
  Widgets.kt         the pill button and the drawn diamond
  MainActivity.kt    routing and the lifecycle flush

Licence

MIT — see LICENSE.

About

A scoresheet for e-ink tablets. Two to four players, rounds with running subtotals, an in-app keypad, and a per-series tally of who has won how many. Deliberately monochrome.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages