← poker-tutor
import Foundation
import TestKit
import PokerCore
import PokerUI
import PokerSession
/// The text-size setting, tested where it actually lives: `TextScale` does the multiplying and the
/// remembering, and `Theme` is a one-line pass-through to it that the screens read as they draw.
/// Nothing here goes near SwiftUI — the point is that the arithmetic and the restart both hold.
public func textSizeTests(_ t: TestRun) {
t.suite("text size") { t in
t.test("normal is the size everything was drawn at before") {
t.equal(TextScale.normal.multiplier, 1)
t.equal(TextScale.normal.size(13), 13)
t.equal(TextScale.normal.size(20), 20)
}
t.test("each step is bigger than the one before it") {
let sizes = TextScale.allCases.map { $0.size(13) }
t.equal(sizes.count, 4, "four steps, named for humans")
for (smaller, larger) in zip(sizes, sizes.dropFirst()) {
t.expect(larger > smaller, "\(larger) should be larger than \(smaller)")
}
t.equal(TextScale.allCases.map(\.name),
["Small", "Normal", "Large", "Extra large"])
}
t.test("the coach's write-ups grow by something worth having") {
// The whole point of the setting: a paragraph of feedback set at 13 has to come out
// noticeably larger, not a point larger.
t.expect(TextScale.extraLarge.size(13) >= 17,
"extra large left the coach's text at \(TextScale.extraLarge.size(13))")
t.expect(TextScale.small.size(13) < 13)
}
t.test("the walk-through is bigger than ordinary text at every setting") {
// It is the one screen somebody reads BEFORE they could have found the text-size
// setting, so it cannot lean on that setting existing. Ken read it on a phone for the
// first time on 2026-08-22 and it was too small; if anybody ever "tidies" the intro
// back down to the ordinary body size, this fails rather than shipping.
for scale in TextScale.allCases {
let ordinary = scale.size(14)
let intro = scale.size(TextScale.introBase(14))
t.expect(intro > ordinary,
"at \(scale.name) the intro is \(intro) against ordinary text at \(ordinary)")
}
}
t.test("the intro grows with the setting as well as starting bigger") {
// Starting bigger is not a substitute for the setting — somebody who turns the type up
// has to see the intro grow too, which is exactly what a hard-coded size would break.
let sizes = TextScale.allCases.map { $0.size(TextScale.introBase(14)) }
for (smaller, larger) in zip(sizes, sizes.dropFirst()) {
t.expect(larger > smaller, "\(larger) should be larger than \(smaller)")
}
}
t.test("text with a fixed-size box around it stops growing at large") {
// Seat names, chip captions and the buttons under the felt. They still grow — just not
// past Large, because past Large the table stops fitting on the screen.
t.equal(TextScale.extraLarge.fittedSize(12), TextScale.large.fittedSize(12))
t.expect(TextScale.extraLarge.fittedSize(12) > TextScale.normal.fittedSize(12))
t.expect(TextScale.extraLarge.fittedSize(12) < TextScale.extraLarge.size(12),
"the cap did nothing")
// Below the cap the two agree, so capping never shrinks anything.
for scale in [TextScale.small, .normal, .large] {
t.equal(scale.fittedSize(11), scale.size(11))
}
}
// Ken, 2026-08-02: "the text on the table screen should track settings". It used to stop at
// Large, because the seat boxes were a fixed width and the writing spilled out of them past
// that. The boxes and the ring they sit on now carry the setting too, so the words can grow
// the whole way — and this is the arithmetic that says the felt still fits when they do.
t.test("the felt still holds nine seats at every text size") {
// Real sizes: the smallest iPhone still supported, a current one, the smallest the Mac
// window can be dragged to (PokerTutorApp sets minWidth 900, minHeight 640), and the
// size it opens at.
let screens = [CGSize(width: 375, height: 667), CGSize(width: 402, height: 874),
CGSize(width: 430, height: 932),
CGSize(width: 900, height: 640), CGSize(width: 1240, height: 820)]
for scale in TextScale.allCases {
autoreleasepool {
for screen in screens {
let metrics = Metrics(width: screen.width, height: screen.height,
textScale: scale)
let ring = FeltRing(size: metrics.feltArea,
seatWidth: metrics.seatWidth,
seatHeight: metrics.ringSeatHeight,
compact: metrics.compact)
for count in 2...9 {
let points = ring.points(of: count)
t.equal(points.count, count)
// The first point is the player's, whose cards and name sit in a row
// under the felt rather than on the ring — so the boxes to check are
// the opponents'. Neighbours are the tightest pair on an ellipse, but
// check them all: a squat window pinches the top and bottom rather
// than the sides.
for (index, first) in points.enumerated().dropFirst() {
for second in points[(index + 1)...] {
// Seat boxes are rectangles, so two of them clear each other
// when they are a full box apart on EITHER axis — seats stacked
// up the side of the felt need height between them, not width.
let dx = abs(first.x - second.x), dy = abs(first.y - second.y)
t.expect(dx + 0.5 >= ring.seatWidth || dy + 0.5 >= ring.seatHeight,
"\(scale.name), \(count) handed at \(Int(screen.width))×\(Int(screen.height)): "
+ "two seats overlap — \(Int(dx)) apart across and \(Int(dy)) down, "
+ "and a box is \(Int(ring.seatWidth))×\(Int(ring.seatHeight))")
}
// And nothing may sit off the edge of the window it is drawn in.
t.expect(first.x - ring.seatWidth / 2 >= -0.5
&& first.x + ring.seatWidth / 2 <= ring.size.width + 0.5,
"\(scale.name), \(count) handed: a seat runs off the side")
t.expect(first.y - ring.seatHeight / 2 >= -0.5
&& first.y + ring.seatHeight / 2 <= ring.size.height + 0.5,
"\(scale.name), \(count) handed: a seat runs off the top or bottom")
}
}
}
}
}
}
t.test("the table's words grow the whole way, like the rest of the app") {
// The seat boxes and the ring are measured off the same setting the writing is, so a
// capped table would show up here as a box that stopped growing before the type did.
let large = Metrics(width: 1240, height: 820, textScale: .large)
let extra = Metrics(width: 1240, height: 820, textScale: .extraLarge)
t.expect(extra.seatWidth > large.seatWidth,
"a seat box stopped growing at Large: \(extra.seatWidth) vs \(large.seatWidth)")
t.expect(extra.ringSeatHeight > large.ringSeatHeight,
"a seat box stopped growing taller at Large")
t.expect(extra.titleSize > large.titleSize, "a screen title stopped growing at Large")
}
// Ken, 2026-08-02: "I'd like the font for the coach to be even a little bigger."
t.test("the coach is set a step above the app's ordinary writing") {
for base in [11, 12, 13, 14] as [CGFloat] {
t.expect(TextScale.coachBase(base) > base,
"the coach's \(base) came out at \(TextScale.coachBase(base))")
}
// The step is on the base size, so it survives the player's own setting rather than
// washing out at the ends. A paragraph of feedback at Extra large is the case that
// matters: it is the biggest the writing ever gets and the reason the dial exists.
for scale in TextScale.allCases {
t.expect(scale.size(TextScale.coachBase(13)) > scale.size(13),
"at \(scale.name) the coach was no larger than ordinary text")
}
t.expect(TextScale.extraLarge.size(TextScale.coachBase(13)) >= 20,
"the coach's writing tops out at "
+ "\(TextScale.extraLarge.size(TextScale.coachBase(13)))")
}
t.test("sizes land on whole points") {
for scale in TextScale.allCases {
for base in [9, 10, 11, 12, 13, 15, 20, 26] as [CGFloat] {
t.equal(scale.size(base), scale.size(base).rounded())
t.expect(scale.size(base) >= 7, "type became unreadably small at \(base)")
}
}
}
t.test("the choice survives a restart") {
let defaults = scratchDefaults()
defer { wipe(defaults) }
// A fresh install has never been told, and reads as Normal.
t.equal(TextScale.stored(in: defaults), .normal)
for scale in TextScale.allCases {
TextScale.store(scale, in: defaults)
// Reading it back through a second handle on the same storage is what a relaunch
// does: nothing is kept in memory between the two.
t.equal(TextScale.stored(in: scratchDefaults()), scale,
"\(scale.name) was not remembered")
t.equal(TextScale.stored(in: scratchDefaults()).multiplier, scale.multiplier)
}
}
t.test("a value it does not recognise reads as normal") {
let defaults = scratchDefaults()
defer { wipe(defaults) }
defaults.set("enormous", forKey: "PokerTutor.textSize")
t.equal(TextScale.stored(in: defaults), .normal, "an unknown setting must not stop it drawing")
}
}
}
/// Somewhere to write that is not the player's own settings.
private func scratchDefaults() -> UserDefaults {
UserDefaults(suiteName: "PokerTutor.tests.textSize") ?? .standard
}
private func wipe(_ defaults: UserDefaults) {
defaults.removePersistentDomain(forName: "PokerTutor.tests.textSize")
}
/// The deck's colours.
///
/// Ken, 2026-08-22: "hard to see the difference between the black suit." Clubs and spades share a
/// colour in a traditional deck and very nearly share a shape at the size a phone draws them.
///
/// What is tested here is the thing that actually matters — that no two suits come out the same —
/// rather than which colours were picked, which is taste and will change.
public func cardColourTests(_ t: TestRun) {
t.suite("telling the suits apart") { t in
/// Roughly how far apart two colours look. Not a perceptual model — enough to catch two
/// suits that were given the same paint, or near enough to it to fail on a phone in
/// daylight.
func distance(_ a: Palette.Components, _ b: Palette.Components) -> Double {
let dr = a.red - b.red, dg = a.green - b.green, db = a.blue - b.blue
return (dr * dr + dg * dg + db * db).squareRoot()
}
t.test("four colours means four different colours, in both palettes") {
for palette in Palette.allCases {
let colours = Suit.allCases.map {
palette.suitComponents($0, fourColour: true)
}
for i in colours.indices {
for j in colours.indices where j > i {
t.greaterThan(distance(colours[i], colours[j]), 0.25,
"\(palette.name): \(Suit.allCases[i]) and "
+ "\(Suit.allCases[j]) are nearly the same colour")
}
}
}
}
t.test("diamonds and clubs are not both dark and cool") {
// Ken, 2026-08-22: "Diamond color looked too much like a club." Diamonds were blue and
// clubs green — two dark, cool colours being read on a green felt, which is the worst
// backdrop available for separating them. Distance alone did not catch it, so this
// asks the question that did: the two must not sit in the same half of the wheel.
for palette in Palette.allCases {
let diamonds = palette.suitComponents(.diamonds, fourColour: true)
let clubs = palette.suitComponents(.clubs, fourColour: true)
t.greaterThan(distance(diamonds, clubs), 0.45,
"\(palette.name): diamonds and clubs are too close")
// One of them has to be plainly warmer than the other. Red-minus-blue is a crude
// warmth, and crude is enough to catch two cool colours side by side.
let diamondWarmth = diamonds.red - diamonds.blue
let clubWarmth = clubs.red - clubs.blue
t.greaterThan(abs(diamondWarmth - clubWarmth), 0.35,
"\(palette.name): diamonds and clubs are both the same temperature, "
+ "which is how blue and green got shipped")
}
}
t.test("clubs and spades are the pair this exists to separate") {
// The complaint, as an assertion. If a later tidy-up collapses these two back onto one
// colour while four-colour is on, that is the whole setting undone.
for palette in Palette.allCases {
let clubs = palette.suitComponents(.clubs, fourColour: true)
let spades = palette.suitComponents(.spades, fourColour: true)
t.greaterThan(distance(clubs, spades), 0.3,
"\(palette.name): clubs and spades still look alike")
}
}
t.test("the colour-blind palette does not put clubs next to hearts in green and red") {
// Green clubs against red hearts is the standard four-colour deck and the one pairing
// a red-green blind reader cannot use. Whatever the colour-blind scheme picks, those
// two have to be further apart than the pair it replaced.
let clubs = Palette.colourBlind.suitComponents(.clubs, fourColour: true)
let hearts = Palette.colourBlind.suitComponents(.hearts, fourColour: true)
t.greaterThan(distance(clubs, hearts), 0.4,
"the colour-blind deck put clubs and hearts too close together")
// And it must not simply be the classic scheme wearing a different name.
t.expect(clubs != Palette.classic.suitComponents(.clubs, fourColour: true),
"the colour-blind palette is using the classic green for clubs")
}
t.test("the traditional deck is still the traditional deck") {
// Somebody who turns four-colour off has asked for red and black, and should get
// exactly that — not a softened version of the new scheme.
for palette in Palette.allCases {
let hearts = palette.suitComponents(.hearts, fourColour: false)
let diamonds = palette.suitComponents(.diamonds, fourColour: false)
let clubs = palette.suitComponents(.clubs, fourColour: false)
let spades = palette.suitComponents(.spades, fourColour: false)
t.equal(hearts, diamonds, "\(palette.name): the two red suits are not both red")
t.equal(clubs, spades, "\(palette.name): the two black suits are not both black")
}
}
t.test("spades stay black whichever scheme is on") {
// The point is to move the three suits that were sharing two colours, not to repaint
// the deck into something nobody recognises.
for palette in Palette.allCases {
t.equal(palette.suitComponents(.spades, fourColour: true),
palette.suitComponents(.spades, fourColour: false),
"\(palette.name) repainted the spades")
}
}
t.test("the choice survives a restart, and a bad value does not stop the cards drawing") {
let defaults = UserDefaults(suiteName: "cards-\(UUID().uuidString)")!
t.equal(CardColours.stored(in: defaults), .four, "four is the default")
CardColours.store(.two, in: defaults)
t.equal(CardColours.stored(in: defaults), .two)
defaults.set("technicolour", forKey: CardColours.storageKey)
t.equal(CardColours.stored(in: defaults), .four,
"an unrecognised value has to fall back rather than refuse to draw a card")
}
}
}
/// The board in the middle of the felt.
///
/// Ken, 2026-08-22: "can we make the flop cards a little bigger?" Bigger has a ceiling, and it is
/// not taste: the board sits in the middle and nine seats sit on a ring around it, so a board that
/// grows far enough collides with the two at the sides. This is that ceiling, checked rather than
/// eyeballed on one phone.
public func boardSizeTests(_ t: TestRun) {
t.suite("the board in the middle") { t in
/// Every screen the app can actually be run on: the smallest iPhone still supported, two
/// current ones, the smallest the Mac window can be dragged to, and the size it opens at.
let screens = [CGSize(width: 375, height: 667), CGSize(width: 402, height: 874),
CGSize(width: 430, height: 932),
CGSize(width: 900, height: 640), CGSize(width: 1240, height: 820)]
t.test("the board never runs into the seats beside it") {
for scale in TextScale.allCases {
for screen in screens {
let metrics = Metrics(width: screen.width, height: screen.height,
textScale: scale)
t.greaterThan(metrics.boardClearance, 4,
"\(scale.name) at \(Int(screen.width))×\(Int(screen.height)): the "
+ "board is \(Int(metrics.boardWidth)) wide and comes within "
+ "\(Int(metrics.boardClearance)) points of the side seats")
}
}
}
t.test("the board fits across the felt with room around it") {
for scale in TextScale.allCases {
for screen in screens {
let metrics = Metrics(width: screen.width, height: screen.height,
textScale: scale)
t.expect(metrics.boardWidth < metrics.feltArea.width - 20,
"\(scale.name) at \(Int(screen.width))×\(Int(screen.height)): a board "
+ "\(Int(metrics.boardWidth)) wide in a felt "
+ "\(Int(metrics.feltArea.width)) across")
}
}
}
t.test("the board is as large as the smallest phone can hold") {
// The ceiling, written down so the next request for bigger cards gets an answer rather
// than a redraw. On the smallest iPhone still supported the seats close in on the
// middle, and a board much past this one overlaps them — 46 points, which would match
// the player's own cards, already does.
let smallest = Metrics(width: 375, height: 667, textScale: .normal)
t.greaterThan(smallest.boardCard, 38,
"the board went back to being the smallest thing on the felt")
t.lessThan(smallest.boardClearance, 30,
"there are \(Int(smallest.boardClearance)) points of felt going spare "
+ "beside the board, so it could be bigger than \(Int(smallest.boardCard))")
}
t.test("a board card is bigger than an opponent's, which nobody has to read") {
for screen in screens {
let metrics = Metrics(width: screen.width, height: screen.height, textScale: .normal)
t.greaterThan(metrics.boardCard, metrics.opponentCard)
}
}
}
}
/// The by-seat table on My game.
///
/// Ken, 2026-08-23: "on the my game screen, the text scrolls too far over to the right and cant be
/// fully read." Four columns with widths of their own, each multiplied by the text-size setting,
/// added up to more than a phone has — and only once the type was turned up, which is precisely
/// what somebody who finds a table of numbers hard to read does first.
///
/// This is the fourth layout fault in a row that the suite could not see. The difference is that
/// this one is arithmetic rather than rendering, so it does not have to stay invisible.
public func statTableTests(_ t: TestRun) {
t.suite("the by-seat table fits the screen") { t in
let screens = [CGSize(width: 375, height: 667), CGSize(width: 402, height: 874),
CGSize(width: 430, height: 932),
CGSize(width: 900, height: 640), CGSize(width: 1240, height: 820)]
t.test("there is room for a seat's name beside its numbers, at every text size") {
// "Seat 2 of 9" over "Big blind" is the widest thing the seat column has to hold. At
// Extra large that line is set at about 17 points, so it wants a little over 100 to
// say itself without breaking up — 90 was the first number tried here and it was not
// enough for the type size the person reading it had chosen.
for scale in TextScale.allCases {
for screen in screens {
let metrics = Metrics(width: screen.width, height: screen.height,
textScale: scale)
t.greaterThan(metrics.statNameWidth, 100,
"\(scale.name) at \(Int(screen.width)) wide: the numbers take "
+ "\(Int(metrics.statFixedColumns)) of "
+ "\(Int(metrics.panelContentWidth)), leaving "
+ "\(Int(metrics.statNameWidth)) for the seat")
}
}
}
t.test("the numbers stop growing before they eat the row") {
// The columns are capped at Large for the same reason the seat boxes on the felt are:
// past that they take the space the words need. Extra large must not cost the seat
// column anything the setting below it did not already.
for screen in screens {
let large = Metrics(width: screen.width, height: screen.height, textScale: .large)
let extra = Metrics(width: screen.width, height: screen.height,
textScale: .extraLarge)
t.equal(extra.statFixedColumns, large.statFixedColumns,
"at \(Int(screen.width)) wide the number columns kept growing past Large")
}
}
t.test("bigger type never leaves less room than smaller type did") {
// The failure this replaces was monotonic and quiet: every step up took a little more
// until the row ran off the side. If a future column ever does that again, it fails
// here first.
for screen in screens {
let widths = TextScale.allCases.map {
Metrics(width: screen.width, height: screen.height, textScale: $0).statNameWidth
}
for (bigger, smaller) in zip(widths.dropFirst(), widths) {
t.expect(bigger <= smaller + 0.5,
"at \(Int(screen.width)) wide the seat column grew with the type, "
+ "which means something else shrank")
}
t.greaterThan(widths.last ?? 0, 100,
"at \(Int(screen.width)) wide the largest type leaves "
+ "\(Int(widths.last ?? 0)) for the seat name")
}
}
}
}
public func twoCardTests(_ t: TestRun) {
t.suite("two cards in a hand") { t in
let screens = [CGSize(width: 375, height: 667), CGSize(width: 402, height: 874),
CGSize(width: 430, height: 932),
CGSize(width: 900, height: 640), CGSize(width: 1240, height: 820)]
func everywhere(_ body: (Metrics, String) -> Void) {
for scale in TextScale.allCases {
for screen in screens {
let metrics = Metrics(width: screen.width, height: screen.height, textScale: scale)
body(metrics, "\(scale.name) at \(Int(screen.width))×\(Int(screen.height))")
}
}
}
t.test("the player is dealt two cards, and the row is measured for two") {
let metrics = Metrics(width: 375, height: 667, textScale: .normal)
t.equal(metrics.heroCardCount, 2)
t.equal(metrics.heroCardFrames.count, 2)
}
t.test("the player's two cards sit side by side and never overlap") {
// Ken, 2026-09-10: "I like the way the cards are presented in the PLO app. Can we have
// holdem match it?" The PLO row was built after a fanned hand proved unreadable on a
// phone; here every card is whole, at every size, and this is the check that says so.
everywhere { metrics, place in
let frames = metrics.heroCardFrames
t.equal(frames.count, metrics.heroCardCount, "\(place): not every card is drawn")
for (first, second) in zip(frames, frames.dropFirst()) {
t.expect(second.minX - first.maxX >= Metrics.cardGap - 0.5,
"\(place): two of the player's cards overlap — one ends at "
+ "\(Int(first.maxX)) and the next starts at \(Int(second.minX))")
}
t.close(Double(metrics.heroHandWidth), Double(frames.last!.maxX), 0.5,
"\(place): the hand's width is not the width of its cards")
}
}
t.test("the player's cards are big enough to read on the smallest phone") {
for scale in TextScale.allCases {
let phone = Metrics(width: 375, height: 667, textScale: scale)
t.expect(phone.heroCard >= 36, "\(scale.name): the player's cards are \(Int(phone.heroCard)) tall")
}
// The cards were 46 on a phone in the seat box; moving them to a row of their own is
// no reason to make them smaller.
t.greaterThan(Metrics(width: 375, height: 667, textScale: .normal).heroCard, 45,
"the player's cards on a phone are smaller than they were")
}
t.test("the player's row fits across the screen") {
everywhere { metrics, place in
t.expect(metrics.heroRowWidth <= metrics.width - 8 + 0.5,
"\(place): the player's row is \(Int(metrics.heroRowWidth)) wide on a "
+ "screen \(Int(metrics.width)) across")
}
}
t.test("the player's row sits clear of the ring of seats and of the board") {
everywhere { metrics, place in
let ring = FeltRing(size: metrics.feltArea, seatWidth: metrics.seatWidth,
seatHeight: metrics.ringSeatHeight, compact: metrics.compact)
for count in 2...9 {
t.expect(ring.holds(count),
"\(place), \(count) handed: a seat runs into a neighbour or off the felt")
t.expect(metrics.lowestSeatBottom(of: count) <= metrics.feltArea.height + 0.5,
"\(place), \(count) handed: the lowest seat reaches "
+ "\(Int(metrics.lowestSeatBottom(of: count))) and the player's row "
+ "starts at \(Int(metrics.feltArea.height))")
}
t.expect(metrics.boardBottom < metrics.feltArea.height,
"\(place): the board and the pot reach \(Int(metrics.boardBottom)) and the "
+ "player's row starts at \(Int(metrics.feltArea.height))")
t.expect(metrics.heroRowHeight >= metrics.heroCard + 8,
"\(place): the player's row is shorter than the cards in it")
}
}
t.test("an opponent's two cards fit across their seat, backs or faces") {
everywhere { metrics, place in
let backs = metrics.cardRowWidth(count: metrics.heroCardCount, height: metrics.opponentCard)
let faces = metrics.cardRowWidth(count: metrics.heroCardCount, height: metrics.opponentRevealCard)
t.expect(max(backs, faces) <= metrics.seatWidth + 0.5,
"\(place): an opponent's cards are \(Int(max(backs, faces))) wide in a seat "
+ "\(Int(metrics.seatWidth)) wide")
}
}
}
}
// MARK: - The button and the blinds
public func tableMarkerTests(_ t: TestRun) {
t.suite("the button and the blinds on the felt") { t in
let screens = [CGSize(width: 375, height: 667), CGSize(width: 402, height: 874),
CGSize(width: 430, height: 932),
CGSize(width: 900, height: 640), CGSize(width: 1240, height: 820)]
func everywhere(_ body: (Metrics, String) -> Void) {
for scale in TextScale.allCases {
for screen in screens {
let metrics = Metrics(width: screen.width, height: screen.height, textScale: scale)
body(metrics, "\(scale.name) at \(Int(screen.width))×\(Int(screen.height))")
}
}
}
/// At least this much felt between a marker and any box or the board, so it reads as a
/// thing on the table rather than part of a seat. Read off `Metrics` so the layout and
/// the test cannot disagree about it; the number itself is pinned below.
let clearance = Metrics(width: 375, height: 667, textScale: .normal).markerClearance
func inside(_ rect: CGRect, _ area: CGSize) -> Bool {
rect.minX >= -0.5 && rect.minY >= -0.5
&& rect.maxX <= area.width + 0.5 && rect.maxY <= area.height + 0.5
}
func gap(_ a: CGRect, _ b: CGRect) -> CGFloat {
let dx = max(0, max(b.minX - a.maxX, a.minX - b.maxX))
let dy = max(0, max(b.minY - a.maxY, a.minY - b.maxY))
return (dx * dx + dy * dy).squareRoot()
}
t.test("every seat's marker sits on the felt, off its seat box, and off the board") {
// Ken, 2026-09-11: "It is hard to see who has the button at any given time."
everywhere { metrics, place in
for count in 2...TableSession.seatsAtTable {
for seat in 1..<count {
let marks = count == 2 ? 2 : 1
let marker = metrics.markerFrame(seatsFromHero: seat, of: count, marks: marks)
let box = metrics.seatBox(seatsFromHero: seat, of: count)
t.expect(inside(marker, metrics.feltArea),
"\(place), \(count) handed, seat \(seat): the marker is off the felt")
t.expect(gap(marker, box) >= clearance - 0.5,
"\(place), \(count) handed, seat \(seat): the marker is "
+ "\(Int(gap(marker, box))) points off its seat box")
t.expect(gap(marker, metrics.boardBlock) >= clearance - 0.5,
"\(place), \(count) handed, seat \(seat): the marker is over the board")
for other in 1..<count where other != seat {
let theirs = metrics.seatBox(seatsFromHero: other, of: count)
t.expect(gap(marker, theirs) >= clearance - 0.5,
"\(place), \(count) handed, seat \(seat): the marker is "
+ "\(Int(gap(marker, theirs))) points off seat \(other)'s box")
}
}
}
}
}
t.test("the player's own marker sits at the empty bottom of the ring, clear of the corner seats") {
everywhere { metrics, place in
for count in 2...TableSession.seatsAtTable {
let marker = metrics.markerFrame(seatsFromHero: 0, of: count, marks: count == 2 ? 2 : 1)
t.expect(inside(marker, metrics.feltArea),
"\(place), \(count) handed: the player's marker is off the felt")
t.expect(gap(marker, metrics.boardBlock) >= clearance - 0.5,
"\(place), \(count) handed: the player's marker is over the board")
for seat in 1..<count {
let box = metrics.seatBox(seatsFromHero: seat, of: count)
t.expect(gap(marker, box) >= clearance - 0.5,
"\(place), \(count) handed: the player's marker is "
+ "\(Int(gap(marker, box))) points off seat \(seat)'s box")
}
}
}
}
t.test("the discs are the size of a chip, not a card") {
t.expect(clearance >= 4, "markers may sit \(clearance) points off a seat, which is touching")
everywhere { metrics, place in
t.lessThan(metrics.dealerDisc, metrics.seatWidth * 0.35,
"\(place): a \(Int(metrics.dealerDisc))-point button on a \(Int(metrics.seatWidth))-point seat")
t.lessThan(metrics.blindMarker, metrics.dealerDisc,
"\(place): the blind chips are not smaller than the button")
t.greaterThan(metrics.blindMarker, 12, "\(place): the blind chips are too small to read")
}
}
t.test("which seats carry which marker") {
t.equal(TableMark.marks(for: .button, headsUp: false), [.dealer])
t.equal(TableMark.marks(for: .smallBlind, headsUp: false), [.smallBlind])
t.equal(TableMark.marks(for: .bigBlind, headsUp: false), [.bigBlind])
t.equal(TableMark.marks(for: .cutoff, headsUp: false), [])
// Heads-up the button posts the small blind and the engine names that seat the button.
t.equal(TableMark.marks(for: .button, headsUp: true), [.dealer, .smallBlind])
t.equal(TableMark.marks(for: .bigBlind, headsUp: true), [.bigBlind])
}
}
}