1use crate::meld::Melds;
27use crate::{Card, Hand, Meld, Player, Rules, deadwood};
28use core::fmt;
29use thiserror::Error;
30
31#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
35#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
36pub enum Phase {
37 Upcard,
39 Draw,
41 Discard,
43 Layoff,
45 Finished,
47}
48
49impl fmt::Display for Phase {
50 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
51 f.write_str(match self {
52 Self::Upcard => "upcard",
53 Self::Draw => "draw",
54 Self::Discard => "discard",
55 Self::Layoff => "layoff",
56 Self::Finished => "finished",
57 })
58 }
59}
60
61#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
67#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
68#[non_exhaustive]
69pub enum RoundResult {
70 Dead,
73 Knock {
75 winner: Player,
77 margin: u8,
79 },
80 Undercut {
82 winner: Player,
84 margin: u8,
86 },
87 Gin {
89 winner: Player,
91 deadwood: u8,
93 },
94 BigGin {
96 winner: Player,
98 deadwood: u8,
100 },
101}
102
103impl RoundResult {
104 #[must_use]
106 pub const fn winner(self) -> Option<Player> {
107 match self {
108 Self::Dead => None,
109 Self::Knock { winner, .. }
110 | Self::Undercut { winner, .. }
111 | Self::Gin { winner, .. }
112 | Self::BigGin { winner, .. } => Some(winner),
113 }
114 }
115
116 #[must_use]
121 pub const fn points(self, rules: &Rules) -> u16 {
122 match self {
123 Self::Dead => 0,
124 Self::Knock { margin, .. } => margin as u16,
125 Self::Undercut { margin, .. } => margin as u16 + rules.undercut_bonus,
126 Self::Gin { deadwood, .. } => deadwood as u16 + rules.gin_bonus,
127 Self::BigGin { deadwood, .. } => {
129 deadwood as u16
130 + match rules.big_gin_bonus {
131 Some(bonus) => bonus,
132 None => rules.gin_bonus,
133 }
134 }
135 }
136 }
137}
138
139#[derive(Debug, Error, Clone, Copy, PartialEq, Eq, Hash)]
141#[non_exhaustive]
142pub enum DealError {
143 #[error("Each player is dealt exactly 10 cards")]
145 WrongHandSize,
146
147 #[error("The stock holds the remaining 31 cards")]
149 WrongStockSize,
150
151 #[error("A card appears twice across the hands, upcard, and stock")]
153 RepeatedCard,
154}
155
156#[derive(Debug, Error, Clone, Copy, PartialEq, Eq, Hash)]
158#[non_exhaustive]
159pub enum RoundError {
160 #[error("This action is not available in the {0} phase")]
162 WrongPhase(Phase),
163
164 #[error("After both players pass the upcard, the first draw must come from the stock")]
167 MustDrawFromStock,
168
169 #[error("{0} is not in the hand")]
171 NotInHand(Card),
172
173 #[error("{0} was taken from the discard pile this turn and cannot be discarded")]
175 DiscardJustTaken(Card),
176
177 #[error("Deadwood {deadwood} exceeds the knock limit {limit}")]
179 TooMuchDeadwood {
180 deadwood: u8,
182 limit: u8,
184 },
185
186 #[error("The melds do not arrange the knocker's remaining hand")]
188 MeldsMismatch,
189
190 #[error("The hand is not fully melded")]
192 NotBigGin,
193
194 #[error("Big gin is disabled by the rules")]
196 BigGinDisabled,
197
198 #[error("No meld at index {0} in the spread")]
200 NoSuchMeld(usize),
201
202 #[error("{0} does not fit the meld")]
204 CannotLayOff(Card),
205}
206
207#[derive(Debug, Clone, Copy, PartialEq, Eq)]
208struct KnockState {
209 knocker: Player,
210 spread: [Option<Meld>; 3],
211 knocker_deadwood: u8,
212 laid_off: Hand,
213}
214
215#[derive(Debug, Clone, PartialEq, Eq)]
224#[cfg_attr(
225 feature = "serde",
226 derive(serde::Serialize, serde::Deserialize),
227 serde(into = "repr::RoundRepr", try_from = "repr::RoundRepr")
228)]
229pub struct Round {
230 rules: Rules,
231 dealer: Player,
232 hands: [Hand; 2],
233 stock: Vec<Card>,
235 discards: Vec<Card>,
237 initial_upcard: Card,
239 phase: Phase,
240 turn: Player,
241 passes: u8,
243 forced_stock: bool,
245 taken_discard: Option<Card>,
247 knock: Option<KnockState>,
248 result: Option<RoundResult>,
249}
250
251impl Round {
252 pub fn from_deal(
262 rules: Rules,
263 dealer: Player,
264 hands: [Hand; 2],
265 upcard: Card,
266 stock: Vec<Card>,
267 ) -> Result<Self, DealError> {
268 if hands[0].len() != 10 || hands[1].len() != 10 {
269 return Err(DealError::WrongHandSize);
270 }
271 if stock.len() != 31 {
272 return Err(DealError::WrongStockSize);
273 }
274
275 let mut seen = hands[0];
276 if !(seen & hands[1]).is_empty() {
277 return Err(DealError::RepeatedCard);
278 }
279 seen |= hands[1];
280 if !seen.insert(upcard) {
281 return Err(DealError::RepeatedCard);
282 }
283 if stock.iter().any(|&card| !seen.insert(card)) {
284 return Err(DealError::RepeatedCard);
285 }
286
287 Ok(Self {
288 rules,
289 dealer,
290 hands,
291 stock,
292 discards: vec![upcard],
293 initial_upcard: upcard,
294 phase: Phase::Upcard,
295 turn: dealer.opponent(),
296 passes: 0,
297 forced_stock: false,
298 taken_discard: None,
299 knock: None,
300 result: None,
301 })
302 }
303
304 #[allow(clippy::missing_panics_doc)]
308 #[cfg(feature = "rand")]
309 #[must_use]
310 pub fn deal(rules: Rules, dealer: Player, rng: &mut (impl rand::Rng + ?Sized)) -> Self {
311 let mut deck = crate::deck::Deck::ALL;
312 let hands = [deck.draw(rng, 10), deck.draw(rng, 10)];
313 let upcard = deck.pop(rng).expect("32 cards remain after the hands");
314
315 let mut stock = Vec::with_capacity(31);
316 while let Some(card) = deck.pop(rng) {
317 stock.push(card);
318 }
319
320 Self::from_deal(rules, dealer, hands, upcard, stock)
321 .expect("a fresh deck deals disjoint cards")
322 }
323
324 #[must_use]
326 #[inline]
327 pub const fn rules(&self) -> &Rules {
328 &self.rules
329 }
330
331 #[must_use]
333 #[inline]
334 pub const fn dealer(&self) -> Player {
335 self.dealer
336 }
337
338 #[must_use]
340 #[inline]
341 pub const fn non_dealer(&self) -> Player {
342 self.dealer.opponent()
343 }
344
345 #[must_use]
347 #[inline]
348 pub const fn phase(&self) -> Phase {
349 self.phase
350 }
351
352 #[must_use]
356 pub const fn turn(&self) -> Option<Player> {
357 match self.phase {
358 Phase::Finished => None,
359 _ => Some(self.turn),
360 }
361 }
362
363 #[must_use]
368 #[inline]
369 pub const fn hand(&self, player: Player) -> Hand {
370 self.hands[player as usize]
371 }
372
373 #[must_use]
375 #[inline]
376 pub fn discard_pile(&self) -> &[Card] {
377 &self.discards
378 }
379
380 #[must_use]
385 #[inline]
386 pub fn stock(&self) -> &[Card] {
387 &self.stock
388 }
389
390 #[must_use]
396 #[inline]
397 pub const fn knock_limit(&self) -> u8 {
398 self.rules.knock_limit_for(self.initial_upcard)
399 }
400
401 #[must_use]
406 #[inline]
407 pub const fn initial_upcard(&self) -> Card {
408 self.initial_upcard
409 }
410
411 #[must_use]
413 pub const fn knocker(&self) -> Option<Player> {
414 match &self.knock {
415 Some(state) => Some(state.knocker),
416 None => None,
417 }
418 }
419
420 pub fn spread(&self) -> impl Iterator<Item = Meld> + '_ {
424 self.knock
425 .iter()
426 .flat_map(|state| state.spread.into_iter().flatten())
427 }
428
429 #[must_use]
431 pub fn laid_off(&self) -> Hand {
432 self.knock.map_or(Hand::EMPTY, |state| state.laid_off)
433 }
434
435 #[must_use]
437 #[inline]
438 pub const fn result(&self) -> Option<RoundResult> {
439 self.result
440 }
441
442 const fn expect_phase(&self, phase: Phase) -> Result<(), RoundError> {
443 if self.phase as u8 == phase as u8 {
444 Ok(())
445 } else {
446 Err(RoundError::WrongPhase(self.phase))
447 }
448 }
449
450 pub fn pass(&mut self) -> Result<(), RoundError> {
459 self.expect_phase(Phase::Upcard)?;
460 self.passes += 1;
461 if self.passes == 2 {
462 self.turn = self.non_dealer();
463 self.phase = Phase::Draw;
464 self.forced_stock = true;
465 } else {
466 self.turn = self.dealer;
467 }
468 Ok(())
469 }
470
471 #[allow(clippy::missing_panics_doc)]
482 pub fn take_discard(&mut self) -> Result<Card, RoundError> {
483 match self.phase {
484 Phase::Draw if self.forced_stock => return Err(RoundError::MustDrawFromStock),
485 Phase::Upcard | Phase::Draw => {}
486 phase => return Err(RoundError::WrongPhase(phase)),
487 }
488
489 let card = self
490 .discards
491 .pop()
492 .expect("the discard pile is never empty when it may be drawn from");
493 self.hands[self.turn as usize].insert(card);
494 self.taken_discard = Some(card);
495 self.phase = Phase::Discard;
496 Ok(card)
497 }
498
499 #[allow(clippy::missing_panics_doc)]
507 pub fn draw_stock(&mut self) -> Result<Card, RoundError> {
508 self.expect_phase(Phase::Draw)?;
509 let card = self
510 .stock
511 .pop()
512 .expect("the dead-hand rule keeps the stock non-empty");
513 self.hands[self.turn as usize].insert(card);
514 self.forced_stock = false;
515 self.phase = Phase::Discard;
516 Ok(card)
517 }
518
519 fn expect_sheddable(&self, card: Card) -> Result<(), RoundError> {
521 if !self.hands[self.turn as usize].contains(card) {
522 return Err(RoundError::NotInHand(card));
523 }
524 if self.taken_discard == Some(card) {
525 return Err(RoundError::DiscardJustTaken(card));
526 }
527 Ok(())
528 }
529
530 fn shed(&mut self, card: Card) {
532 self.hands[self.turn as usize].remove(card);
533 self.discards.push(card);
534 self.taken_discard = None;
535 }
536
537 pub fn discard(&mut self, card: Card) -> Result<(), RoundError> {
548 self.expect_phase(Phase::Discard)?;
549 self.expect_sheddable(card)?;
550 self.shed(card);
551
552 if self.stock.len() == 2 {
553 self.result = Some(RoundResult::Dead);
554 self.phase = Phase::Finished;
555 } else {
556 self.turn = self.turn.opponent();
557 self.phase = Phase::Draw;
558 }
559 Ok(())
560 }
561
562 pub fn knock(&mut self, card: Card, melds: Melds) -> Result<(), RoundError> {
589 self.expect_phase(Phase::Discard)?;
590 self.expect_sheddable(card)?;
591 if melds.hand() != self.hands[self.turn as usize] - card.into() {
592 return Err(RoundError::MeldsMismatch);
593 }
594
595 let knocker_deadwood = melds.deadwood();
596 if knocker_deadwood > self.knock_limit() {
597 return Err(RoundError::TooMuchDeadwood {
598 deadwood: knocker_deadwood,
599 limit: self.knock_limit(),
600 });
601 }
602
603 self.shed(card);
604 let knocker = self.turn;
605 self.knock = Some(KnockState {
606 knocker,
607 spread: melds.into_array(),
608 knocker_deadwood,
609 laid_off: Hand::EMPTY,
610 });
611
612 if knocker_deadwood == 0 {
613 let loser = deadwood(self.hands[knocker.opponent() as usize]);
614 self.result = Some(RoundResult::Gin {
615 winner: knocker,
616 deadwood: loser,
617 });
618 self.phase = Phase::Finished;
619 } else {
620 self.turn = knocker.opponent();
621 self.phase = Phase::Layoff;
622 }
623 Ok(())
624 }
625
626 pub fn declare_big_gin(&mut self, melds: Melds) -> Result<(), RoundError> {
640 self.expect_phase(Phase::Discard)?;
641 if self.rules.big_gin_bonus.is_none() {
642 return Err(RoundError::BigGinDisabled);
643 }
644 if melds.hand() != self.hands[self.turn as usize] {
645 return Err(RoundError::MeldsMismatch);
646 }
647 if melds.deadwood() != 0 {
648 return Err(RoundError::NotBigGin);
649 }
650
651 let winner = self.turn;
652 self.taken_discard = None;
653 self.knock = Some(KnockState {
654 knocker: winner,
655 spread: melds.into_array(),
656 knocker_deadwood: 0,
657 laid_off: Hand::EMPTY,
658 });
659 self.result = Some(RoundResult::BigGin {
660 winner,
661 deadwood: deadwood(self.hands[winner.opponent() as usize]),
662 });
663 self.phase = Phase::Finished;
664 Ok(())
665 }
666
667 #[allow(clippy::missing_panics_doc)]
680 pub fn lay_off(&mut self, card: Card, index: usize) -> Result<(), RoundError> {
681 self.expect_phase(Phase::Layoff)?;
682 if !self.hands[self.turn as usize].contains(card) {
683 return Err(RoundError::NotInHand(card));
684 }
685
686 let state = self.knock.as_mut().expect("a layoff follows a knock");
687 let meld = match state.spread.get(index) {
688 Some(Some(meld)) => *meld,
689 _ => return Err(RoundError::NoSuchMeld(index)),
690 };
691 let extended = meld.extended(card).ok_or(RoundError::CannotLayOff(card))?;
692
693 state.spread[index] = Some(extended);
694 state.laid_off.insert(card);
695 self.hands[self.turn as usize].remove(card);
696 Ok(())
697 }
698
699 #[allow(clippy::missing_panics_doc)]
711 pub fn finish_layoffs(&mut self) -> Result<RoundResult, RoundError> {
712 self.expect_phase(Phase::Layoff)?;
713 let state = self.knock.as_ref().expect("a layoff follows a knock");
714 let knocker = state.knocker;
715 let knocker_deadwood = state.knocker_deadwood;
716 let defender_deadwood = deadwood(self.hands[knocker.opponent() as usize]);
717
718 let undercut = defender_deadwood < knocker_deadwood
719 || (defender_deadwood == knocker_deadwood && self.rules.undercut_on_tie);
720 let result = if undercut {
721 RoundResult::Undercut {
722 winner: knocker.opponent(),
723 margin: knocker_deadwood - defender_deadwood,
724 }
725 } else {
726 RoundResult::Knock {
727 winner: knocker,
728 margin: defender_deadwood - knocker_deadwood,
729 }
730 };
731
732 self.result = Some(result);
733 self.phase = Phase::Finished;
734 Ok(result)
735 }
736}
737
738#[cfg(feature = "serde")]
740mod repr {
741 use super::{KnockState, Phase, Round, RoundResult};
742 use crate::{Card, Hand, Meld, Player, Rules, deadwood, pip_sum};
743 use thiserror::Error;
744
745 #[derive(Debug, Error, Clone, Copy, PartialEq, Eq)]
746 pub enum InvalidRound {
747 #[error(
748 "The hands, stock, discard pile, and layoffs must hold each of the 52 cards exactly once"
749 )]
750 NotAPartition,
751 #[error("Card counts are inconsistent with the phase")]
752 WrongCounts,
753 #[error("The pass counter is out of range or contradicts the acting player")]
754 BadUpcardState,
755 #[error("The initial upcard contradicts the discard pile")]
756 BadInitialUpcard,
757 #[error("A flag or component contradicts the phase")]
758 PhaseMismatch,
759 #[error("The spread is not disjoint melds over the knocker's cards and the layoffs")]
760 BadSpread,
761 #[error("The knocker's deadwood violates the knock limit")]
762 BadDeadwood,
763 #[error("The result does not match the recomputed score")]
764 BadResult,
765 }
766
767 #[derive(Clone, serde::Serialize, serde::Deserialize)]
768 pub struct KnockRepr {
769 knocker: Player,
770 spread: Vec<Meld>,
771 laid_off: Hand,
772 }
773
774 #[derive(Clone, serde::Serialize, serde::Deserialize)]
775 pub struct RoundRepr {
776 rules: Rules,
777 dealer: Player,
778 hands: [Hand; 2],
779 stock: Vec<Card>,
780 discards: Vec<Card>,
781 initial_upcard: Card,
782 phase: Phase,
783 turn: Player,
784 passes: u8,
785 forced_stock: bool,
786 taken_discard: Option<Card>,
787 knock: Option<KnockRepr>,
788 result: Option<RoundResult>,
789 }
790
791 impl From<Round> for RoundRepr {
792 fn from(round: Round) -> Self {
793 Self {
794 rules: round.rules,
795 dealer: round.dealer,
796 hands: round.hands,
797 stock: round.stock,
798 discards: round.discards,
799 initial_upcard: round.initial_upcard,
800 phase: round.phase,
801 turn: round.turn,
802 passes: round.passes,
803 forced_stock: round.forced_stock,
804 taken_discard: round.taken_discard,
805 knock: round.knock.map(|state| KnockRepr {
806 knocker: state.knocker,
807 spread: state.spread.into_iter().flatten().collect(),
808 laid_off: state.laid_off,
809 }),
810 result: round.result,
811 }
812 }
813 }
814
815 fn validate_knock(repr: &KnockRepr, hands: &[Hand; 2]) -> Result<KnockState, InvalidRound> {
818 if repr.spread.len() > 3 {
819 return Err(InvalidRound::BadSpread);
820 }
821
822 let mut union = Hand::EMPTY;
823 let mut spread = [None; 3];
824 for (slot, &meld) in spread.iter_mut().zip(&repr.spread) {
825 if !(union & meld.cards()).is_empty() {
826 return Err(InvalidRound::BadSpread);
827 }
828 union |= meld.cards();
829 *slot = Some(meld);
830 }
831
832 let own = union - repr.laid_off;
835 if repr.laid_off & union != repr.laid_off || own & hands[repr.knocker as usize] != own {
836 return Err(InvalidRound::BadSpread);
837 }
838
839 let knocker_deadwood = pip_sum(hands[repr.knocker as usize] - union) as u8;
841 Ok(KnockState {
842 knocker: repr.knocker,
843 spread,
844 knocker_deadwood,
845 laid_off: repr.laid_off,
846 })
847 }
848
849 impl TryFrom<RoundRepr> for Round {
850 type Error = InvalidRound;
851
852 #[allow(clippy::too_many_lines)]
853 fn try_from(repr: RoundRepr) -> Result<Self, InvalidRound> {
854 let mut seen = repr.hands[0];
856 if !(seen & repr.hands[1]).is_empty() {
857 return Err(InvalidRound::NotAPartition);
858 }
859 seen |= repr.hands[1];
860 for &card in repr.stock.iter().chain(&repr.discards) {
861 if !seen.insert(card) {
862 return Err(InvalidRound::NotAPartition);
863 }
864 }
865 if let Some(knock) = &repr.knock {
866 if !(seen & knock.laid_off).is_empty() {
867 return Err(InvalidRound::NotAPartition);
868 }
869 seen |= knock.laid_off;
870 }
871 if seen != Hand::ALL {
872 return Err(InvalidRound::NotAPartition);
873 }
874
875 let len = |player: Player| repr.hands[player as usize].len();
876 let turn = repr.turn;
877 if repr.passes > 2 {
878 return Err(InvalidRound::BadUpcardState);
879 }
880
881 if repr.forced_stock
885 && !(repr.phase == Phase::Draw && repr.passes == 2 && repr.stock.len() == 31)
886 {
887 return Err(InvalidRound::PhaseMismatch);
888 }
889 if repr.taken_discard.is_some() && repr.phase != Phase::Discard {
890 return Err(InvalidRound::PhaseMismatch);
891 }
892 if repr.result.is_some() != (repr.phase == Phase::Finished) {
893 return Err(InvalidRound::PhaseMismatch);
894 }
895
896 if (repr.phase == Phase::Upcard || repr.passes == 2)
901 && repr.discards.first() != Some(&repr.initial_upcard)
902 {
903 return Err(InvalidRound::BadInitialUpcard);
904 }
905 let knock_limit = repr.rules.knock_limit_for(repr.initial_upcard);
906
907 let knock = match repr.phase {
908 Phase::Upcard => {
909 if repr.passes > 1
910 || turn
911 != if repr.passes == 0 {
912 repr.dealer.opponent()
913 } else {
914 repr.dealer
915 }
916 {
917 return Err(InvalidRound::BadUpcardState);
918 }
919 if repr.stock.len() != 31
920 || repr.discards.len() != 1
921 || len(Player::One) != 10
922 || len(Player::Two) != 10
923 {
924 return Err(InvalidRound::WrongCounts);
925 }
926 if repr.knock.is_some() {
927 return Err(InvalidRound::PhaseMismatch);
928 }
929 None
930 }
931 Phase::Draw => {
932 if len(Player::One) != 10
933 || len(Player::Two) != 10
934 || repr.stock.len() < 3
935 || repr.discards.is_empty()
936 {
937 return Err(InvalidRound::WrongCounts);
938 }
939 if repr.knock.is_some() {
940 return Err(InvalidRound::PhaseMismatch);
941 }
942 None
943 }
944 Phase::Discard => {
945 if len(turn) != 11 || len(turn.opponent()) != 10 || repr.stock.len() < 2 {
946 return Err(InvalidRound::WrongCounts);
947 }
948 if repr.knock.is_some() {
949 return Err(InvalidRound::PhaseMismatch);
950 }
951 if let Some(card) = repr.taken_discard
952 && !repr.hands[turn as usize].contains(card)
953 {
954 return Err(InvalidRound::PhaseMismatch);
955 }
956 None
957 }
958 Phase::Layoff => {
959 let knock = repr.knock.as_ref().ok_or(InvalidRound::PhaseMismatch)?;
960 let state = validate_knock(knock, &repr.hands)?;
961 if state.knocker_deadwood == 0 {
964 return Err(InvalidRound::PhaseMismatch);
965 }
966 if state.knocker_deadwood > knock_limit {
967 return Err(InvalidRound::BadDeadwood);
968 }
969 if turn != state.knocker.opponent() {
970 return Err(InvalidRound::PhaseMismatch);
971 }
972 if len(state.knocker) != 10
973 || len(turn) + state.laid_off.len() != 10
974 || repr.stock.len() < 2
975 || repr.discards.is_empty()
976 {
977 return Err(InvalidRound::WrongCounts);
978 }
979 Some(state)
980 }
981 Phase::Finished => {
982 let result = repr.result.ok_or(InvalidRound::PhaseMismatch)?;
983 match result {
984 RoundResult::Dead => {
985 if repr.knock.is_some() {
986 return Err(InvalidRound::PhaseMismatch);
987 }
988 if repr.stock.len() != 2
989 || len(Player::One) != 10
990 || len(Player::Two) != 10
991 {
992 return Err(InvalidRound::WrongCounts);
993 }
994 None
995 }
996 RoundResult::Knock { winner, margin }
997 | RoundResult::Undercut { winner, margin } => {
998 let knock = repr.knock.as_ref().ok_or(InvalidRound::PhaseMismatch)?;
999 let state = validate_knock(knock, &repr.hands)?;
1000 let undercut = matches!(result, RoundResult::Undercut { .. });
1001 let knocker = if undercut { winner.opponent() } else { winner };
1002 if state.knocker != knocker || state.knocker_deadwood == 0 {
1003 return Err(InvalidRound::BadResult);
1004 }
1005 if state.knocker_deadwood > knock_limit {
1006 return Err(InvalidRound::BadDeadwood);
1007 }
1008 let defender = knocker.opponent();
1009 if len(knocker) != 10
1010 || len(defender) + state.laid_off.len() != 10
1011 || repr.stock.len() < 2
1012 {
1013 return Err(InvalidRound::WrongCounts);
1014 }
1015
1016 let defender_deadwood = deadwood(repr.hands[defender as usize]);
1020 let expected = defender_deadwood < state.knocker_deadwood
1021 || (defender_deadwood == state.knocker_deadwood
1022 && repr.rules.undercut_on_tie);
1023 let expected_margin = if undercut {
1024 state.knocker_deadwood.wrapping_sub(defender_deadwood)
1025 } else {
1026 defender_deadwood.wrapping_sub(state.knocker_deadwood)
1027 };
1028 if undercut != expected || margin != expected_margin {
1029 return Err(InvalidRound::BadResult);
1030 }
1031 Some(state)
1032 }
1033 RoundResult::Gin {
1034 winner,
1035 deadwood: loser,
1036 }
1037 | RoundResult::BigGin {
1038 winner,
1039 deadwood: loser,
1040 } => {
1041 let knock = repr.knock.as_ref().ok_or(InvalidRound::PhaseMismatch)?;
1042 let state = validate_knock(knock, &repr.hands)?;
1043 let big = matches!(result, RoundResult::BigGin { .. });
1044 let expected_len = if big { 11 } else { 10 };
1045 if state.knocker != winner
1046 || state.knocker_deadwood != 0
1047 || !state.laid_off.is_empty()
1048 {
1049 return Err(InvalidRound::BadResult);
1050 }
1051 if big && repr.rules.big_gin_bonus.is_none() {
1052 return Err(InvalidRound::BadResult);
1053 }
1054 if len(winner) != expected_len
1055 || len(winner.opponent()) != 10
1056 || repr.stock.len() < 2
1057 {
1058 return Err(InvalidRound::WrongCounts);
1059 }
1060 if loser != deadwood(repr.hands[winner.opponent() as usize]) {
1061 return Err(InvalidRound::BadResult);
1062 }
1063 Some(state)
1064 }
1065 }
1066 }
1067 };
1068
1069 Ok(Self {
1070 rules: repr.rules,
1071 dealer: repr.dealer,
1072 hands: repr.hands,
1073 stock: repr.stock,
1074 discards: repr.discards,
1075 initial_upcard: repr.initial_upcard,
1076 phase: repr.phase,
1077 turn: repr.turn,
1078 passes: repr.passes,
1079 forced_stock: repr.forced_stock,
1080 taken_discard: repr.taken_discard,
1081 knock,
1082 result: repr.result,
1083 })
1084 }
1085 }
1086}