← poker-tutor
import Foundation
import PokerCore
import PokerCoach
import PokerSession
/// Every sentence the screens write for a person to read.
///
/// It is gathered here for one reason: until it was, no test could read a word of it. The plain
/// English rule was enforced across the coach, the leak catalogue, the assessment and the drills —
/// every layer with a testable string on it — and stopped dead at the screens, which is where most
/// of the writing actually lives. A commit whose whole purpose was removing one poker word left
/// that same word sitting in a `Text(...)` two files away, and nothing noticed, because nothing
/// could look. `PokerTests` walks `Copy.prose` now, so a poker word typed into a screen fails the
/// suite in the same breath as one typed into a leak write-up.
///
/// **What is in here and what is not.** The prose is: sentences, paragraphs, the subtitle under a
/// panel heading, the note that explains why a number is missing. The things left where they were
/// are buttons ("Next hand", "Start again"), column headings ("SEAT", "HANDS"), and anything that
/// is really a number with a word beside it ("\(n) hands"). Those are labels rather than writing —
/// there is no sentence in them to get wrong, and moving them here would bury the writing in a list
/// of nouns. The limit is worth knowing: a poker word used as a *button label* is not covered by
/// the suite, and would have to be caught by reading.
///
/// Sentences that need a number in them are functions. Each one appears in `prose` filled in with a
/// plausible value, because the test reads the finished sentence rather than the format string.
public enum Copy {
// MARK: - Home
public static let appTagline = "Play, and find out what your game is costing you."
/// Both of these name the hands behind the number, because the ladder prints a rating of its
/// own — over the hands at the table you are on now — and the two are visible at once.
public static let ratingSettling =
"Every hand you have ever played, and still settling — play more to firm it up."
public static let ratingSettled =
"Every hand you have ever played, rated by what your mistakes cost."
/// Before there is a single hand to rate. The app has never seen this person play, so it says
/// so rather than drawing a 50 in inch-high type beside "Solid amateur" — which is the same
/// overclaim the statistics panel three inches away already refuses to make, where a nought
/// with no hands behind it is printed as a dash on the grounds that nought reads as a result
/// somebody earned. Being called a solid amateur off nothing is not a result either.
public static let ratingUnknown =
"Nothing to rate yet. Play a hand and this fills in — a rating off no hands would be a "
+ "number about nobody."
/// Which of the three the rating card and the sidebar are entitled to say. Written once because
/// both of them show the same number and used to disagree about when it means anything.
public static func ratingSubtitle(handsBehindIt: Int, settled: Bool) -> String {
guard handsBehindIt > 0 else { return ratingUnknown }
return settled ? ratingSettled : ratingSettling
}
public static let tournamentOffer = "blinds climb, nine players, three paid"
// MARK: The cash app
public static let cashSitDownTitle = "Sit down"
public static let cashSitDownButton = "Sit down"
public static let cashStakesLabel = "Blinds"
public static let cashStructureLabel = "How much you can bet"
/// The one-line offer under the button: "25/50, no-limit, 100 big blinds to sit down with."
public static func cashOfferLine(stakes: String, structure: String, buyInBB: Int) -> String {
"\(stakes), \(structure), \(buyInBB) big blinds to sit down with."
}
/// What each structure means, in a sentence a person can pick from.
public static func structureName(_ structure: BettingStructure) -> String {
switch structure {
case .fixedLimit: return "Fixed-limit"
case .potLimit: return "Pot-limit"
case .noLimit: return "No-limit"
}
}
public static func structureExplained(_ structure: BettingStructure) -> String {
switch structure {
case .fixedLimit: return "Every bet is one set size, and a round stops after a bet and three raises."
case .potLimit: return "You can bet anything up to what is in the pot."
case .noLimit: return "You can bet anything up to everything you have."
}
}
public static let cashHowItWorks =
"The blinds stay where you set them. Everybody sits down with a hundred big blinds and "
+ "keeps what they win from hand to hand; anyone who loses the lot buys back in for the "
+ "same again. Leave whenever you like — that is cashing out."
/// "Last session: won 12 big blinds over 84 hands, 14 for every 100. Bought back in once."
public static func cashResultLine(netBB: Double, hands: Int, per100: Double, rebuys: Int) -> String {
let won = netBB >= 0 ? "won" : "lost"
let amount = String(format: "%.0f", abs(netBB))
let rate = String(format: "%+.0f", per100)
var line = "Last session: \(won) \(amount) big blinds over \(Self.hands(hands)), \(rate) for every 100."
if rebuys == 1 { line += " Bought back in once." }
if rebuys > 1 { line += " Bought back in \(Self.times(rebuys))." }
return line
}
public static let cashResultNone = "Nothing played yet. What you win and lose is counted in big blinds, the way cash players count it."
/// Fixed-limit: the buttons carry the one size a bet has.
public static func fixedBetTitle(_ amount: Int) -> String { "Bet \(amount)" }
public static func fixedRaiseTitle(_ to: Int) -> String { "Raise to \(to)" }
public static let bettingCapped = "Capped — no more raising this round"
public static let cashStructureReadout = "Limit"
public static let assessmentOffer =
"A measured sitting rather than a game. Each hand puts you in whichever situation I know "
+ "least about, and at the end you get a starting rating and the first list of what your "
+ "game is actually costing you."
public static func assessmentLength(_ hands: Int) -> String {
"About \(hands) hands, roughly twenty-five minutes."
}
public static func assessmentStoppedAt(_ hands: Int) -> String {
"You stopped \(Copy.hands(hands)) in. It carries on from there."
}
/// The starting rating, named as what it is: the number from that one sitting, which never
/// moves. Ken read this card's 54 and the rating card's number as the same thing and asked why
/// one of them did not update — so each card says which hands it is counting and that the
/// other one exists.
public static func assessmentResult(rating: Int, label: String, low: Int, high: Int,
hands: Int) -> String {
"Your starting rating from those \(Copy.hands(hands)): \(rating) — \(label). Those hands "
+ "support somewhere between \(low) and \(high). This card does not change; the "
+ "rating card below follows every hand since."
}
/// When the sitting was played, for the card's subtitle.
public static func assessmentPlayedOn(_ date: Date) -> String {
"Played on \(Copy.dayAndMonth(date))."
}
/// The report's title the one time it opens by itself, at the end of the sitting. Being taken
/// off the felt without a word read as being thrown out of the game; this says why it happened.
public static func assessmentJustFinishedTitle(hands: Int) -> String {
"Your \(hands) hands are done — here is where your game is now"
}
/// "10 September", the way a person says a date.
public static func dayAndMonth(_ date: Date) -> String {
let formatter = DateFormatter()
formatter.locale = Locale(identifier: "en_US")
formatter.dateFormat = "d MMMM"
return formatter.string(from: date)
}
/// What the rating is made of, in the units the arithmetic actually uses. The number on its own
/// read as a verdict from nowhere; this is the figure it is worked out from.
public static func ratingCostLine(costPer100: Double) -> String {
"Your mistakes cost about \(Int(costPer100.rounded())) big blinds for every 100 hands."
}
/// A zero needs a word beside it, or it reads as "you have no rating".
public static func ratingLabel(value: Int, label: String) -> String {
value == 0 ? "\(label) — the bottom of the scale" : label
}
/// The hands the rating is worked out from, saying what is left out of it: drill and re-check
/// hands are practice at one situation with you watching for it, and are kept out on purpose.
public static func ratingHandsWatched(_ n: Int) -> String {
"\(Copy.hands(n)) watched (practice hands are left out)"
}
/// The three hand counts on Home are three different things, and this is the one that adds
/// them up: everything ever saved, split into hands that count towards the rating and hands
/// that were practice — drills and re-checks. Assessment hands count as played.
public static func handsSavedTitle(saved: Int, played: Int, practice: Int) -> String {
practice == 0
? "\(Copy.hands(saved)) saved"
: "\(Copy.hands(saved)) saved — \(played) played, \(practice) practice"
}
public static let focusNone = "Nothing confirmed yet."
public static let focusWorst = "Your most expensive habit right now."
public static let focusEmpty =
"Play some hands and the coach will tell you which mistakes are habits rather than "
+ "accidents. It will not name one until it has seen enough of the same spot to be sure."
/// The line over the panel that reports the sitting on the table right now.
///
/// Built from the hands written down for this sitting with the arranged ones left out, which
/// is what every other screen in the app counts. The panel used to read the coach's own memory
/// instead, and that is not the sitting: it is everything since the app opened, so a
/// tournament, an assessment and two hundred drill hands were being added together, priced per
/// hundred and reported as one evening's poker.
public static func sittingLine(hands: Int, flagged: Int) -> String {
guard hands > 0 else { return "Nothing played this sitting yet." }
let played = "\(hands) hand\(hands == 1 ? "" : "s") this sitting"
// Under the floor the cost per hundred is a dash, and a dash with nothing said about it
// reads as a fault in the app rather than as a refusal.
let thin = hands < GameAnalysis.costFloor
? " Too few hands yet to spread a cost over a hundred of them."
: ""
guard flagged > 0 else { return "\(played), nothing worth flagging.\(thin)" }
return "\(played). \(flagged) decision\(flagged == 1 ? "" : "s") cost you money.\(thin)"
}
public static let handsSavedSubtitle = "Kept from every sitting you have ever played."
public static let handsSavedBlurb =
"Most of what there is to learn here lives in the pattern across all of them rather than in "
+ "any single hand. My game reads the lot and tells you where the money actually goes."
// MARK: - The hand review
public static let nothingToFlag =
"The coach had nothing to flag in this hand. It only speaks when it is confident a decision "
+ "cost you money — silence here means the decisions were reasonable, not that it was not "
+ "watching."
public static let nothingToFlagShort = "The coach had nothing to flag in this hand."
public static let quietOnCloseSpots = "The coach stays quiet on close spots by design."
/// What a hand says about itself when the coach kept quiet and the scorecard still counted
/// something in it.
///
/// The bar for speaking is higher than the bar for counting, deliberately: a third of a big
/// blind is never worth stopping somebody over and is ruinous forty times an hour. That gap is
/// where the app used to contradict itself. A player whose named habit was a cheap one — too
/// many hands, limping — opened the Hands list to check the coach's work and read "clean"
/// against the very hands the habit was counted from. The rule about staying quiet at the table
/// stands; what stopped is the screens afterwards asserting there was nothing there.
///
/// It says nothing about where those decisions are drawn, because it is used on two screens
/// that draw them differently — the hand detail marks each one, the review at the table does
/// not — and a sentence pointing at something that is only sometimes there is worse than one
/// that does not point.
public static func nothingFlaggedButCounted(_ decisions: Int) -> String {
let subject = decisions == 1
? "One decision was"
: "\(decisions) decisions were"
return "Nothing here was worth stopping you over. \(subject) still counted towards a habit "
+ "on your My game list."
}
/// One such decision, said where the player is reading about it.
public static func countedTowards(_ leak: String) -> String {
"Too small to stop you over, and counted all the same. It is one of the decisions behind "
+ "“\(leak)” on your My game list."
}
/// The same, where the habit it belongs to is one this build no longer recognises — an old hand
/// whose leak has since been renamed. The decision still counted; there is no name to give it.
public static let countedTowardsUnnamed =
"Too small to stop you over, and counted all the same. It is one of the decisions behind a "
+ "habit on your My game list."
/// Which of three things a per-hand screen says about one decision.
///
/// The rule lives here, in one place, because three screens ask it — the Hands list, the hand
/// detail and the replay — and they were each asking it in their own words. Two of the three
/// asked whether the coach spoke, which is a different question from whether the decision
/// counted, and answered the second question with the first one's answer.
public enum DecisionNote: Equatable {
/// The coach spoke about it at the table. Its own write-up says what it is.
case flagged
/// The coach kept quiet and the scorecard counted it towards a habit.
case counted
/// Close enough that calling it a mistake would be guessing.
case tooClose
}
public static func decisionNote(spoke: Bool, countsTowardsLeak: Bool) -> DecisionNote {
if spoke { return .flagged }
return countsTowardsLeak ? .counted : .tooClose
}
/// The badge beside a hand in the list. Three states rather than two: a hand can be one the
/// coach kept quiet on and still be one the scorecard counted a habit from, and "clean" is a
/// claim about the second question rather than the first.
public static func handBadge(flagged: Int, counted: Int) -> String {
if flagged > 0 { return "\(flagged) flagged" }
return counted > 0 ? "\(counted) counted" : "clean"
}
/// The line above the decisions in the hand detail. Nil where the coach flagged something —
/// there, the write-ups underneath say it better than a summary would.
public static func handSummary(flagged: Int, counted: Int) -> String? {
guard flagged == 0 else { return nil }
return counted == 0 ? nothingToFlagShort : nothingFlaggedButCounted(counted)
}
// MARK: - My game
/// The line under the heading, which sits directly above the scope bar — so it says what is
/// kept and leaves what is counted to the control that answers it. It used to end "every hand
/// you have ever played is counted here", one line above a bar that can be set to a single
/// sitting, and the subtitle further down said the opposite and was the one telling the truth.
public static let myGameIntro =
"This is the part worth coming back to. One hand tells you almost nothing — there is too "
+ "much luck in a single hand for it to mean much. A few hundred will tell you things about "
+ "how you play that you did not know, and every hand you play is kept, so this only gets "
+ "clearer. Pick which of them you are looking at below."
public static let costingYouSubtitle =
"Your weaknesses, worst first, ranked by what they have actually cost. Counted from every "
+ "hand above, so it follows whichever sitting you are looking at and is still here tomorrow."
public static let tooSoonSubtitle =
"Heading the wrong way, on too few chances to be sure. Named so you know what is being kept "
+ "an eye on — not findings."
public static let everythingMeasuredSubtitle = "The full list, including what you are doing well."
public static let numbersMeasured = "This is measured, not estimated."
public static func numbersAcross(_ sittings: Int) -> String { "Across \(sittings) sittings." }
/// The seat rows, explained without a sentence that stops being true at the end of a
/// tournament.
///
/// It used to say "seat 1 puts in the small blind", which is right at any table of three or
/// more and wrong two-handed, where the dealer pays the small blind and the other seat is
/// numbered 1. Every tournament finishes two-handed, so the table below produces those rows on
/// the way past. The highest number being the dealer holds at every size, so that is what the
/// sentence leans on now.
public static let bySeatSubtitle =
"Where the money comes from. The highest number is the dealer and acts last, and the seats "
+ "that pay before the cards come out lose money by definition; the late numbers are where "
+ "it should be earned back. Pick a row to see where it sits."
public static let whatItCostSubtitle = "The mistakes the coach was confident about, worst first."
/// Why the habits and the rounds of betting in the same panel do not add up.
///
/// How often a habit happens is counted over assessment hands and what it cost is not, which is
/// right — being put in a situation on purpose does not change how often you get it right when
/// you are in it, but it does change what the evening won or lost. Two hand sets an inch apart
/// with nothing saying so is how a screen quietly stops being believed.
///
/// This used to be drawn inside the branch that draws the round-by-round tiles, and those tiles
/// are built from ordinary hands only — so on an assessment-only history there were no tiles,
/// no note, and a list of habits made entirely of the hands the note exists to explain. It sits
/// outside that branch now.
public static func roundsLeaveOutArranged(_ hands: Int) -> String {
"\(hands) arranged hand\(hands == 1 ? "" : "s") — an assessment — "
+ "\(hands == 1 ? "is" : "are") counted in the times above and left out of the money, here "
+ "and everywhere else on this screen. What a hand you were put into won or lost is not a "
+ "fair sample of anything; how often you got the decision right in it still is."
}
public static let worthAnotherLookSubtitle = "The two biggest pots in the hands you are looking at."
public static let thinSeatRows = "Greyed rows have too few hands to read anything into yet."
public static let noFlaggedHands =
"Nothing flagged in these hands. The coach only speaks when it is confident a decision cost "
+ "you money."
/// The line under the headline numbers. Which of the four is owed depends only on how many
/// hands are behind them, so the choice sits here where a test can read it rather than in the
/// view — where the empty one was written and then rendered inside a branch that only ran once
/// there were hands, and so could never appear.
///
/// `arranged` is what stops the fourth case reading as the first. Straight after a forty-hand
/// assessment — the first thing anybody does on a fresh install — this panel counted nought
/// hands, because arranged hands are kept out of every number in it, and then told the player
/// "nothing has been played". They had just played forty.
public static func statisticsFootnote(hands: Int, arranged: Int = 0, enough: Bool) -> String {
if hands == 0 { return arranged > 0 ? statisticsAllArranged(arranged) : statisticsEmpty }
return (enough ? statisticsSettled : statisticsThin(hands)) + " " + flaggedCostNote
}
public static let statisticsEmpty =
"Nothing has been played in what you are looking at, so there is no win rate to work out. "
+ "The dashes above are there instead of noughts on purpose: nought big blinds per hundred "
+ "hands is a result somebody earned, and no hands at all is not one."
public static func statisticsAllArranged(_ hands: Int) -> String {
let played = hands == 1
? "The one hand you are looking at was arranged"
: "All \(Copy.hands(hands)) you are looking at were arranged"
return "\(played) rather than dealt: the seat you sat in and the chips in front of "
+ "everybody were chosen for you, so what they won or lost is no sort of win rate and "
+ "the dashes above say so. What those hands do say is in the habits further up."
}
public static func statisticsThin(_ hands: Int) -> String {
"VPIP is how often you play a hand, PFR how often you raise first. Win rate over \(hands) "
+ "hands is still mostly luck — under a couple of hundred hands, trust the coach's verdicts "
+ "over the number above."
}
public static let statisticsSettled =
"VPIP is how often you play a hand, PFR how often you raise first. A tight, aggressive "
+ "nine-handed game usually sits near 18 / 14."
/// Why the money in this panel is smaller than the money at the top of the screen.
///
/// Two figures in big blinds per hundred hands, a few inches apart, over different sets of
/// decisions. This one is the mistakes the coach spoke about; the habits above also count the
/// cheap decisions it stays quiet on, so their total is always the larger of the two, and the
/// gap grows with exactly the habits the scorecard exists to catch.
///
/// Written so it is true on a screen with no habits on it as well. "Their total is the bigger
/// number" points at a total that is not always there.
public static let flaggedCostNote =
"The cost above counts only the decisions the coach spoke about. A habit named at the top "
+ "of this screen is priced over the small ones it stays quiet on as well, so that figure "
+ "is always the larger of the two."
// MARK: - The scorecard
public static let scorecardEmpty =
"A leak is the same mistake in the same spot, often enough to be a habit rather than an "
+ "accident. Nothing is put on this list until there have been enough chances to be sure of "
+ "it, so an empty list is good news."
/// Both halves are needed. The list draws a row for every habit in the catalogue, and the ones
/// that have never come up used to get a short green bar of their own — the same picture as a
/// habit measured and going well, directly above a sentence saying a short bar means the thing
/// is working. Those rows have no bar at all now, and this says what that means.
public static let barMarkNote =
"The mark on each bar is the line past which this app will call something a habit. It is "
+ "set well clear of where a steady player actually sits, so that a bad run of cards is "
+ "never mistaken for one. A bar that stops short of the mark is a part of your game that "
+ "is working. A row with no bar at all has not come up yet, which is a different thing "
+ "from going well."
public static let costNotKnown = "cost not known"
// MARK: - The hand list
public static let flaggedOnlyToggle = "Only hands the coach flagged"
public static let noHandsYet = "No hands yet"
public static let noHandsYetBlurb = "Sit down and play — every hand is kept, and stays here between visits."
public static let nothingFlagged = "Nothing flagged"
public static let nothingFlaggedBlurb =
"Every hand you are looking at was played well enough that the coach kept quiet."
public static let pickAHand = "Pick a hand"
public static let pickAHandBlurb =
"Every hand you have played, with what the coach thought at each decision — including the "
+ "ones it chose not to comment on. Hands played recently can be stepped through one action "
+ "at a time, forwards or back."
public static let replayOffer = "one round of betting at a time, forwards or back"
public static let noComment = "no comment"
// MARK: - Playing a hand back
public static let cannotReplay = "This hand cannot be played back"
public static let cannotReplayBlurb =
"It was saved before the app started writing down every action in a hand. The summary above "
+ "is everything that was kept. Hands you play from now on can be stepped through in full."
public static func coachSaidNothing(about taken: String) -> String {
"Nothing to say about this one. You chose to \(taken), which was close enough to the best "
+ "answer that calling it a mistake would be guessing."
}
// MARK: - The assessment
public static func assessmentIntro(_ hands: Int) -> String {
"You play \(hands) hands and I watch. At the end you get a starting rating and the first "
+ "honest list of what your game is costing you."
}
public static let everyHandIsChosen = "Every hand is chosen"
public static let everyHandIsChosenBlurb =
"I pick the seat you sit in and how many big blinds everybody has in front of them, and I "
+ "change them between hands. That is so I can put you in the situations I have not seen you "
+ "handle yet instead of waiting for the deal to get round to them. What each hand is "
+ "looking at I keep to myself until the end, because being told what is being measured "
+ "changes it — you get the whole list in the report."
public static let cardsNotChosen = "The cards are not chosen"
public static let cardsNotChosenBlurb =
"Every hand comes off a fresh shuffle. I never give you a hand to make a point with, because "
+ "the moment I did, everything at the end of this would be worth nothing."
public static let stopWhenever = "You can stop whenever you like"
public static let stopWheneverBlurb =
"Walking away costs nothing. The hands you played are kept, and picking it back up carries "
+ "on from where you left it."
public static let noPretending = "It will not pretend to know more than it does"
public static func noPretendingBlurb(_ hands: Int) -> String {
"\(hands) hands is a short look at anybody. The report says what it could measure, what it "
+ "could not get near, and how far out its own numbers could be."
}
public static let assessmentFinishedTitle = "Where your game is now"
public static let assessmentStoppedTitle = "As far as you got"
public static let startingRating = "Your starting rating"
public static func ratingRangeLine(low: Int, high: Int, hands: Int) -> String {
"\(low) to \(high) is what \(Copy.hands(hands)) support"
}
public static let alreadyClear = "What is already clear"
public static let whereToLookFirst = "Where I would look first"
public static let whereToLookFirstNote =
"Not proved. These happened often enough to be worth your attention and not often enough "
+ "for me to call them habits."
public static let nothingToName = "Nothing to name"
public static let whatIGotToLookAt = "What I got to look at"
public static func decisionsAcrossHands(decisions: Int, hands: Int) -> String {
"\(decisions) decisions across \(Copy.hands(hands))."
}
public static let whatIArranged = "What I arranged, and what I did not"
// MARK: - The drills
public static func drillTitle(_ leak: String) -> String { "Drill: \(leak)" }
public static let checkingBackTitle = "Checking back"
public static let checkingBackHint =
"You cleared this one. This is the same situation again, now you have stopped thinking "
+ "about it."
public static let sameSituation = "The same situation, over and over"
public static func sameSituationBlurb(seat: String, depthInBB: Int, lean: String) -> String {
"I put you in \(seat) with \(depthInBB) big blinds in front of everybody, \(lean), because "
+ "that is the arrangement most likely to hand you this decision. If it stops producing it, "
+ "I change the arrangement rather than waiting."
}
// MARK: Seat drills — where you lose most
public static let whereYouLoseMost = "Where you lose most"
public static let whereYouLoseMostSubtitle =
"Which seat your decisions go wrong in most often, off every hand the deal put you in — "
+ "never the ones the app arranged."
public static let seatDrillTitle = "Drill this seat"
public static func drillSeatTitle(_ seat: String) -> String { "Drill \(seat)" }
public static let seatDrillLocked = "Included with a subscription"
public static let seatDrillHint =
"Every hand from the one seat, until the decisions you make there go wrong no more often "
+ "than a steady player's do."
public static let sameSeat = "The same seat, every hand"
public static func sameSeatBlurb(seat: String, lean: String) -> String {
"I put you in \(seat) every hand, with 100 big blinds in front of everybody, \(lean). "
+ "The seat is the whole of what is arranged; what comes at you from it is up to the deal."
}
public static let seatCounted = "Everything you decide there counts"
public static let seatCountedBlurb =
"A drill at a habit counts one situation. A drill at a seat counts every decision the "
+ "coach looks at from it, whatever the habit — that is what made the seat expensive."
public static let seatDrillsHeading = "By seat"
public static let seatDrillsFooter =
"A seat drill sits you in one seat every hand. The seat to start with is the one My game "
+ "says you lose most from."
public static func seatBannerLine(_ seat: String, watching: String?) -> String {
guard let watching else { return "\(seat) every hand." }
return "\(seat) every hand, watching for \(watching)."
}
public static let cardsStillNotChosen = "The cards are still not chosen"
public static let cardsStillNotChosenBlurb =
"Every hand comes off a fresh shuffle, exactly as it does at a normal table. I arrange the "
+ "situation and never the hand you are holding in it."
public static let checkTakesAsLong = "It takes as long as it takes"
public static func checkTakesAsLongBlurb(_ chances: Int) -> String {
"\(chances) chances at it and then an answer, either way. A check that ran until it liked "
+ "the result would not be a check."
}
public static let drillEndsWhenStopped = "It ends when you have actually stopped"
public static let drillEndsWhenStoppedBlurb =
"Not after a set number of hands. It ends when you have had enough chances for the rate to "
+ "be worth reading at all, and that rate is counted with one slip always added in, so a "
+ "clean run cannot finish it early. Get one wrong and it takes a few more, which is the "
+ "whole point of it. If it will not clear, it says so rather than letting you off."
public static let drillHandsKeptOut = "These hands are kept out of your numbers"
public static let drillHandsKeptOutBlurb =
"Practice at one situation, knowing it is coming, is not the same as how you play when it "
+ "turns up unannounced. So drill hands do not touch your win rate and they do not touch "
+ "your leaks. What counts is the check a few days from now, once you have stopped working "
+ "on it."
public static let whatHappened = "What happened"
public static func drillHandsAndChances(hands: Int, chances: Int) -> String {
"\(Copy.hands(hands)), \(Copy.times(chances)) in the situation."
}
public static let willCheckBack = "I will check back"
public static func inAboutDays(_ days: Int) -> String { "In about \(days) days." }
public static let willCheckBackBlurb =
"An evening of getting something right while you are watching for it is not the same as "
+ "having stopped doing it. So this goes in the diary rather than in the past: in a few "
+ "days, once you have stopped working on it, you get the same situation again, and that is "
+ "the one that decides whether it comes off your list. It will be waiting on the home "
+ "screen when it is due, however long the app is shut."
/// What is owed after a check that held up, which is not the same sentence as after a drill.
/// The gap has just doubled, and a screen that says "I will check once more" and then does not
/// say when leaves the player with nothing to plan around.
public static let checkHeldBlurb =
"It has held up once, so the gap doubles before the next one. That is on purpose: the first "
+ "check asks whether the fix survived the week, the third asks whether it survived not "
+ "thinking about it at all. Nothing is owed from you in the meantime — it will be waiting "
+ "on the home screen when it is due, however long the app is shut."
public static let backOnYourList = "Back on your list"
public static let backOnYourListBlurb =
"This is why the check exists. It is on the My game screen again, with the same write-up it "
+ "had before, and you can drill it again whenever you like — nothing about the first drill "
+ "was wasted."
public static let whatToDoAboutIt = "What to do about it"
public static let stoppingCostsNothing =
"Nothing is lost by stopping here. The hands are written down and picking the drill back up "
+ "carries on from where you left it."
public static let checkIsDue = "A check is due"
public static func chancesAtTheSameSituation(_ chances: Int) -> String {
"\(chances) chances at the same situation"
}
public static let drillHalfDone = "A drill left half done"
// MARK: Practice
public static let practiceTitle = "Practice"
public static let practiceSubtitle =
"Pick a situation and play nothing else until it stops needing to be worked out."
public static let practicePanelSubtitle = "Choose a situation and drill it, whenever you like."
public static let practiceBlurb =
"The coach hands you drills once it has watched you long enough to be sure about a habit. "
+ "This is the other way in: anything on the list can be practised now, whether or not it "
+ "has ever been a problem for you."
public static let practiceOpen = "Choose something to practise"
public static let practiceSomethingElse = "Practise something else"
public static let practiceStart = "Practise this"
public static let practiceBeforeFlop = "Before the flop"
public static let practiceOnTheFlop = "On the flop"
public static let practiceLaterStreets = "Turn and river"
public static let practiceFooter =
"Hands played in a drill are kept apart from your real ones. They never move your rating, "
+ "and they are left out of everything on My game — an arranged table would flatter or "
+ "punish those numbers without either being true."
/// A word for a drill somebody has already been through, or nothing at all where they have not.
static func practiceMark(_ stage: DrillBook.Stage) -> String? {
switch stage {
case .notStarted: return nil
case .running: return "In progress"
case .due: return "Check due"
case .holding: return "Cleared"
case .reopened: return "Back on your list"
case .stalled: return "Unfinished"
}
}
public static let practiseOneThing = "Practise one thing"
public static let practiseOneThingBlurb =
"A set of hands that puts you in this one situation over and over, until you have got it "
+ "right often enough for that to mean something. Then I check back in a few days to see "
+ "whether it stuck."
public static func drillHintLine(_ hint: String) -> String { "Drill: \(hint)" }
// MARK: - The ladder
public static func ladderIntro(_ tables: Int) -> String {
"\(tables) tables, each one harder than the last — and what makes one harder than the next "
+ "is who is sitting at it. Beat a table over enough hands and you are moved up. Stop "
+ "beating it and you are moved back down, which is the only way the rung you are on means "
+ "anything at all."
}
public static func handsAtThisTable(played: Int, needed: Int) -> String {
"\(played) of \(Copy.hands(needed)) at this table"
}
public static let nineSeatsBlurb =
"Nine seats — you and eight of them. Seats 1 and 2 pay before the cards come out, 9 is the "
+ "dealer, and the whole ring shifts one seat every hand."
public static func nextTableIs(_ tier: String) -> String { "Your next table is \(tier)." }
public static func nextTableIsAgain(_ tier: String) -> String { "Your next table is \(tier) again." }
public static let topOfTheLadder = "The top of the ladder"
public static let whatItTakesToMoveUp = "What it takes to move up"
public static let costingAtThisTable = "What is costing you at this table"
public static let everyRung = "Every rung"
public static func moveUpAt(_ rating: Int) -> String { "move up at \(rating)" }
public static let sameEightPeople = "The same eight people, playing their styles better."
public static func whoChanges(leaving: String, arriving: String) -> String {
"Walking in there, \(leaving) get up and \(arriving) sit down — and they play a little "
+ "better than these ones do. Who is at the table is the bigger half of that by a long way."
}
public static let nobody = "nobody"
// MARK: - The table picture
public static let seatsPayBeforeCards =
"Seats 1 and 2 put money in before anyone has seen a card — that is the chip. D is the "
+ "dealer, and it moves one seat along every hand, so everybody sits in every seat."
// MARK: - Settings
public static let coloursSubtitle = "Pick whichever you can read most easily."
public static let textSizeSubtitle = "How big everything is written, the coach's write-ups most of all."
public static let soundSubtitle = "Chips, knuckles on the felt, cards sliding away."
public static let playTableSounds = "Play table sounds"
/// "1 hand", "2 hands". Written once because a count of one turns up all over this app — the
/// first hand of a sitting, a habit seen once, a rating built on a single hand — and every
/// place that gets it wrong reads as software rather than as somebody talking to you.
public static func hands(_ n: Int) -> String { n == 1 ? "1 hand" : "\(n) hands" }
/// The same for the other things counted in ones here.
public static func sittings(_ n: Int) -> String { n == 1 ? "1 sitting" : "\(n) sittings" }
public static func times(_ n: Int) -> String { n == 1 ? "once" : "\(n) times" }
public static func chances(_ n: Int) -> String { n == 1 ? "1 chance" : "\(n) chances" }
// MARK: Who made it
public static let madeByTitle = "The (Digital) Ad Lab"
public static let madeBySubtitle = "Applied data science and AI, by Ken Rona."
public static var madeByBlurb: String {
"\(AppVariant.current.displayName) is one of the things built at The (Digital) Ad Lab, "
+ "alongside the Refacto newsletters and a handful of other tools. Everything it knows "
+ "about your game stays on this device: nothing is uploaded, there is no account to make, "
+ "and nobody else can see a hand you have played."
}
public static let visitAdLab = "Visit the Ad Lab"
/// The About link, the page it opens and the privacy page all follow the product, so the
/// cash app never sends anyone to the tournament app's pages (Apple checks the privacy URL).
public static var readAboutPokerTutor: String { "About \(AppVariant.current.displayName)" }
public static let adLabURL = "https://www.thedigitaladlab.com/"
public static var pokerTutorPageURL: String { AppVariant.current.pageURL }
public static var privacyURL: String { AppVariant.current.privacyURL }
public static let privacyLink = "Privacy"
public static func versionLine(_ version: String) -> String {
// The LEGAL name, without the parentheses. "The (Digital) Ad Lab" is the brand and belongs
// in `madeByTitle`; this line is an ownership claim and has to match the entity as it is
// registered in Georgia, which is what Apple and the App Store will show.
"\(AppVariant.current.displayName) \(version) — The Digital Ad Lab, LLC"
}
// MARK: The basics
public static let basicsTitle = "The basics"
public static func basicsSubtitle(_ count: Int) -> String {
"\(count) things people who have played for years assume everybody knows."
}
public static let basicsPanelSubtitle =
"The short answers nobody says out loud, starting with how big a bet is supposed to be."
public static let basicsBlurb =
"Most of what makes somebody hard to play against is not clever. It is a handful of things "
+ "they learned so long ago that they no longer know they know them, and nobody at a table "
+ "will ever explain any of it. This is that list, with the numbers worked through."
public static let basicsOpen = "Read the basics"
public static let workedThrough = "Worked through"
public static let practiseThis = "Practise this"
public static let basicsFooter =
"None of these are laws. They are where a good player starts before they have a reason to "
+ "do something else, which is a different thing — and knowing which one you are departing "
+ "from is most of what being good at this means."
public static func pokerWordsSubtitle(_ count: Int) -> String { "\(count) of them, in plain English." }
public static let pokerWordsBlurb =
"Anywhere the app has to use a poker word — the numbers on My game, the old seat names, "
+ "what the coach says about a hand — you can tap it and the meaning opens where you are "
+ "reading. The whole list is here too."
public static let gettingStarted = "Getting started"
public static let gettingStartedSubtitle = "The short walk-through you saw the first time."
public static let startOver = "Start over"
public static let startOverSubtitle = "Throw away every saved hand and let the coach meet you fresh."
public static let nothingSavedYet = "There is nothing saved yet."
/// What "Start over" actually costs, said in the panel as well as in the dialog.
///
/// The rung you are on, the checks waiting on your drills and how far through an assessment you
/// got are all counted back off the hands themselves — there is no second copy of any of them
/// anywhere. That is what makes them survive the app being closed, and it is also what makes
/// clearing the hands clear them. Saying "resets your rating" and stopping there described
/// about a third of what the button does.
public static func handsAcrossSittings(hands: Int, sittings: Int) -> String {
"\(Copy.hands(hands)) across \(Copy.sittings(sittings)), kept since the first one. Clearing them also "
+ "resets your rating, puts you back at the first table on the ladder and cancels any drill "
+ "checks that were waiting, because all three are counted back off the hands themselves."
}
public static let whereHandsAreKept = "Where your hands are kept"
public static let whereHandsAreKeptSubtitle = "They stay put — updating the app does not touch them."
public static let whereHandsAreKeptBlurb =
"Every hand is written down as it finishes and kept outside the app itself, so a new version "
+ "arrives without disturbing anything you have played. Resetting is the only thing that "
+ "clears them, and even then the old file is set aside rather than deleted."
public static let resetTitle = "Reset history?"
public static let resetWarning =
"This clears every hand you have played and resets your rating. It also puts you back at "
+ "the first table on the ladder and cancels any drill checks that were waiting, because "
+ "both of those are counted back off the hands themselves. If you are at a table, you "
+ "will leave it. A copy of the old history is kept on disk in case this was a mistake."
// MARK: - The glossary screen
public static let pokerWordsTitle = "Poker words"
public static func pokerWordsIntro(_ count: Int) -> String {
"\(count) words, in plain English. Anywhere one of these turns up in the app you can tap it "
+ "and get the same answer without leaving what you were reading."
}
public static let noWordMatches =
"No word here matches that. Every term the app uses is on this list, so it is worth trying a "
+ "shorter piece of it."
// MARK: - The walk-through
public struct TutorialPage: Sendable {
public let icon: String
public let title: String
public let body: String
}
/// The tutorial for the game this product plays.
public static var tutorial: [TutorialPage] { tutorial(for: AppVariant.current.format) }
public static func tutorial(for format: AppVariant.Format,
structure: BettingStructure = .noLimit) -> [TutorialPage] {
guard format == .cash else { return tournamentTutorial }
// At fixed-limit there is no tray to count chips out of — the buttons carry the one size
// a bet has — so the page about counting chips describes a screen the player never sees.
return structure == .fixedLimit
? cashTutorial.filter { $0.title != chipPageTitle }
: cashTutorial
}
static let chipPageTitle = "Bet by counting out chips"
/// The cash app's tutorial: the same six things, with the first page saying what a cash table
/// is, and one more page for the thing that is only true there — the session is yours to end.
public static let cashTutorial: [TutorialPage] =
[TutorialPage(icon: "suit.spade.fill",
title: "This is a coach, not a card game",
body: "You sit down at a cash table with a hundred big blinds against eight "
+ "opponents, at the stakes and the kind of betting you choose, and "
+ "afterwards someone tells you which of your decisions cost money. "
+ "Winning the hand is not the point. Playing it well is — and those "
+ "are different things more often than you would like. Pick "
+ "fixed-limit and the coach judges that game, which is a different "
+ "one: bets have one size, so the price to keep going is nearly "
+ "always right, and the expensive mistakes are folds."),
TutorialPage(icon: "banknote.fill",
title: "The session is yours to end",
body: "The blinds never move and nobody is knocked out. What you win stays "
+ "in front of you, what you lose can be bought back, and you leave "
+ "when you like. Your result is counted the way cash players count "
+ "it: big blinds won or lost, and how many for every hundred hands.")]
+ tournamentTutorial.dropFirst()
public static let tournamentTutorial: [TutorialPage] = [
TutorialPage(icon: "suit.spade.fill",
title: "This is a coach, not a card game",
body: "You play real hands against eight opponents, and afterwards someone "
+ "tells you which of your decisions cost money. Winning the hand is not "
+ "the point. Playing it well is — and those are different things more "
+ "often than you would like."),
TutorialPage(icon: "hand.thumbsup.fill",
title: "Silence is good news",
body: "Most hands, nothing will be said. That is deliberate. The coach only "
+ "speaks when it is confident a decision actually cost you something, "
+ "because a coach that second-guesses everything is one you stop "
+ "believing. A hand you played well gets a thumbs up and deals straight "
+ "on."),
TutorialPage(icon: "chart.bar.doc.horizontal",
title: "The real learning is in My game",
body: "One hand tells you almost nothing — poker has too much luck in it for "
+ "that. Fifty hands start to show a pattern, and a few hundred will tell "
+ "you things about how you play that you did not know. Every hand is "
+ "kept, so that picture only gets clearer. Go and look at it often; it is "
+ "the part of this app that changes how you play."),
TutorialPage(icon: "circle.grid.3x3.fill",
title: "Bet by counting out chips",
body: "Pick chips up to build your bet: the hundred twice and the fifty once "
+ "is 250. Undo puts the last one back. It is a click or two slower than a "
+ "slider, which is the idea — a bet you had to count out is a bet you "
+ "thought about."),
TutorialPage(icon: "person.2.fill",
title: "Nobody tells you who is who",
body: "The players around you have styles — some far too loose, some who never "
+ "bluff — but the app will not label them. Working out who you are up "
+ "against is the skill. As your game improves, the people you are sat "
+ "with quietly get better too."),
TutorialPage(icon: "clock.arrow.circlepath",
title: "Nothing is thrown away",
body: "Every hand you play is saved and stays saved, including through "
+ "updates. You can go back through any of them, see what the coach "
+ "thought at each decision, and compare one sitting against everything "
+ "you have ever played."),
]
// MARK: - The cards
public static let cardColoursTitle = "Card colours"
public static let cardColoursSubtitle =
"Clubs and spades are both black in a traditional deck, and at the size a phone draws them "
+ "the only difference is a small notch at the bottom."
public static let tapToPutItBack = "Touch anywhere to put it back."
public static let touchACard =
"Touch any card on the table to hold it up large enough to read."
// MARK: - The subscription
/// Apple rejects a paywall that does not say, on the screen where the buyer taps buy, what it
/// costs, how long it lasts, and that it renews until cancelled. Those three facts are in
/// `paywallPrice` and it must not be shortened into something friendlier.
public static let paywallTitle = "The free hands are used up"
public static func paywallBlurb(_ played: Int) -> String {
"That is \(hands(played)) played. A subscription keeps the table open: as many hands as "
+ "you want, every drill, and the coach watching all of them."
}
/// The renewal terms, in the buyer's own currency. `price` comes from Apple — never build it.
public static func paywallPrice(_ price: String) -> String {
"\(price) a year. It renews every year until you cancel, and you can cancel any time in "
+ "your Apple Account settings."
}
/// Said instead of a price when Apple has not answered. A buy button with no price beside it
/// is somebody being asked to agree to a number nobody has shown them.
public static let paywallNoPrice =
"The price could not be loaded just now. Check your connection and try again."
public static let paywallKeepsYourHands =
"Everything you have already played stays where it is. Your hands, your numbers and the "
+ "coach's write-ups are yours whether you subscribe or not."
public static let paywallRestoreNote =
"Already subscribed, or playing on a new phone? Restore it here."
/// Ask to Buy, or a bank wanting confirmation. Nothing has been charged and nothing is owed.
public static let paywallPendingNote =
"That purchase is waiting for somebody else to approve it, and nothing has been charged. "
+ "The table opens by itself once it goes through."
/// Said throughout the free hands, so the price is not the first anybody hears of it.
public static func handsLeftWarning(_ left: Int) -> String {
"\(left) of your \(FreeTrial.freeHands) free hands left."
}
// MARK: The free tier — one habit in full, the rest blurred, one drill
/// Over the blurred rows, with the count. Said the same way on the scorecard and the report.
public static func hiddenHabits(_ count: Int) -> String {
count == 1
? "One more habit is in your game. See it, and everything else, with a subscription."
: "\(count) more habits are in your game. See them all with a subscription."
}
/// The one row a free player is shown, when it is not yet proved.
public static let freeRowNotProved =
"Your most common habit so far, on too few chances to be sure. Play on and it firms up."
/// The one row, when it is proved.
public static let freeRowNamed = "Your most common habit. This is the one to work on first."
/// What a screen reader says for a blurred row, in place of the name.
public static let hiddenRowLabel = "A habit, hidden until you subscribe"
/// The practice button on a locked row.
public static let practiceLocked = "Included with a subscription"
/// The Practice screen's line for a free player.
public static let practiceFreeNote =
"Your free hands come with the drill for your most common habit. The rest open with a subscription."
/// Home's "what to work on" for a free player with nothing shown yet.
public static let focusFreeEmpty =
"Play your free hands and the coach names your most common habit here."
public static let subscribedTitle = "Subscribed"
public static let subscribedSubtitle =
"Renews every year. You can manage it or cancel it in your Apple Account settings."
/// Apple's own standard terms. Every subscription paywall has to link to them, and to a privacy
/// policy — `privacyURL` above is the other half.
public static let termsURL = "https://www.apple.com/legal/internet-services/itunes/dev/stdeula/"
public static let termsLink = "Terms of Use"
// MARK: - Everything above, for the suite
/// Every sentence on the list, with the ones that take a number filled in.
///
/// The filled-in values are ordinary ones rather than edge cases: the test is reading English,
/// and a sentence reads the same whether the number in it is 12 or 40.
public static var prose: [String] {
var lines: [String] = Basics.allText + [
cashSitDownTitle, cashSitDownButton, cashStakesLabel, cashStructureLabel,
cashOfferLine(stakes: "25/50", structure: "no-limit", buyInBB: 100),
structureName(.fixedLimit), structureName(.potLimit), structureName(.noLimit),
structureExplained(.fixedLimit), structureExplained(.potLimit), structureExplained(.noLimit),
cashHowItWorks, cashResultLine(netBB: 12, hands: 84, per100: 14.3, rebuys: 1),
cashResultLine(netBB: -3, hands: 20, per100: -15, rebuys: 2), cashResultNone,
fixedBetTitle(100), fixedRaiseTitle(200), bettingCapped, cashStructureReadout,
basicsTitle, basicsPanelSubtitle, basicsBlurb, basicsOpen, workedThrough,
basicsFooter, basicsSubtitle(10), practiseThis,
practiceTitle, practiceSubtitle, practicePanelSubtitle, practiceBlurb,
practiceOpen, practiceStart, practiceFooter, practiceSomethingElse,
madeByTitle, madeBySubtitle, madeByBlurb, visitAdLab, readAboutPokerTutor,
privacyLink, versionLine("1.0"),
appTagline, ratingSettling, ratingSettled, ratingUnknown, tournamentOffer, assessmentOffer, focusNone, focusWorst, focusEmpty, handsSavedSubtitle, handsSavedBlurb,
assessmentResult(rating: 54, label: "Solid amateur", low: 46, high: 61, hands: 40),
assessmentPlayedOn(Date(timeIntervalSince1970: 1_788_000_000)),
assessmentJustFinishedTitle(hands: 40), ratingCostLine(costPer100: 23.4),
ratingLabel(value: 0, label: "Beginner"), ratingHandsWatched(199),
handsSavedTitle(saved: 271, played: 199, practice: 72),
nothingToFlag, nothingToFlagShort, quietOnCloseSpots, countedTowardsUnnamed,
myGameIntro, costingYouSubtitle, tooSoonSubtitle, everythingMeasuredSubtitle,
numbersMeasured, bySeatSubtitle, whatItCostSubtitle, worthAnotherLookSubtitle,
thinSeatRows, noFlaggedHands, statisticsEmpty, statisticsSettled, flaggedCostNote,
scorecardEmpty, barMarkNote, costNotKnown,
flaggedOnlyToggle, noHandsYet, noHandsYetBlurb, nothingFlagged, nothingFlaggedBlurb,
pickAHand, pickAHandBlurb, replayOffer, noComment,
cannotReplay, cannotReplayBlurb,
everyHandIsChosen, everyHandIsChosenBlurb, cardsNotChosen, cardsNotChosenBlurb,
stopWhenever, stopWheneverBlurb, noPretending,
assessmentFinishedTitle, assessmentStoppedTitle, startingRating,
alreadyClear, whereToLookFirst, whereToLookFirstNote, nothingToName,
whatIGotToLookAt, whatIArranged,
checkingBackTitle, checkingBackHint,
sameSituation, cardsStillNotChosen, cardsStillNotChosenBlurb,
whereYouLoseMost, whereYouLoseMostSubtitle, seatDrillTitle, seatDrillLocked,
seatDrillHint, sameSeat, seatCounted, seatCountedBlurb, seatDrillsHeading,
seatDrillsFooter,
checkTakesAsLong, drillEndsWhenStopped, drillEndsWhenStoppedBlurb,
drillHandsKeptOut, drillHandsKeptOutBlurb, whatHappened,
willCheckBack, willCheckBackBlurb, checkHeldBlurb, backOnYourList, backOnYourListBlurb,
whatToDoAboutIt, stoppingCostsNothing, checkIsDue, drillHalfDone,
practiseOneThing, practiseOneThingBlurb,
nineSeatsBlurb, topOfTheLadder, whatItTakesToMoveUp, costingAtThisTable, everyRung,
sameEightPeople, nobody, seatsPayBeforeCards,
coloursSubtitle, textSizeSubtitle, soundSubtitle, playTableSounds, pokerWordsBlurb,
gettingStarted, gettingStartedSubtitle, startOver, startOverSubtitle, nothingSavedYet,
whereHandsAreKept, whereHandsAreKeptSubtitle, whereHandsAreKeptBlurb,
resetTitle, resetWarning,
pokerWordsTitle, noWordMatches,
cardColoursTitle, cardColoursSubtitle, tapToPutItBack, touchACard,
paywallTitle, paywallNoPrice, paywallKeepsYourHands, paywallRestoreNote,
paywallPendingNote, subscribedTitle, subscribedSubtitle, termsLink,
]
lines += [
assessmentLength(40), assessmentStoppedAt(15),
assessmentResult(rating: 62, label: "Steady", low: 48, high: 74, hands: 40),
numbersAcross(3), statisticsThin(60), coachSaidNothing(about: "call"),
roundsLeaveOutArranged(40), roundsLeaveOutArranged(1),
statisticsAllArranged(40), statisticsAllArranged(1),
statisticsFootnote(hands: 0, arranged: 40, enough: false),
statisticsFootnote(hands: 60, arranged: 40, enough: false),
statisticsFootnote(hands: 400, enough: true),
nothingFlaggedButCounted(1), nothingFlaggedButCounted(3),
countedTowards("Playing too many hands"),
assessmentIntro(40), noPretendingBlurb(40),
ratingRangeLine(low: 48, high: 74, hands: 40),
decisionsAcrossHands(decisions: 96, hands: 40),
drillTitle("Folding the big blind too often"),
sameSituationBlurb(seat: "seat 2 of 9 (the big blind)", depthInBB: 40,
lean: "with a raiser or two behind you"),
checkTakesAsLongBlurb(12), drillHandsAndChances(hands: 30, chances: 14),
inAboutDays(5), chancesAtTheSameSituation(12),
drillHintLine("play back more of the hands worth playing back"),
ladderIntro(5), handsAtThisTable(played: 80, needed: 200),
nextTableIs("the kitchen table"), nextTableIsAgain("the kitchen table"),
moveUpAt(60), whoChanges(leaving: "two calling stations", arriving: "two regulars"),
pokerWordsSubtitle(72), pokerWordsIntro(72),
handsAcrossSittings(hands: 900, sittings: 14),
sittingLine(hands: 0, flagged: 0), sittingLine(hands: 24, flagged: 0),
sittingLine(hands: 24, flagged: 3), sittingLine(hands: 60, flagged: 3),
paywallBlurb(100), paywallBlurb(1), paywallPrice("$9.99"),
handsLeftWarning(12), handsLeftWarning(1), hiddenHabits(1), hiddenHabits(4),
freeRowNotProved, freeRowNamed, hiddenRowLabel, practiceLocked, practiceFreeNote, focusFreeEmpty,
]
lines += tournamentTutorial.flatMap { [$0.title, $0.body] }
lines += cashTutorial.flatMap { [$0.title, $0.body] }
lines += [cashRungLine(rung: "50/100", chosen: "50/100"), cashRungLine(rung: "50/100", chosen: "100/200"),
cashRungLine(rung: "50/100", chosen: "25/50"), cashWinRateLine(rate: 8, hands: 213)]
return lines
}
/// Where the ladder has the player, said on the cash setup panel beside the stakes they chose.
public static func cashRungLine(rung: String, chosen: String) -> String {
if rung == chosen {
return "\(rung) is where the ladder has you. Play well there over enough hands and it "
+ "will suggest the next stakes up; any stakes are yours to sit at whenever you like."
}
return "The ladder has you at \(rung). You can sit down at \(chosen) whenever you like — each "
+ "stakes keeps its own record, and the ladder reads the one you are playing."
}
public static func cashWinRateLine(rate: Double, hands: Int) -> String {
String(format: "%+.0f big blinds for every 100 hands, over \(hands) hands.", rate)
}
}
/// What the running build calls itself, read from the bundle rather than written down twice.
///
/// A version number kept in the source is a version number that disagrees with the store within a
/// release or two, and the one place it must be right is the screen somebody reads before writing
/// to say something is broken.
public enum AppInfo {
public static var version: String {
let marketing = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String
return marketing ?? "1.0"
}
}