Examples
Every script here either ships with the app or is built from parts that do. Copy one, paste it into the field named above it, and adjust.
Timer screen text
Section titled “Timer screen text”These go in a content item’s Value field.
The countdown
Section titled “The countdown”{{ (secondsLeftInLevel * 1000).toCompactTimeDurationString }}secondsLeftInLevel is seconds, and the duration formatters read milliseconds, so it’s multiplied by 1000 first. toCompactTimeDurationString gives 20:00; toTimeDurationString would give 0:20:00.
Level number that ignores breaks
Section titled “Level number that ignores breaks”Level {{ blindLevelNumber }} of {{ blindLevelCount }}With a condition of isBlindsLevel, so the line disappears during breaks instead of freezing on the level that just ended.
Blinds, compactly
Section titled “Blinds, compactly”{{ (currentLevel.smallBlind).toCompactDecimalString + " / " + (currentLevel.bigBlind).toCompactDecimalString }}toCompactDecimalString turns 1500 into 1.5K, which keeps big-stack tournaments from overflowing the cell.
Average stack in big blinds
Section titled “Average stack in big blinds”{{ (averageChipsPerPlayer / activeBigBlind).round }} BBsGive it a condition of activeBigBlind > 0 so it can’t divide by zero before the blinds start.
Estimated finish time
Section titled “Estimated finish time”ends around {{ (now() + estimatedSecondsUntilEnd * 1000).round.toHMTimeString }}now() is milliseconds, so the estimate is converted to match, then formatted as a clock time. Condition it on isTimerRunning — the estimate is meaningless while paused.
Chip leaders
Section titled “Chip leaders”{{ chipLeaders.take(3).map(func(p) { p.shortName + " — " + p.currentChips.round.toCompactDecimalString }).join("\n") }}chipLeaders is the active players with a reported chip count, most chips first, so take(3) gives the top three; map turns each into a “name — stack” line and join stacks them. Condition: chipLeaders.isNotEmpty.
High hand
Section titled “High hand”{{ cards = highHand.pokerHandToCards; highHandPlayer.shortName + " — " + cards.pokerCardsToHandName + " " + cards.pokerCardsToText }}Two statements on one line, separated by ;, because a template has to fit inside one pair of braces. The first decodes the hand into cards; the second builds the line. Condition: hasHighHand.
Leaderboards
Section titled “Leaderboards”The trick to a leaderboard is where to filter, sort with a negated key to rank, take to cut it off, and join("\n") to stack the results into a column.
Top five knockouts
Section titled “Top five knockouts”{{eliminators = players.where(func(p) { p.totalEliminated > 0 }).sort(func(p) { -p.totalEliminated })eliminators.take(5).map(func(p) { p.shortName }).join("\n")+ if (eliminators.count > 5) { "\n…" } else { "" }}}The trailing if adds an ellipsis when there are more players than fit — and adds nothing when there aren’t, since if produces a value either way.
The totals column beside it is the same script with two changes — p.totalEliminated in place of p.shortName, and a bare "\n" in place of "\n…":
{{eliminators = players.where(func(p) { p.totalEliminated > 0 }).sort(func(p) { -p.totalEliminated })eliminators.take(5).map(func(p) { p.totalEliminated }).join("\n")+ if (eliminators.count > 5) { "\n" } else { "" }}}Only the names column gets the ellipsis; the totals column just adds the blank line, so the two columns stay the same height and their rows line up.
Final standings
Section titled “Final standings”The built-in Points widget, in two columns. playersEliminated is already ordered by place, and the winner is the one active player with a place, so they’re prepended once the tournament is over — take of a conditional count adds them then and nobody now:
{{scored = playersActive.take(if (isPostgame) { 1 } else { 0 }) + playersEliminatedscored.take(10).map(func(p) { p.shortName }).join("\n")+ if (scored.count > 10) { "\n…" } else { "" }}}The points column beside it swaps p.shortName for p.points, and the ellipsis for a bare "\n":
{{scored = playersActive.take(if (isPostgame) { 1 } else { 0 }) + playersEliminatedscored.take(10).map(func(p) { p.points }).join("\n")+ if (scored.count > 10) { "\n" } else { "" }}}p.points runs your own points formula, so there’s no scoring written out here to keep in step with it.
Top ten bounty winners
Section titled “Top ten bounty winners”{{winners = players.where(func(p) { p.bountyChipWinnings > 0 }).sort(func(p) { -p.bountyChipWinnings })winners.take(10).map(func(p) { p.shortName }).join("\n")+ if (winners.count > 10) { "\n…" } else { "" }}}Its winnings column pairs with it the same way — p.bountyChipWinnings.toCurrencyString for the value, and "\n" without the ellipsis.
A payout column
Section titled “A payout column”The simplest version of the payout list is three content items shown side by side. Places:
{{ i = 0; payouts.take(payoutsToShow).map(func(e) { i = i + 1; i.withOrdinalSuffix }).join("\n") }}Amounts:
{{ payouts.take(payoutsToShow).map(func(e) { e.toCurrencyString }).join("\n") }}And once players have finished, their names:
{{ playersWithPayouts.map(func(p) { p.shortName }).join("\n") }}The built-in Payouts widget uses longer versions of these three that merge the unclaimed places with the finished players and append the recent knockouts below a blank line. They’re worth opening once you’re comfortable — but these three are the pattern.
Conditions
Section titled “Conditions”Conditions go in a content item’s Condition field, or on a row or column in Layout & content.
totalCollected > 0currentLevel.bigBlind > 0 || currentLevel.ante > 0levelIndex + 1 < levels.lengthuseBountyChips && players.where(func(p) { p.bountyChipWinnings > 0 }).count > 0Those four are the real conditions on the built-in Payouts, Blinds and antes, Next level and break, and Bounties widgets.
Alerts
Section titled “Alerts”An alert’s condition is checked about once a second, and the alert fires every time it’s true. becameTrue(...), changed(...), and was(...) are what turn “true for ten minutes” into “true at one instant”.
Fire once, when something first becomes true
Section titled “Fire once, when something first becomes true”becameTrue(currentPlayerCount == 3)becameTrue(currentLevel.bigBlind >= 100)One minute left in a blind level
Section titled “One minute left in a blind level”becameTrue(secondsLeftInLevel <= 60) && currentLevel.isBlindLevelThis is the built-in near end of level alert. Without the becameTrue it would fire sixty times.
Somebody just busted
Section titled “Somebody just busted”currentPlayerCount < was(currentPlayerCount)was(x) gives you the value itself a second ago, for comparisons the other two don’t state directly — here, that the count went down rather than merely changed.
Blinds went up
Section titled “Blinds went up”(changed(levelIndex) || becameTrue(isInGame)) && currentLevel.isBlindLevel && currentLevel.bigBlind > 0This is the built-in blinds changed alert. The first half is the level changing; the second is play beginning, which is a blind change too — from nothing to the opening blinds. That half is what announces the blinds in a structure with no pregame level, where the timer starts on the first blind level and so never advances onto it.
With TTS text:
The small blind is {{ currentLevel.smallBlind }}, and the big blind is {{ currentLevel.bigBlind }}Someone got knocked out
Section titled “Someone got knocked out”if log.entries.isEmpty { false } else { e = log.entries.last e.type == "knockout" && e.status == "enabled" && changed(log.entries.last.dt)}The isEmpty guard comes first because .last on an empty log would fail. changed(...) on the entry’s dt is what stops it re-firing on the same knockout, and it comes last so the type check runs first. Note that it spells out log.entries.last.dt rather than reusing e: these functions re-read tournament values, and e is one you assigned yourself.
Its TTS text names everyone involved:
It’s a knock out! {{ e = log.entries.last; e.players.map(func(p) { p.toPlayer().name }).join(", ", ", and ") }}, knocked out by {{ e.byPlayers.map(func(p) { p.toPlayer().name }).join(", ", ", and ") }}!join(", ", ", and ") gives the last name its own separator, so three names read “Alice, Bob, and Carol” instead of “Alice, Bob, Carol”.
Payout structures
Section titled “Payout structures”These go in Payouts → Script. The result must be a list of amounts; its length sets how many places get paid.
More places as the field grows
Section titled “More places as the field grows”places = 1 + totalBuyIns / 5defaultPayoutPercentages(places).map(func(p) { p * netPrize })One paid place for every five buy-ins, split by the app’s own percentages.
if totalBuyIns < 10 { [100, 60, 40] }else if totalBuyIns < 17 { [150, 90, 60] }else { [180, 110, 70, 40] }Fixed amounts rather than percentages, with a fourth place appearing at seventeen buy-ins.
Hold back a fixed amount for third
Section titled “Hold back a fixed amount for third”[0.55 * (netPrize - 50), 0.45 * (netPrize - 50), 50]Third place always gets $50; the top two split what’s left 55/45.
Your league’s payout chart
Section titled “Your league’s payout chart”table = [ [90, 30], [110, 40], [130, 50], [150, 60], [160, 60, 20], [170, 70, 30]]n = totalBuyIns + totalRebuystable[min(max(n, 4), 9) - 4]One row per entry count, starting at four. The last line clamps the count into the range the table covers and picks the matching row. Rows can have different lengths, so the number of paid places changes as the chart progresses — extend it a row at a time.
League points
Section titled “League points”These go in Stats → Player points formula, and are evaluated once per player with playerId set to that player.
The built-in formula, with two alternatives left in as comments:
player = playerId.toPlayer()p = player.placee = player.totalEliminatedc = totalPlayerCountt = totalCollectedpoints = 0
// Linear// points = c - p + 1
// Square root// points = sqrt(c + 1) / p * 10
// Logarithmicpoints = ln(c + 1) / p * 10
points.roundSwap which line is commented to change the curve. Linear pays one point per player you outlasted; logarithmic — the default — rewards the top few places far more heavily.
To fold in knockouts, add them:
points = ln(c + 1) / p * 10 + e * 2points.roundThe same formula is what Club stats sums across a season, so changing it changes the season leaderboard too.