Tinylib
DiscoverDocsSign in
Sign in
Tinylib
AboutDiscoverDocsMake an appPricingTermsPrivacyDeveloper TermsContact

Start

  • Quickstart
  • How Tinylib works

Guides

  • Data
  • Rooms and rules
  • Games
  • Friends
  • Places and links
  • Reminders and notifications
  • Publishing
  • Errors and offline
  • Platforms

Client API

  • tinylib
  • tinylib.data
  • tinylib.rooms
  • tinylib.invitations
  • tinylib.friends
  • tinylib.places
  • tinylib.reminders
  • Types
  • tinylib-sdk/kits
    • Dates and randomness
    • Daily results
    • Solo games
    • Clocks
    • Daily reminder
    • Turn games, in rules
    • Seats, in rules
    • Games with friends
    • Game pages
    • Playing with no signal
    • Connection
    • People
    • Screens
    • Words

Rules API

  • Kinds
  • ctx

Reference

  • tinylib.json
  • Command line
  • Limits

Plain text

  • llms.txt
  • llms-full.txt

tinylib-sdk/kits

Your page imports these from tinylib-sdk/kits, and publish bundles them into the version. Your rules import the ones that need no tinylib: the rule kits below, addDays, seeded, shuffled, clock, and today given a date. Games shows them in use.

JavaScript
import { daily, followGames, today } from 'tinylib-sdk/kits'

Dates and randomness

today()

today(date?: Date): string
Returns the date on Tinylib's clock in the device's time zone, as 'YYYY-MM-DD'.
JavaScript
const day = today() // '2026-10-20'

Parameters

dateDateoptional
Defaults to now on Tinylib's clock.

addDays()

addDays(day: string, days: number): string
Returns the date days after day, both as 'YYYY-MM-DD'. A negative days counts back.
JavaScript
const yesterday = addDays(today(), -1)

Throws

TypeError
When day isn't a date as 'YYYY-MM-DD'.

seeded()

seeded(seed: string): () => number
Returns a function like Math.random whose sequence is fixed by seed. The same seed gives the same sequence on every device, so seeding with the date deals everyone the same puzzle.
JavaScript
const random = seeded(`hard/${today()}`) const target = 100 + Math.floor(random() * 900)

shuffled()

shuffled<T>(items: readonly T[], random?: () => number): T[]
Returns a shuffled copy of items.
JavaScript
const tiles = shuffled(letters, seeded(`tiles/${today()}`))

Parameters

itemsreadonly T[]
The items to shuffle.
random() => numberoptional
A function like Math.random, such as one from seeded. Defaults to Math.random.

Daily results

daily()

daily(name: string): Daily
Returns the daily puzzle name, which saves one result per date in tinylib.data with audience: 'friends', so leaderboard can rank it among friends' results. Saving needs "friends" in uses.
JavaScript
const hard = daily('hard') hard.save({ seconds: 312 }, day) const days = hard.streak()

Parameters

namestring
The puzzle's name: letters, digits, - and _, starting with a letter or digit.

Throws

TypeError
When name isn't a valid name.

Daily

interface Daily
The user's results of one daily puzzle, from daily.

Methods

key(day?)
Returns the tinylib.data key the result for day is saved under, 'day/<name>/YYYY-MM-DD'.
get(day?)
Returns the user's result for day, or undefined when they have none.
save(result, day?)
Saves result for day with audience: 'friends' and returns true.
played()
Returns the dates the user has a result for, as 'YYYY-MM-DD', oldest first.
streak(from?)
Returns how many days in a row the user has a result for, ending at from.

puzzle.key()

puzzle.key(day?: string): string
Returns the tinylib.data key the result for day is saved under, 'day/<name>/YYYY-MM-DD'. Pass it to leaderboard or friendsWith for friends' results that day.
JavaScript
const rows = await leaderboard(hard.key(day), (a, b) => a.seconds - b.seconds)
Parameters
daystringoptional
'YYYY-MM-DD'. Defaults to today.

puzzle.get()

puzzle.get<T = unknown>(day?: string): T | undefined
Returns the user's result for day, or undefined when they have none.
JavaScript
const result = hard.get(day) // { seconds: 312 }
Parameters
daystringoptional
'YYYY-MM-DD'. Defaults to today.

puzzle.save()

puzzle.save(result: unknown, day?: string): boolean
Saves result for day with audience: 'friends' and returns true. When this device already has a result for day, it keeps that one and returns false.
JavaScript
if (!hard.save({ seconds: 312 }, day)) showResult(hard.get(day))
Parameters
resultunknown
Any JSON value, such as { seconds: 312, mistakes: 1 }.
daystringoptional
'YYYY-MM-DD'. Defaults to today.
Throws
TypeError
When result can't be saved as JSON.
limit
When it would take the user's data from your client past 1 MB.

puzzle.played()

puzzle.played(): string[]
Returns the dates the user has a result for, as 'YYYY-MM-DD', oldest first.
JavaScript
const played = new Set(hard.played())

puzzle.streak()

puzzle.streak(from?: string): number
Returns how many days in a row the user has a result for, ending at from. When from has no result yet, it counts from the day before, so a streak lasts until a whole day is missed.
JavaScript
streakLabel.textContent = `${hard.streak()}-day streak`
Parameters
fromstringoptional
'YYYY-MM-DD'. Defaults to today.

leaderboard()

leaderboard<T = any>( key: string, compare: (a: T, b: T) => number, options?: { mine?: T; valid?: (value: unknown) => boolean }, ): Promise<Standing<T>[]>
Resolves with the user's and each friend's value under key, best first by compare. On a tie, the user's row comes first.
JavaScript
const rows = await leaderboard(hard.key(day), (a, b) => a.seconds - b.seconds, { valid: (value) => typeof value?.seconds === 'number', }) for (const row of rows) renderRow(row.you ? 'You' : row.name, clock(row.value.seconds))

Parameters

keystring
The key each value is saved under, such as daily('hard').key().
compare(a: T, b: T) => number
As Array.prototype.sort takes: negative when a is better than b.
options{ mine?: T; valid?: (value: unknown) => boolean }optional
options.mineToptional
The user's value, for example to rank a result before it's saved. Defaults to tinylib.data.get(key). When it's undefined, the user has no row.
options.valid(value: unknown) => booleanoptional
Returns false for a friend's value to leave out, such as one saved by an older version of your app.

Rejects with

offline
When the device is offline.
refused
When your app doesn't list "friends" in uses, or the user is a guest.

Standing

interface Standing<T = unknown>
One row of leaderboard: the user's or a friend's.

Properties

idstring
The user's or the friend's id.
namestring
The user's or the friend's name.
valueT
The value under the key, or options.mine in the user's row when leaderboard was given it.
youboolean
True for the user's own row.

friendsWith()

friendsWith(key: string): Promise<FriendResult[]>
Resolves with each friend's value under key, with the friend's name, in the order of tinylib.friends.list(). Friends without a value are left out.
JavaScript
const friends = await friendsWith(hard.key(day)) // [{ id, name: 'Ben', value: { seconds: 280 }, changedAt }]

Parameters

keystring
A key friends save values under with audience: 'friends', such as daily('hard').key().

Rejects with

offline
When the device is offline.
refused
When your app doesn't list "friends" in uses, or the user is a guest.

FriendResult

interface FriendResult
A friend's value under one key, from friendsWith.

Properties

idstring
The friend's id.
namestring
The friend's name.
valueunknown
The value the friend saved.
changedAtnumber
When the value last changed, in milliseconds on Tinylib's clock. Compare it with tinylib.now().

Solo games

solo()

solo(name: string, better?: (a: any, b: any) => boolean): SoloGames
Returns the solo games of the puzzle name.
JavaScript
const games = solo('sudoku') games.keep(TODAY, { grid }) const { record } = games.record('hard', seconds)

Parameters

namestring
The puzzle, as daily takes it.
better(a: any, b: any) => booleanoptional
Says whether score a beats b. Lower wins by default.

SoloGames

interface SoloGames
The solo games of a puzzle, from solo.

Properties

resultsDaily
The day's results, from daily(name).

Methods

key(mode)
Returns the data key of the game kept in mode, for watching it change.
saved(mode)
Returns the game kept in mode, or null.
keep(mode, game)
Keeps game in mode.
clear(mode)
Removes the game kept in mode.
best(mode)
Returns the best score in mode, or null.
record(mode, score, options?)
Keeps score as the best in mode when it beats it, with options as tinylib.data.set takes them, and returns the best from before it and whether this is a record.
week(day?)
Returns the seven days ending day, each with its date, its result, and whether it's that day.

SoloGames.key()

SoloGames.key(mode: string): string
Returns the data key of the game kept in mode, for watching it change.

SoloGames.saved()

SoloGames.saved(mode: string): any
Returns the game kept in mode, or null. Today's counts only on its day.

SoloGames.keep()

SoloGames.keep(mode: string, game: object): void
Keeps game in mode. Today's stays on game.day, or on today when it has none.

SoloGames.clear()

SoloGames.clear(mode: string): void
Removes the game kept in mode.

SoloGames.best()

SoloGames.best(mode: string): any
Returns the best score in mode, or null.

SoloGames.record()

SoloGames.record(mode: string, score: any, options?: { audience?: Audience }): { best: any; record: boolean }
Keeps score as the best in mode when it beats it, with options as tinylib.data.set takes them, and returns the best from before it and whether this is a record.

SoloGames.week()

SoloGames.week(day?: string): { day: string; result: unknown; today: boolean }[]
Returns the seven days ending day, each with its date, its result, and whether it's that day.

TODAY

const TODAY = 'today'
The mode of today's puzzle.

followFriendsToday()

followFriendsToday<T = any>(key: string, compare: (a: T, b: T) => number, options: { mine?: T; valid?: (value: unknown) => boolean } | undefined, fn: (friends: FriendsToday<T>) => void): () => void
Follows friends' results for the day key names, best first by compare, with options as leaderboard takes them. Calls fn now and each time the connection changes, and returns a function that stops it.

FriendsToday

interface FriendsToday<T = any>
What followFriendsToday calls its function with.

Properties

rowsStanding<T>[]
loadingboolean
onlineboolean
guestboolean
errorTinylibError | null

friendsToday()

friendsToday<T = any>(key: string, options: FriendsTodayOptions<T>): Placed
Makes a list of the results under key, such as games.results.key(day) or best/classic, the player's own among them and marked, best first by compare, filled in when online. A friend's name opens Block and Report.

The list is a section of className, with lis that have .you for the player's own, and .place, .name, .result, and .tools inside them, a p.note, and a button.send with .loud or .quiet. Your page styles them.

FriendsTodayOptions

interface FriendsTodayOptions<T = any>
What friendsToday takes.

Properties

classNamestringoptional
titlestringoptional
The list's heading.

Default: 'Friends today'

alonestringoptional
What the list says when no friend has played.
mineToptional
The user's value, as leaderboard takes it.

Methods

compare(a: T, b: T) => number
As leaderboard takes: negative when a is better than b.
show(result: T) => Node | string
Turns one result into text or an element.
valid(value: unknown) => booleanoptional
Returns false for a friend's value to leave out, as leaderboard takes it.

reminderSwitch()

reminderSwitch(options: { title: string; text?: string; className?: string; address?: string }): Placed
Makes "Remind me each day" with a time, shown once the current reminder is read, and hidden offline.

Parameters

options{ title: string; text?: string; className?: string; address?: string }
options.titlestring
The reminder's title.
options.textstringoptional
The reminder's text.
options.classNamestringoptional
The switch's class.
options.addressstringoptional
The address the reminder opens. Defaults to 'today'.

Placed

interface Placed
An element a kit made, and a function that stops it.

Properties

nodeHTMLElement

Methods

stop() => void

Clocks

stopwatch()

stopwatch(onTick?: (seconds: number) => void, every?: number): Stopwatch
Returns a stopwatch that counts seconds from start until pause. It doesn't count while the page is hidden, so time the user spends in another app is left out.
JavaScript
const watch = stopwatch((seconds) => { timer.textContent = clock(seconds) }) watch.start() // Once the puzzle is solved: watch.pause() hard.save({ seconds: Math.round(watch.seconds) }, day)

Parameters

onTick(seconds: number) => voidoptional
Called with the seconds counted, every every milliseconds while the stopwatch counts.
everynumberoptional
Milliseconds between calls to onTick. Defaults to 1000.

Stopwatch

interface Stopwatch
A stopwatch, from stopwatch.

Properties

secondsnumber
The seconds counted, with their fraction. Set it to carry on from a time saved earlier.
runningbooleanread-only
True after start and before pause, except while the page is hidden.

Methods

start()
Counts on from seconds.
pause()
Stops counting and keeps seconds.

watch.start()

watch.start(): void
Counts on from seconds.

watch.pause()

watch.pause(): void
Stops counting and keeps seconds.

countdown()

countdown(fn: (left: number) => void): () => void
Calls fn now and every second with the milliseconds left until midnight. When the date changes, it calls fn with 0 and stops. Returns a function that stops the calls.
JavaScript
const stopCountdown = countdown((left) => { if (left) nextPuzzle.textContent = `New puzzle in ${clock(Math.ceil(left / 1000))}` else showToday() })

untilTomorrow()

untilTomorrow(now?: Date): number
Returns the milliseconds from now until the next midnight in the device's time zone, when today changes.
JavaScript
const hours = Math.floor(untilTomorrow() / 3_600_000)

Parameters

nowDateoptional
Defaults to now on Tinylib's clock.

clock()

clock(seconds: number): string
Formats seconds as 'M:SS', or 'H:MM:SS' from an hour on. Fractions of a second are dropped.
JavaScript
clock(83) // '1:23' clock(3725) // '1:02:05'

Daily reminder

followDailyReminder()

followDailyReminder( name: string, options: Omit<ReminderOptions, 'at' | 'repeat'>, fn: (reminder: DailyReminder) => void, ): { set(time: string | null): Promise<void>; stop(): void }
Keeps a toggle on your page in step with the daily reminder name. Calls fn with the reminder now, and again after each set, when the device goes online or offline, and when the user changes your app's switches in Tinylib.

set(time) makes the reminder go off every day at time, 'HH:MM' on a 24-hour clock, or cancels it when time is null. If that fails, set rejects with the error and calls fn with the reminder unchanged, so your toggle flips back. stop stops the calls.

Needs "reminders" in uses.

JavaScript
const reminder = followDailyReminder('sudoku', { title: 'Sudoku', text: "Today's puzzle is ready." }, ({ available, time, muted }) => { reminderRow.hidden = !available reminderToggle.checked = time !== null reminderTime.value = time ?? '09:00' remindersOff.hidden = !muted }) reminderToggle.onchange = async () => { try { await reminder.set(reminderToggle.checked ? reminderTime.value : null) } catch (error) { toast(error.message) } }

Parameters

namestring
The reminder's name, as tinylib.reminders.set takes it.
optionsOmit<ReminderOptions, 'at' | 'repeat'>
What the reminder says and the address it opens, as tinylib.reminders.set takes them.
fn(reminder: DailyReminder) => void
Called with the reminder, as DailyReminder.

DailyReminder

interface DailyReminder
The user's daily reminder, from followDailyReminder.

Properties

availableboolean
False until the reminder has been read, and again while the device is offline or when reading fails. Hide your toggle while it's false.
timestring | null
The time it goes off each day, as 'HH:MM', or null when it's off.
mutedboolean
True when it's on but the user has turned off your app's Reminders switch in Tinylib, so it doesn't go off.

Turn games, in rules

defineGame()

defineGame<S extends GameState = GameState>(spec: GameSpec<S>): Kind<S, any, GameView>
Compiles a game to a kind, which a rules file exports like any other. The rules say what each player may do, and that one list gates the move and tells the page what's legal. Who sits where, whose turn it is, the clock, a player who leaves or goes quiet, and the end of the game are the kit's. Wins count as wins, which goes in rulesKeys in tinylib.json.

In a move, ctx.random() is the only randomness and ctx.now the only clock.

JavaScript
export const game = defineGame({ seats: { min: 2, max: 2 }, turn: { limit: null }, prepare(state) { state.table.board = Array(9).fill(null) }, moves: { mark: { options: ({ table }) => table.board.flatMap((cell, at) => (cell === null ? [at] : [])), run: ({ table }, seat, at) => void (table.board[at] = seat.color), }, }, over(state, { by }) { return wonBy(state.table.board, by?.color) ? { winners: [by.id] } : null }, })

GameSpec

interface GameSpec<S extends GameState = GameState>
A game, as defineGame takes it.

Properties

addressstringoptional
The kind's address, which notifications and links open.

Default: 'game/:id'

idleMsnumberoptional
How long a room lasts with nobody in it. Defaults to 30 days.
migrateKind<S>['migrate']optional
presenceKind<S>['presence']optional
guestsbooleanoptional
seats{ min: number; max: number; wait?: boolean }
The table starts when max is reached, or when the host starts it from min. wait: true leaves a full table for the host to start.
turnGameTurn<S> | false
The game's turns, or false for a game with none.
show{ table?(state: S): object; seat?(seat: GameSeat, state: S): object }optional
What everyone sees besides table, such as how many cards are in a hidden hand. Runs once the game has started, and may also shape what table shows.
movesRecord<string, Move<S>>
What the players can do, by name.

Methods

prepare(state, options, ctx)optional
Runs when the room is made, for what the page draws while waiting: settings, an empty board.
setup(state, ctx)optional
Deals when the game starts, and may return what happened, for said and to tell the first player.
first(state, ctx)optional
Returns the seat index to start at, or 0 if you leave it out.
left(state, seat, ctx)optional
Tidies up after a player who left or ran out of time, such as cards back in the deck.
over(state, turn)optional
Returns { winners, why } when the game is over, or null.
score(state, outcome)optional
Returns what to add to any outcome, however the game ended, such as scores.
text(outcome, id, name)optional
Words the result for id, which is null for nobody in particular, or returns undefined for the default.

GameSpec.prepare()

GameSpec.prepare?(state: S, options: any, ctx: Context): void
Runs when the room is made, for what the page draws while waiting: settings, an empty board.

GameSpec.setup()

GameSpec.setup?(state: S, ctx: Context): string | void
Deals when the game starts, and may return what happened, for said and to tell the first player.

GameSpec.first()

GameSpec.first?(state: S, ctx: Context): number
Returns the seat index to start at, or 0 if you leave it out.

GameSpec.left()

GameSpec.left?(state: S, seat: GameSeat, ctx: Context): void
Tidies up after a player who left or ran out of time, such as cards back in the deck. Setting state.said here replaces the kit's "left the game".

GameSpec.over()

GameSpec.over?(state: S, turn: { canMove: boolean; by: GameSeat | null }): ({ winners: string[]; why?: string | null } & Record<string, unknown>) | null | undefined
Returns { winners, why } when the game is over, or null. by is the seat that just moved, or null, and canMove says whether the player whose turn it is has a move. Anything else returned stays on the outcome.

GameSpec.score()

GameSpec.score?(state: S, outcome: Outcome): object | undefined
Returns what to add to any outcome, however the game ended, such as scores.

GameSpec.text()

GameSpec.text?(outcome: Outcome, id: string | null, name: (id: string) => string): string | undefined
Words the result for id, which is null for nobody in particular, or returns undefined for the default. name names a player.

GameTurn

interface GameTurn<S extends GameState = GameState>
A game's turns.

Properties

limitnumber | null | ((state: S) => number | null)
The milliseconds a turn may take, null for no clock, or a function of the state for a limit the room chose.
timeoutstring[]optional
The moves tried in order when the time runs out, each with options, and the first option is played. With none, the turn passes.
strikesnumber | nulloptional
How many times in a row a player can run out of time before they're out. Defaults to 3, and to 1 for a game with no timeout; null is never.
warnnumber | nulloptional
How many milliseconds before the limit to remind the player.

Methods

passed(state)optional
Runs whenever the turn moves on, for what a turn keeps count of.

GameTurn.passed()

GameTurn.passed?(state: S): void
Runs whenever the turn moves on, for what a turn keeps count of.

Move

interface Move<S extends GameState = GameState>
One thing a player can do. Give it options or check.

Properties

who'turn' | 'any' | 'host'optional
Who may make it. Defaults to 'turn' in a game with turns, and to 'any' in one without.
when'playing' | 'waiting'optional
When it can be made.

Default: 'playing'

keepsbooleanoptional
Leaves the turn, the clock, moved, and said alone, for a turn shared as it's built, and doesn't ask over.
refusestringoptional
The sentence for data that isn't on the list.

Methods

options(state, seat)optional
Returns the list of what the player may send.
check(state, seat, data)optional
Refuses what the player sent by returning a sentence, for a move too open to list.
run(state, seat, data, ctx)
Makes the move, and may return what happened, as words after the player's name.

Move.options()

Move.options?(state: S, seat: GameSeat): unknown[]
Returns the list of what the player may send. A move that takes nothing lists [null].

Move.check()

Move.check?(state: S, seat: GameSeat, data: any): string | null | undefined
Refuses what the player sent by returning a sentence, for a move too open to list.

Move.run()

Move.run(state: S, seat: GameSeat, data: any, ctx: MoveContext): string | void
Makes the move, and may return what happened, as words after the player's name. A move ends the turn unless it calls ctx.stay().

MoveContext

interface MoveContext extends Omit<Context, 'user' | 'refuse'>
The ctx of a move: a room's ctx with no user or refuse, since a timeout plays moves too.
Extends Omit.

Properties

timedOutboolean
Whether a timeout played this move.

Methods

stay()
Keeps the turn with this player.
turnTo(seat)
Says who goes next, or the next still in after them.

MoveContext.stay()

MoveContext.stay(): void
Keeps the turn with this player.

MoveContext.turnTo()

MoveContext.turnTo(seat: GameSeat): void
Says who goes next, or the next still in after them.

GameState

interface GameState<Table = any, Hidden = any, Private = any>
The state of a room defineGame made. Keep a game's own state in table, hidden, and each seat.private.

Properties

phase'waiting' | 'playing' | 'over'
seatsGameSeat<Private>[]
turnnumber
The index in seats of the player whose turn it is.
clocknumber
Counts the turns.
deadlinenumber | null
saidSaid | null
tableTable
What everyone sees.
hiddenHidden
What nobody sees.
outcomeOutcome | null

GameSeat

interface GameSeat<Private = any> extends Seat
A player at the table, as the rules see them.
Extends Seat, so it also has id, color, here, and out.

Properties

namestring
guestboolean
movedboolean
Whether they've made a move of their own.
outboolean
Whether they're out of the game. A move that knocks a player out sets it.
latenumber
How many times in a row their time ran out.
privatePrivate
What only this player sees.

Said

interface Said
The last thing that happened: by and name say who did it, when someone did.

Properties

bystring | null
namestring | null
textstring

Outcome

interface Outcome
How a game ended, with whatever over and score added.

Properties

winnersstring[]
whystring | null
over's why, or the kit's own: 'left', 'time', 'unplayed', or 'stuck'.
leaverstring | null
The id of the player who left or ran out of time.
timedOutbooleanoptional
With 'unplayed', whether the player ran out of time rather than left.

GameView

interface GameView
What a member sees of a game.

Properties

phaseGameState['phase']
youstring | null
hoststring | null
turnstring | null
deadlinenumber | null
When this turn runs out, or null.
clocknumber
Counts the turns.
saidSaid | null
linestring | null
said as a sentence for this member: "You drew a card."
playersSeatView[]
Who's still in.
seatsSeatView[]
Everyone who sat down.
limits{ min: number; max: number }
The seats the table holds.
tableany
state.table plus show.table. Before the game starts, only what prepare set.
privateany
The member's own seat.private, or null.
legalRecord<string, unknown[] | true>
What the member may do now, by move: the list of what they may send, or true for a move with check. The host's start is there once the table can start.
outcomeOutcome | null
statusstring
The member's own sentence.

SeatView

interface SeatView
A seat as a member's view shows it, plus show.seat.

Properties

idstring
atnumber
The seat's index.
namestring
guestboolean
colornumber
movedboolean
outboolean

FinalView

interface FinalView extends GameView
What a game keeps when it ends: the view of nobody in particular, under final in the room's data, with each player's own sentence.
Extends GameView, so it also has phase, you, host, turn, deadline, clock, said, line, players, seats, limits, table, private, legal, outcome, and status.

Properties

resultsRecord<string, string>

current()

current(state: GameState): GameSeat
Returns the seat whose turn it is. In over and left, the turn may already have moved on: use by to see who moved.

ifGone()

ifGone(seats: Pick<GameSeat, 'id' | 'moved' | 'out'>[], seat: Pick<GameSeat, 'id' | 'moved'>, min: number): 'on' | 'rest' | 'void'
Returns what seat going would do to a game in play, from seats of a state or a view and the table's min: 'on' when the rest play on, 'rest' when the game goes to the rest, and 'void' when it ends with no winner.

Seats, in rules

seats()

seats(ctx: Context, more?: Record<string, unknown>): Seats
Returns the seats of a room just made, with its maker in the first, in color 0, for a kind's create to spread into its state. more goes on their seat.

Seats

interface Seats<S extends Seat = Seat>
What a room's state holds of its seats.

Properties

seatsS[]

Seat

interface Seat
A player's seat. more of seats and sit goes on it too.

Properties

idstring
colornumber
Their color: the lowest number free when they sat down.
herebooleanoptional
Whether they have the room open, once present keeps it.
outbooleanoptional
Whether they're out of the game but keep the seat.

seatOf()

seatOf<S extends Seat>(state: Seats<S>, id: string | null): S | undefined
Returns the seat of id, or undefined.

playing()

playing<S extends Seat>(state: Seats<S>): S[]
Returns the seats still in the game.

hostOf()

hostOf<S extends Seat>(state: Seats<S>): S | undefined
Returns the host's seat: the first still in, which is the room's maker until they go.

full()

full(state: Seats<any>, most: number): boolean
Returns whether every seat of most is taken.

sit()

sit(state: Seats<any>, ctx: Context, most: number, more?: Record<string, unknown>): Seat
Seats the member who joined in the first color free of most, with more on their seat, and returns the seat.

stand()

stand(state: Seats<any>, id: string): void
Frees the seat of id.

present()

present(state: Seats, ctx: Context, open: boolean): void
Keeps whether the member has the room open, for a kind's presence.

anyoneHere()

anyoneHere(state: Seats): boolean
Returns whether any player has the room open.

allHere()

allHere(state: Seats): boolean
Returns whether every player has the room open.

remove()

remove(state: Seats<any>, id: string, ctx: Context): void
Removes id from the room, when the member acting is the host and id is another player. An action can be this.

seatsIn()

seatsIn(state: Seats, members: Pick<RoomMember, 'id' | 'name' | 'guest'>[]): (Seat & { name: string; guest: boolean; here: boolean })[]
Returns the seats as a view shows them: each with its name, whether they're a guest, and whether they're here.

Games with friends

followGames()

followGames(kind: RoomKind | null, fn: (games: Games) => void): () => void
Calls fn with the user's games and game invitations now, and again whenever they change or the device goes online or offline. Returns a function that stops the calls.

While it or followGame runs, Tinylib doesn't show those invitations on its own sheet, so draw invitations yourself, as in Show invitations.

JavaScript
const stop = followGames('fourInARow', ({ current, past, invitations, loading, online, error }) => { renderGames(current, past) renderInvitations(invitations) noGames.hidden = loading || !online || current.length > 0 gamesMessage.textContent = error?.message ?? (online ? '' : "Your games come back when you're online.") })

Parameters

kindRoomKind | null
Only rooms and invitations of this kind, or null for every kind.
fn(games: Games) => void
Called with the games, as Games.

Games

interface Games
The user's games, from followGames.

Properties

currentRoomListItem[]
Games still going, the ones waiting on the user first.
pastRoomListItem[]
Games that ended, most recently ended first, then games the user left, most recently left first.
invitationsInvitation[]
The user's invitations to games of this kind, newest first.
loadingboolean
True until the games have been read, and again after the device goes online or offline until they're read again. While it's true, empty lists don't mean the user has no games.
onlineboolean
Whether the device is online. Offline, current is empty.
errorTinylibError | null
Why the list couldn't be read, or null.

followGame()

followGame<K extends RoomKind = RoomKind>(id: string, fn: (game: Game<K>) => void): () => void
Opens the room id and calls fn with the game on each new view, each change to its invitations, and each time the device goes online or offline. Until the game has been read, view, room, error, and past are null. Returns a function that closes the room and stops the calls.

While it runs, Tinylib doesn't show the invitations in invitations, rematch, and duplicate on its own sheet. Draw them yourself without covering the game, with a way to block and report the sender.

JavaScript
const stop = followGame(id, (game) => { renderInvitations(game.invitations) if (game.past?.left) renderLeft(game.past) else if (game.past && !game.view) renderFinished(id, game.past) else if (game.error) showMessage(game.error.message) else if (game.view) renderBoard(game.view, game.room) else showSpinner() })

Parameters

idstring
The room's id.
fn(game: Game<K>) => void
Called with the game, as Game.

Game

interface Game<K extends string = RoomKind>
One game, from followGame.

Properties

viewRoomView<K> | null
The user's latest view, or null before the first.
roomOpenRoom<K> | null
The open room, or null when it couldn't be opened. Its ended says whether the room has ended, and left whether the user left it.
errorTinylibError | null
Why the game couldn't be opened, or null.
pastRoomListItem | null

The game's entry in tinylib.rooms.list() once it has ended or the user has left it, or null. A game that's over can't be opened again, so when followGame starts on one, view stays null and error has the code ended, or not_member if the user left.

  • past.left is true when the user left or was removed. Its status and members are as they were then.
  • Otherwise the game ended, and its members leave out anyone who left before then.
  • past.closed is true when the game ended without the rules calling ctx.end: nobody played it for the kind's idleMs, or its last player left. No handler ran then, so the rules saved nothing at the end, and its status is the last one they gave.
rematchInvitation | null
Another member's invitation to a rematch of this game, or null. When several members asked for one, everyone gets the same one.
duplicateInvitation | null
The invitation to the rematch everyone keeps, or null. It's set when the user started this game with playAgain at the same moment another member started a rematch of the same game. playAgain(game) then joins the kept game and leaves this one.
invitationsInvitation[]
The user's other invitations to games of this game's kind, or of a kind followGames has listed, newest first.
onlineboolean
Whether the device is online.

playAgain()

playAgain(game: Pick<Game, 'room' | 'past' | 'rematch'> & Partial<Pick<Game, 'duplicate'>>, options?: unknown): Promise<{ id: string }>
Starts a rematch of a game, open or ended, or one the user left, and resolves with { id }, the room to open.
  • With game.duplicate, it joins that member's room and leaves this one.
  • With game.rematch, it accepts it.
  • For a game the user left, or one that ended more than a day ago, it creates a room of the same kind and invites the members game.past lists, except guests.
  • Otherwise it accepts another member's invitation to a rematch of this game, if there is one. If not, it creates a room of the same kind with tinylib.rooms.create and rematchOf, which invites the game's other members who have an account. To play again with guests, don't end the room: reset it with an action.
JavaScript
again.onclick = async () => { try { const { id } = await playAgain(game) tinylib.places.go(`game/${id}`) } catch (error) { toast(error.message) } }

Parameters

gamePick<Game, 'room' | 'past' | 'rematch'> & Partial<Pick<Game, 'duplicate'>>
The game, from followGame.
optionsunknownoptional
Passed to the kind's create when this makes the room.

Rejects with

TypeError
When game has no room, past, rematch, or duplicate, or options can't be saved as JSON.
not_found
When the invitation it accepts no longer lets the user in.
refused
When the user is a guest, or the kind's create refuses. A rematch with rematchOf is also refused when the user has blocked a member or been blocked by one.
limit
When the user is already in 50 rooms of your app, has created rooms faster than 60 per hour in your app, or the room it joins already has 64 members.
failed
When the kind's create throws.
offline
When the device is offline.

Game pages

gamePages()

gamePages(address: string, options?: GamePagesOptions): GamePages
Returns the pages of a game at address, as its kind says it.
JavaScript
const pages = gamePages('game/:id', { noun: 'table' }) const stop = pages.follow(id, ({ view, gone, message, again }) => draw(view ?? gone ?? message, again))

GamePagesOptions

interface GamePagesOptions
How gamePages words a game.

Properties

invitationstringoptional
What an invitation says after the sender's name, unless it's for a rematch.

Default: 'wants to play'

nounstringoptional
What the game calls a game, such as "table", for its buttons.

Default: 'game'

loadingstringoptional
What shows until the game is read.

Default: 'Loading…'

Methods

rematch(name: string) => stringoptional
Words the button for a rematch someone sent.
settings(view: GameView) => unknownoptional
A new game's settings, from a view of this one.

GamePages

interface GamePages
The pages of a game, from gamePages.

Methods

open(id, replace?)
Goes to the game id, replacing this address with replace.
invited(invitation)
Returns what invitation says, and its two answers, which open the game or dismiss it.
follow(id, fn)
Follows the game id as followGame does, and calls fn with what to draw of it.

GamePages.open()

GamePages.open(id: string, replace?: boolean): void
Goes to the game id, replacing this address with replace.

GamePages.invited()

GamePages.invited(invitation: Invitation): { text: string; accept: () => Promise<void>; dismiss: () => Promise<void> }
Returns what invitation says, and its two answers, which open the game or dismiss it.

GamePages.follow()

GamePages.follow(id: string, fn: (game: GameScreen) => void): () => void
Follows the game id as followGame does, and calls fn with what to draw of it. Returns a function that stops it.

GameScreen

interface GameScreen extends Omit<Game, 'view'>
What to draw of a game, from GamePages's follow. Exactly one of view, gone, and message says what the screen is.
Extends Omit.

Properties

viewGameView | null
The board: the live view, or the one the rules kept once the game is over.
endedboolean
Whether the game is over, or has no open room to play in, as while it loads.
peoplePerson[]
Everyone else seen in this game, kept after they leave so Block and Report stay.
gone{ text: string; people: Person[] } | null
For a game with no board: what to say, and who else played.
messagestring | null
A line to show alone: the game loading, its error, or no connection.
againButtonProps
The rematch button, hidden until the game ends. It invites whoever has an account, or starts a new game with nobody to invite.
twin{ text: string; join: ButtonProps }
For a room waiting while someone else's rematch of the same game waits too: what to say, or nothing, and the button that joins theirs.

ButtonProps

interface ButtonProps
A button's properties.

Properties

hiddenboolean
textContentstring

Methods

onclick() => Promise<void>

Person

interface Person
Someone seen in a game.

Properties

idstring
namestringoptional
guestbooleanoptional

drawStrip()

drawStrip(strip: HTMLElement, invitations: Invitation[], row: (invitation: Invitation) => Node): void
Fills strip with row of each invitation, only when the invitations change.

followWins()

followWins(fn: (wins: number) => void): () => void
Calls fn with how many games the user has won, now and whenever it changes. Returns a function that stops it.

leaveWarning()

leaveWarning(view: GameView, options?: { playsOn?: string }): string | null
Returns what to ask before the user leaves the game in view, or null when leaving costs nothing. playsOn is the question for a table that carries on without them.

leaveGame()

leaveGame(id: string): Promise<void>
Leaves the game id and goes back to the first page.

roomRow()

roomRow(room: RoomListItem, options: { title?: (names: string) => string; empty: string }): { people: Person[]; title: string; status: string }
Returns for a row in the games list who else is in room, its title with their names or else empty, and its status.

Playing with no signal

localRoom()

localRoom(kind: Kind, config: { players: { id: string; name: string }[]; options?: unknown; now?: () => number; random?: () => number }): LocalRoom
Runs a room of kind on this device, for a game against bots with no signal. It's your rules, unchanged, with what Tinylib does around them done here. Nothing it keeps is saved, notifications go nowhere, and everyone in it has it open.
JavaScript
const clock = pausingClock() const room = localRoom(game, { players: [{ id: 'you', name: 'You' }, { id: 'bot', name: 'Bot' }], now: clock.now }) const port = room.port('you') port.onView(draw) setInterval(room.tick, 100)

Parameters

kindKind
The kind your rules export.
config{ players: { id: string; name: string }[]; options?: unknown; now?: () => number; random?: () => number }
config.players{ id: string; name: string }[]
Who's in it. The first makes it.
config.optionsunknownoptional
What create gets.
config.now() => numberoptional
The clock. Defaults to Tinylib's.
config.random() => numberoptional
The random numbers ctx.random gives.

LocalRoom

interface LocalRoom
A room on this device, from localRoom.

Properties

endedbooleanread-only
namestring | nullread-only
The name the rules gave the room.
roomDataValuesread-only
What the rules wrote to the room's data.

Methods

port(id)
Returns the member id's way into the room.
join(player)
leave(id)
tick()
Runs what's due.
values(id)
What the rules wrote for the member id.

LocalRoom.port()

LocalRoom.port(id: string): LocalPort
Returns the member id's way into the room.

LocalRoom.join()

LocalRoom.join(player: { id: string; name: string }): void

LocalRoom.leave()

LocalRoom.leave(id: string): void

LocalRoom.tick()

LocalRoom.tick(): void
Runs what's due. Nothing moves until it's called, so call it often.

LocalRoom.values()

LocalRoom.values(id: string): Values
What the rules wrote for the member id.

LocalPort

interface LocalPort
A member's way into a LocalRoom: what a page gets from tinylib.rooms, plus view and onView.

Properties

idstring
endedbooleanread-only
leftbooleanread-only

Methods

view()
Returns what the rules' view gives this member.
flush()
Tells onView's functions, if the view changed, and then onEvent's.
onView(fn)
Calls fn with the view whenever it changes.
onEvent(fn)
onRelay(fn)
relay(data)
act(action, data?)

LocalPort.view()

LocalPort.view(): any
Returns what the rules' view gives this member.

LocalPort.flush()

LocalPort.flush(): void
Tells onView's functions, if the view changed, and then onEvent's.

LocalPort.onView()

LocalPort.onView(fn: (view: any) => void): () => void
Calls fn with the view whenever it changes. Returns a function that stops it.

LocalPort.onEvent()

LocalPort.onEvent(fn: (event: unknown) => void): () => void

LocalPort.onRelay()

LocalPort.onRelay(fn: (from: string, data: unknown, at: number) => void): () => void

LocalPort.relay()

LocalPort.relay(data: unknown): void

LocalPort.act()

LocalPort.act(action: string, data?: unknown): Promise<void>

pausingClock()

pausingClock(): { now: () => number; stop: () => void }
Returns a clock for localRoom that stops while the page is hidden, so a game waits for the user. stop ends the watching.

Connection

needsConnection()

needsConnection(elements: { disabled: boolean }[]): () => void
Disables each of elements without a connection and enables it when one returns. Returns a function that stops it.
JavaScript
const stop = needsConnection([hostButton, joinButton])

NO_CONNECTION

const NO_CONNECTION = 'No connection'
What an app says without a connection.

connectionLine()

connectionLine(list: { online: boolean; error: (Error & { code?: string }) | null }): string
Returns the line that goes with a list that follows the connection, from the { online, error } it was called with: NO_CONNECTION without one, its error, or nothing.

People

personRows()

personRows(person: { id: string; guest?: boolean }, options?: { after?: (done: boolean) => void }): PersonRow[]
Returns person's rows: Block, and Report between accounts, not by a guest and not of one. after gets what Tinylib's sheet answered, and an error goes to the toast.

PersonRow

interface PersonRow
A row of what can be done about a person: its label, and run to do it.

Properties

labelstring

Methods

run() => Promise<void>

personButtons()

personButtons(person: { id: string; guest?: boolean }, options?: { className?: string; after?: (done: boolean) => void }): HTMLButtonElement[]
Returns person's rows as buttons with className.

moreButton()

moreButton(panel: HTMLElement, { key, ...props }?: { key?: string } & Partial<HTMLButtonElement>): HTMLButtonElement
Makes a "…" button that opens and closes panel, with the rest of its properties in props. With key, a room's or a person's id, a panel drawn again comes back as it was left.

Screens

route()

route(screens: Record<string, Screen>, options?: { keep?: boolean }): () => void
Shows one screen at a time, chosen by the part of tinylib.places.address before the first /. When the address changes, it stops the current screen and shows the next. Screens get the address without its query; read the query from tinylib.places.address.

Returns a function that stops the current screen and stops switching.

JavaScript
route({ '': showGames, // the start address game: showGame, // 'game/abc' calls showGame('abc') }, { keep: true })

Parameters

screensRecord<string, Screen>
Screens keyed by the part of the address before the first /. Each is called with the rest of the address. The screen under '' shows the start address and any address no other key matches, and is called with the whole address. When a screen throws, nothing shows until the address changes again.
options{ keep?: boolean }optional
options.keepbooleanoptional
When true, going to the address already showing leaves its screen as it is instead of starting it again.

Screen

type Screen = (rest: string) => (() => void) | void
A screen route shows. It draws itself and can return a function that stops it.

$()

$<T extends HTMLElement = HTMLElement>(id: string): T
Returns the element with the id id.

element()

element<K extends keyof HTMLElementTagNameMap>(tag: K, props?: Partial<HTMLElementTagNameMap[K]>, children?: (Node | string)[]): HTMLElementTagNameMap[K]
Makes an element with props set on it and children inside it.
JavaScript
const row = element('li', { className: 'row' }, [element('span', { textContent: name }), ' played'])

toast()

toast(text: string): void
Shows text for a moment in the element with the id toast, which it adds if the page has none.

show()

show(error: Error): void
Toasts an error's message, for a catch.

Words

listNames()

listNames(names: string[]): string
Writes names in a sentence.
JavaScript
listNames(['Ben', 'Cy', 'Di']) // 'Ben, Cy, and Di'

initial()

initial(name: string): string
Returns a name's first letter for an avatar, or '?' for a blank name.

plural()

plural(count: number, word: string): string
Writes count of word, with an s unless there's one.
JavaScript
plural(3, 'game') // '3 games'
PreviousTypesNextKinds

On this page

  • Dates and randomness
  • today()
  • addDays()
  • seeded()
  • shuffled()
  • Daily results
  • daily()
  • Daily
  • leaderboard()
  • Standing
  • friendsWith()
  • FriendResult
  • Solo games
  • solo()
  • SoloGames
  • TODAY
  • followFriendsToday()
  • FriendsToday
  • friendsToday()
  • FriendsTodayOptions
  • reminderSwitch()
  • Placed
  • Clocks
  • stopwatch()
  • Stopwatch
  • countdown()
  • untilTomorrow()
  • clock()
  • Daily reminder
  • followDailyReminder()
  • DailyReminder
  • Turn games, in rules
  • defineGame()
  • GameSpec
  • GameTurn
  • Move
  • MoveContext
  • GameState
  • GameSeat
  • Said
  • Outcome
  • GameView
  • SeatView
  • FinalView
  • current()
  • ifGone()
  • Seats, in rules
  • seats()
  • Seats
  • Seat
  • seatOf()
  • playing()
  • hostOf()
  • full()
  • sit()
  • stand()
  • present()
  • anyoneHere()
  • allHere()
  • remove()
  • seatsIn()
  • Games with friends
  • followGames()
  • Games
  • followGame()
  • Game
  • playAgain()
  • Game pages
  • gamePages()
  • GamePagesOptions
  • GamePages
  • GameScreen
  • ButtonProps
  • Person
  • drawStrip()
  • followWins()
  • leaveWarning()
  • leaveGame()
  • roomRow()
  • Playing with no signal
  • localRoom()
  • LocalRoom
  • LocalPort
  • pausingClock()
  • Connection
  • needsConnection()
  • NO_CONNECTION
  • connectionLine()
  • People
  • personRows()
  • PersonRow
  • personButtons()
  • moreButton()
  • Screens
  • route()
  • Screen
  • $()
  • element()
  • toast()
  • show()
  • Words
  • listNames()
  • initial()
  • plural()