Merge remote-tracking branch 'origin/dev' into claude/locked-phone-turn-issue-x9pvbv
This commit is contained in:
@@ -250,6 +250,39 @@ class KaiserRulesTest {
|
||||
assertEquals("all four held for display", 4, state.completedTrick.size)
|
||||
}
|
||||
|
||||
/**
|
||||
* Kaiser scores in points, not tricks, so nothing in the rules reads this
|
||||
* count — the table does, to draw the cards each player has taken. That is
|
||||
* exactly why it is worth a test: a field no rule depends on is a field
|
||||
* that can quietly stop being maintained.
|
||||
*/
|
||||
@Test
|
||||
fun `tricks are counted to the seat that took them`() {
|
||||
// Everything is clubs and nothing is trump, so each trick simply goes
|
||||
// to the highest card in it: the ace, then the king.
|
||||
val hands = listOf(
|
||||
listOf(Card(Rank.ACE, Suit.CLUBS), Card(Rank.SEVEN, Suit.CLUBS)),
|
||||
listOf(Card(Rank.EIGHT, Suit.CLUBS), Card(Rank.NINE, Suit.CLUBS)),
|
||||
listOf(Card(Rank.TEN, Suit.CLUBS), Card(Rank.JACK, Suit.CLUBS)),
|
||||
listOf(Card(Rank.QUEEN, Suit.CLUBS), Card(Rank.KING, Suit.CLUBS)),
|
||||
)
|
||||
var state = playingState(hands, trump = Suit.SPADES)
|
||||
assertEquals("nothing taken at the deal", listOf(0, 0, 0, 0), state.tricksWon)
|
||||
|
||||
repeat(4) {
|
||||
val seat = KaiserRules.currentSeat(state)!!
|
||||
state = KaiserRules.applyMove(state, seat, PlayCard(state.hands[seat].first()))
|
||||
}
|
||||
assertEquals("the ace takes the first", listOf(1, 0, 0, 0), state.tricksWon)
|
||||
|
||||
repeat(4) {
|
||||
val seat = KaiserRules.currentSeat(state)!!
|
||||
state = KaiserRules.applyMove(state, seat, PlayCard(state.hands[seat].first()))
|
||||
}
|
||||
assertEquals("the king takes the second", listOf(1, 0, 0, 1), state.tricksWon)
|
||||
assertEquals("and they add up", 2, state.tricksWon.sum())
|
||||
}
|
||||
|
||||
// -- Scoring ------------------------------------------------------------
|
||||
|
||||
@Test
|
||||
|
||||
@@ -0,0 +1,247 @@
|
||||
package org.prolibertate.games.ui.game
|
||||
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertNotEquals
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
import kotlin.math.abs
|
||||
import org.prolibertate.games.game.cards.Card
|
||||
import org.prolibertate.games.game.cards.Decks
|
||||
import org.prolibertate.games.game.cards.Rank
|
||||
import org.prolibertate.games.game.cards.Suit
|
||||
|
||||
/**
|
||||
* The scatter under the card animations.
|
||||
*
|
||||
* Two properties matter and neither is visible in a screenshot. It has to be
|
||||
* *bounded*, because the whole difference between cards on a table and cards
|
||||
* thrown down a flight of stairs is the size of the numbers; and it has to be
|
||||
* *stable*, because a card that picks a new angle on every recomposition
|
||||
* twitches, and a card whose angle depends on where it sits in a list jumps
|
||||
* whenever a card is played next to it.
|
||||
*/
|
||||
class CardPhysicsTest {
|
||||
|
||||
private val deck = Decks.standard52()
|
||||
|
||||
@Test
|
||||
fun `noise stays inside minus one to one`() {
|
||||
for (seed in -500..500) {
|
||||
for (salt in 0..8) {
|
||||
val n = unitNoise(seed, salt)
|
||||
assertTrue("$seed/$salt gave $n", n >= -1f && n < 1f)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `noise is the same every time it is asked`() {
|
||||
assertEquals(unitNoise(12345, 3), unitNoise(12345, 3), 0f)
|
||||
assertEquals(unitNoise(-7, 1), unitNoise(-7, 1), 0f)
|
||||
}
|
||||
|
||||
/**
|
||||
* The salt is what lets one seed produce a tilt, two drifts, a spin and a
|
||||
* queue position that are not all the same number. If it stopped separating
|
||||
* them, every card would drift exactly as far as it tilted.
|
||||
*/
|
||||
@Test
|
||||
fun `salts do not agree with each other`() {
|
||||
val fromOneSeed = (0..8).map { unitNoise(4242, it) }
|
||||
assertEquals(fromOneSeed.size, fromOneSeed.toSet().size)
|
||||
}
|
||||
|
||||
/** Sequential seeds are the normal case: adjacent cards in a hand. */
|
||||
@Test
|
||||
fun `neighbouring seeds do not give neighbouring values`() {
|
||||
val runs = (0 until 200).count { abs(unitNoise(it, 1) - unitNoise(it + 1, 1)) < 0.05f }
|
||||
assertTrue("$runs of 200 neighbouring seeds landed together", runs < 30)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `every card in a deck rests within the stated bounds`() {
|
||||
deck.forEach { card ->
|
||||
val rest = cardRest(cardSeed(card))
|
||||
assertTrue(abs(rest.tiltDegrees) <= REST_TILT_DEGREES)
|
||||
assertTrue(abs(rest.driftX) <= REST_DRIFT_FRACTION)
|
||||
assertTrue(abs(rest.driftY) <= REST_DRIFT_FRACTION)
|
||||
assertTrue(abs(rest.spinDegrees) <= THROW_SPIN_DEGREES)
|
||||
assertTrue(rest.lateness in 0f..1f)
|
||||
assertTrue(abs(handTilt(cardSeed(card))) <= HAND_TILT_DEGREES)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `a card lies the same way however often it is asked`() {
|
||||
val card = Card(Rank.QUEEN, Suit.SPADES)
|
||||
assertEquals(cardRest(cardSeed(card)), cardRest(cardSeed(card)))
|
||||
assertEquals(cardRest(cardSeed(card, 3)), cardRest(cardSeed(card, 3)))
|
||||
}
|
||||
|
||||
/**
|
||||
* Two cards lying at the same angle in the same place is the one thing that
|
||||
* makes a scattered pile look printed rather than dealt.
|
||||
*/
|
||||
@Test
|
||||
fun `no two cards in a deck share a seed`() {
|
||||
val seeds = deck.map { cardSeed(it) }
|
||||
assertEquals(seeds.size, seeds.toSet().size)
|
||||
}
|
||||
|
||||
/**
|
||||
* A pile can hold the same card twice — Golf and Spider deal more than one
|
||||
* deck — so where a card is lying has to be part of its seed.
|
||||
*/
|
||||
@Test
|
||||
fun `the same card in two places lies two ways`() {
|
||||
val card = Card(Rank.SEVEN, Suit.HEARTS)
|
||||
assertNotEquals(cardRest(cardSeed(card, 0)), cardRest(cardSeed(card, 1)))
|
||||
}
|
||||
|
||||
/**
|
||||
* A heap is untidier than a card laid in front of a player, and has to be:
|
||||
* at the tight bounds five cards on a discard pile are hidden to within a
|
||||
* twentieth of a card width of each other and the pile looks like one card.
|
||||
*/
|
||||
@Test
|
||||
fun `a heap lies looser than a table but still within bounds`() {
|
||||
deck.forEach { card ->
|
||||
val seed = cardSeed(card)
|
||||
val heap = heapRest(seed)
|
||||
val table = cardRest(seed)
|
||||
assertTrue(abs(heap.tiltDegrees) <= HEAP_TILT_DEGREES)
|
||||
assertTrue(abs(heap.driftX) <= HEAP_DRIFT_FRACTION)
|
||||
assertTrue(abs(heap.driftY) <= HEAP_DRIFT_FRACTION)
|
||||
assertTrue(abs(heap.tiltDegrees) >= abs(table.tiltDegrees))
|
||||
assertTrue(abs(heap.driftX) >= abs(table.driftX))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The two have to agree about everything except how far the card strayed,
|
||||
* or a card would jump when it was drawn one way rather than the other.
|
||||
*/
|
||||
@Test
|
||||
fun `a heap and a table agree about the throw`() {
|
||||
deck.take(6).forEach { card ->
|
||||
val seed = cardSeed(card)
|
||||
assertEquals(cardRest(seed).spinDegrees, heapRest(seed).spinDegrees, 0f)
|
||||
assertEquals(cardRest(seed).lateness, heapRest(seed).lateness, 0f)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A buried card has to end up near the rim of whatever is covering it. One
|
||||
* left in the middle is one nobody can see, and then drawing the pile as a
|
||||
* pile achieves nothing.
|
||||
*/
|
||||
@Test
|
||||
fun `a buried card ends up out where a corner of it shows`() {
|
||||
deck.forEach { card ->
|
||||
val (x, y) = buriedOffset(cardSeed(card), halfWidth = 100f, halfHeight = 40f)
|
||||
val howFarOut = kotlin.math.hypot(x / 100f, y / 40f)
|
||||
assertTrue("$card sat at $howFarOut of the way out", howFarOut >= 0.719f)
|
||||
assertTrue(howFarOut <= 1.001f)
|
||||
}
|
||||
}
|
||||
|
||||
/** Two cards buried under the same set must not end up in the same place. */
|
||||
@Test
|
||||
fun `buried cards do not stack on each other`() {
|
||||
val places = (0 until 6).map { buriedOffset(cardSeed(deck[it], it), 100f, 40f) }
|
||||
assertEquals(places.size, places.toSet().size)
|
||||
}
|
||||
|
||||
/** Every seat's card arrives from that seat, and from a card-width or two out. */
|
||||
@Test
|
||||
fun `a card comes in from its own player's side`() {
|
||||
assertEquals(0f to THROW_DISTANCE, seatOrigin(0))
|
||||
assertEquals(-THROW_DISTANCE to 0f, seatOrigin(1))
|
||||
assertEquals(0f to -THROW_DISTANCE, seatOrigin(2))
|
||||
assertEquals(THROW_DISTANCE to 0f, seatOrigin(3))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `a pile card comes in from somewhere off the pile`() {
|
||||
deck.forEach { card ->
|
||||
val (x, y) = pileOrigin(cardSeed(card))
|
||||
val distance = kotlin.math.hypot(x, y)
|
||||
assertEquals(THROW_DISTANCE, distance, 0.001f)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `nothing has left the table before the sweep starts`() {
|
||||
deck.take(8).forEach { card ->
|
||||
assertEquals(0f, sweepProgress(0f, cardRest(cardSeed(card))), 0f)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `everything has left the table by the end of the sweep`() {
|
||||
deck.take(8).forEach { card ->
|
||||
assertEquals(1f, sweepProgress(1f, cardRest(cardSeed(card))), 0.0001f)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `a card only ever gets further from the table`() {
|
||||
val rest = cardRest(cardSeed(Card(Rank.ACE, Suit.CLUBS)))
|
||||
var previous = 0f
|
||||
for (step in 0..100) {
|
||||
val now = sweepProgress(step / 100f, rest)
|
||||
assertTrue("went backwards at $step", now >= previous - 0.0001f)
|
||||
assertTrue(now in 0f..1f)
|
||||
previous = now
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The stagger: a trick gathers up as one motion, so the cards must not all
|
||||
* be at the same point in it, and none of them may still be sitting there
|
||||
* when the sweep is nearly over.
|
||||
*/
|
||||
@Test
|
||||
fun `cards leave at different moments but none is left behind`() {
|
||||
val hand = deck.take(4).map { cardRest(cardSeed(it)) }
|
||||
val halfway = hand.map { sweepProgress(0.5f, it) }
|
||||
assertEquals(halfway.size, halfway.toSet().size)
|
||||
hand.forEach { assertTrue(sweepProgress(0.95f, it) > 0.5f) }
|
||||
}
|
||||
|
||||
/**
|
||||
* A gathered card has to still be whole when it reaches the winner's pile,
|
||||
* because it vanishes at the same moment the pile grows by one and that
|
||||
* swap is only invisible if there was something there to swap.
|
||||
*/
|
||||
@Test
|
||||
fun `a gathered card is whole most of the way and gone at the end`() {
|
||||
assertEquals(1f, gatherAlpha(0f), 0f)
|
||||
assertEquals(1f, gatherAlpha(0.5f), 0f)
|
||||
assertEquals(0f, gatherAlpha(1f), 0f)
|
||||
var previous = 1f
|
||||
for (step in 0..100) {
|
||||
val now = gatherAlpha(step / 100f)
|
||||
assertTrue("went back up at $step", now <= previous + 0.0001f)
|
||||
assertTrue(now in 0f..1f)
|
||||
previous = now
|
||||
}
|
||||
}
|
||||
|
||||
/** And is squared down into the bundle rather than arriving full size. */
|
||||
@Test
|
||||
fun `a gathered card is squared down on the way`() {
|
||||
assertEquals(1f, gatherScale(0f), 0f)
|
||||
assertTrue(gatherScale(1f) < 1f)
|
||||
assertTrue(gatherScale(1f) > 0.5f)
|
||||
assertTrue(gatherScale(0.5f) < gatherScale(0.25f))
|
||||
}
|
||||
|
||||
/** A trick that never completes must not leave a card part way off the table. */
|
||||
@Test
|
||||
fun `a sweep that never starts moves nothing`() {
|
||||
deck.take(4).forEach { card ->
|
||||
assertEquals(0f, sweepProgress(0f, cardRest(cardSeed(card)), spread = 0f), 0f)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user