public void SomeHandByTsumo(string handString, int roundWind, int seatWind, string discardString, Yaku expectedYaku) { var discard = TileType.FromString(discardString); var sp = new ShorthandParser(handString); var hand = new HandCalculator(sp); var wind = new WindScoringData(roundWind, seatWind); var(yaku, fu) = ScoreCalculator.TsumoWithYaku(hand.ScoringData, wind, discard); Assert.Equal(expectedYaku, yaku); }
public void TotalFuTsumo(string handString, int roundWind, int seatWind, string drawString, int expectedFu) { var draw = TileType.FromString(drawString); var sp = new ShorthandParser(handString); var hand = new HandCalculator(sp); var wind = new WindScoringData(roundWind, seatWind); var(han, fu) = ScoreCalculator.Tsumo(hand.ScoringData, wind, draw); Assert.Equal(expectedFu, fu); }
public static bool CanRon(Board board, int seatIndex) { var seat = board.Seats[seatIndex]; if (seat.IgnoredRonFuriten) { return(false); } var winningTile = board.Seats[board.ActiveSeatIndex].CurrentDiscard !; var hand = (HandCalculator)seat.Hand.WithTile(winningTile.TileType); if (hand.Shanten != -1) { return(false); } var furitenTileTypes = seat.Hand.GetFuritenTileTypes().ToList(); if (furitenTileTypes.Intersect(seat.Discards.Select(t => t.TileType)).Any()) { return(false); } // Most yaku var w = new WindScoringData(board.RoundWind.Index, seat.SeatWind.Index); if (hand.ScoringData.YakuAndFu(w, winningTile.TileType, true).Item1 != 0) { return(true); } // riichi, double riichi, ippatsu if (seat.DeclaredRiichi) { return(true); } // chiihou if (board.IsFirstGoAround) { return(true); } // houtei raoyui if (board.Wall.RemainingDraws == 0) { return(true); } return(false); }
public static bool CanTsumo(Board board, bool isRinshanDraw) { var seat = board.ActiveSeat; if (seat.Hand.Shanten != -1) { return(false); } // Most yaku var w = new WindScoringData(board.RoundWind.Index, seat.SeatWind.Index); if (seat.Hand.ScoringData.YakuAndFu(w, seat.CurrentDraw !.TileType, false).Item1 != 0) { return(true); } // riichi, double riichi, ippatsu if (seat.DeclaredRiichi) { return(true); } // tenhou if (board.IsFirstGoAround) { return(true); } // haitei raoyue if (board.Wall.RemainingDraws == 0) { return(true); } // rinshan kaihou if (seat.CurrentDraw != null && isRinshanDraw && board.Wall.RemainingDraws > 0) { return(true); } return(false); }
public (long, int) YakuAndFu(WindScoringData windScoringData, TileType winningTile, bool isRon) { Span <long> waitShiftValues = stackalloc long[4]; _waitShiftValues.CopyTo(waitShiftValues); var winningTileIndex = winningTile.Index; var winningTileSuit = winningTile.SuitId; waitShiftValues[winningTileSuit] >>= winningTileIndex + 1; var suitsAnd = _suitOr[0] & _suitOr[1] & _suitOr[2]; var sanshokuShift = (int)suitsAnd & 0b11111; var sanshoku = (suitsAnd >> sanshokuShift) & SanshokuYakuFilter; /* * Pinfu * Honors are shifted by an amount based on value winds to make sure only guest wind pairs are possible * The suit with the winning tile is shifted by the drawn tile to ensure ryanmen wait and non-honor wait (also used for other yaku) * After that, some constellations where pinfu is not possible because of other yaku locking shapes are eliminated: * Sanankou and sanshoku. Ittsuu is a single suit issue and has been dealt with in the lookup preparation. * Some chiitoitsu hands evaluate to pinfu by the previous steps, despite being clearly not pinfu. * A flag in BigSum is created by adding 1 for each suit with a pair and a 2 for honors. * This will leave a 0 in the second bit in the bad case: 11223399m11p11s44z This 0 is aligned with the pinfu bit index. */ var bigSum = (_suitOr[0] & SuitBigSumFilter) + (_suitOr[1] & SuitBigSumFilter) + (_suitOr[2] & SuitBigSumFilter) + (_honorOr & HonorBigSumFilter); var honorWindShift = _honorOr >> windScoringData.HonorShift; var waitAndWindShift = waitShiftValues[0] & waitShiftValues[1] & waitShiftValues[2] & waitShiftValues[3] & honorWindShift; var pinfu = waitAndWindShift & // TODO suitsAnd + 1 should instead be handled in bitField preparation (_suitOr[winningTileSuit] >> (int)((winningTileIndex + ((suitsAnd + 1) & 1)) * (sanshoku >> 6))) & bigSum & PinfuYakuFilter; var tankiBit = waitShiftValues[winningTileSuit] & 0b1L; // get this before ron shifting // TODO get rid of this conditional / check if this is converted to (0/1) - 1 AND difference var singleWaitFuJihai = winningTileSuit == 3 ? (waitShiftValues[winningTileSuit] >> 9) & 2L : 0; // TODO waitAndRonShift is only used for ankou now, so maybe shifting inside the array is not necessary anymore? // TODO might be able to rework ron shift to not use up so many bits. // TODO check if this is converted to (0/1) - 1 AND difference var ronShiftAmount = isRon ? 9 : 0; waitShiftValues[winningTileSuit] >>= ronShiftAmount; var waitAndRonShift = (waitShiftValues[0] & RonShiftSumFilter) + (waitShiftValues[1] & RonShiftSumFilter) + (waitShiftValues[2] & RonShiftSumFilter) + (waitShiftValues[3] & RonShiftSumFilter); waitAndRonShift += _shiftedAnkanCount; waitAndRonShift += bigSum & (0b111L << AnkouRonShiftSumFilterIndex); waitAndRonShift += waitAndRonShift & (0b101L << AnkouRonShiftSumFilterIndex); var bigAnd = suitsAnd & _honorOr; var result = 0L; result |= ((1L << BitIndex.MenzenTsumo) >> ronShiftAmount) & (1L << BitIndex.MenzenTsumo); result |= waitAndRonShift & AnkouYakuFilter; result |= sanshoku; result |= pinfu; result |= bigAnd & BigAndYakuFilter; bigSum |= bigAnd & _bigAndToSumFilter; var bigSumPostElimination = bigSum & ~((bigSum & BigSumEliminationFilter) >> EliminationDelta); result |= bigSumPostElimination & BigSumPostEliminationYakuFilter; result |= _honorSum & HonorSumYakuFilter & windScoringData.ValueWindFilter; result |= _sankantsuSuukantsu & (11L << BitIndex.Sankantsu); var ryuuiisouSum = (_suitOr[0] & RyuuiisouSumFilter01) + (_suitOr[1] & RyuuiisouSumFilter01) + (_suitOr[2] & RyuuiisouSumFilter2) + _honorSum; result |= ryuuiisouSum & (1L << BitIndex.Ryuuiisou); if ((result & (1L << BitIndex.Chiitoitsu)) != 0) { result &= ~((1L << BitIndex.ClosedChanta) | (1L << BitIndex.Iipeikou)); } var openIipeikouBit = (result >> BitIndex.Iipeikou) & 1L; var sanankouBit = (result >> BitIndex.Sanankou) & 1L; // get this before shifting to open ssk var sanshokuFuMultiplier = (int)(((result >> BitIndex.SanshokuDoukou) + (result >> BitIndex.ClosedSanshokuDoujun)) & 1); result += (result & OpenBitFilter) * _openBit; var closedChantaBit = (result >> BitIndex.ClosedChanta) & 1L; var closedJunchanBit = (result >> BitIndex.ClosedJunchan) & 1L; var openJunchanBit = (result >> BitIndex.OpenJunchan) & 1L; var toitoiBit = (result >> BitIndex.Toitoi) & 1L; var x = openIipeikouBit & (closedChantaBit | closedJunchanBit); var removeSequenceYaku = (sanankouBit ^ x) & sanankouBit; var removeOpenJunchan = openIipeikouBit & (sanankouBit | toitoiBit) & openJunchanBit; var removeSanankou = x * (1 - toitoiBit); result -= (result & (1L << BitIndex.Sanankou)) * removeSanankou; // (openIipeikouBit << BitIndex.OpenChanta) means 111222333 shape and chanta, here excluded in case of sanankou result -= (result & ((1L << BitIndex.Pinfu) | (1L << BitIndex.Iipeikou) | (openIipeikouBit << BitIndex.OpenChanta))) * removeSequenceYaku; result -= (result & (1L << BitIndex.OpenJunchan)) * removeOpenJunchan; result -= (result & ((1L << BitIndex.Iipeikou) | (1L << BitIndex.ClosedJunchan))) * (toitoiBit & (1 - _openBit)); result += (result & TankiUpgradeableFilter) * tankiBit; // This covers the 22234555m222p222s case where sanankou/sanshoku doukou depend on the wait. var sanankouAndDoukou = (_suitOr[winningTileSuit] >> (BitIndex.Sanankou - BitIndex.SanshokuDoukou + 1)) & (1L << BitIndex.SanshokuDoukou); var waitPreventsDoukou = (suitsAnd >> (winningTileIndex + ronShiftAmount - 9)) & sanankouAndDoukou; result -= waitPreventsDoukou & (result >> (BitIndex.Sanankou - BitIndex.SanshokuDoukou)); result &= _baseMask & _baseMaskFilter; var yakuman = result & YakumanFilter; if (yakuman != 0) { return(yakuman, 0); } if ((result & (1L << BitIndex.Chiitoitsu)) != 0) { return(result, 25); } var closedRonFu = (int)(1 - _openBit) * (10 >> (9 - ronShiftAmount)); if ((result & PinfuYakuFilter) != 0) { return(result, 20 + closedRonFu); } var squareTypeToShuntsu = ((ryuuiisouSum & ~result) >> BitIndex.Sanankou) & ((1L - _openBit) | (result >> BitIndex.OpenJunchan) | ((result >> BitIndex.OpenChanta) & 1L)); var footprintKey = (sanshokuShift + 1) * 40 * sanshokuFuMultiplier; footprintKey += (int)squareTypeToShuntsu * 40; footprintKey |= ronShiftAmount & 1; // ronShiftAmount is either 0 or 0b1001 footprintKey |= (int)_openBit << 1; Span <int> keys = stackalloc int[] { footprintKey, footprintKey, footprintKey, 0 }; keys[winningTileSuit] += (winningTileIndex + 1) * 4; var fuM = FuFootprint(0, keys[0]); var fuP = FuFootprint(1, keys[1]); var fuS = FuFootprint(2, keys[2]); // TODO jihai single wait and ankou fu the same way as suits, maybe with value wind info instead of ssk, incorporating valuePairFu var bonusAnkouCountWinningSuit = (waitShiftValues[winningTileSuit] >> 32) & 1L; var potentialBonusAnkouCountWinningSuit = (_waitShiftValues[winningTileSuit] >> 32) & 1L; var ankouFuCorrection = (potentialBonusAnkouCountWinningSuit - bonusAnkouCountWinningSuit) * ((0b100000 >> winningTileSuit) & 0b100); var ankouFuJihai = ((_waitShiftValues[3] >> 17) & (0b111L << 3)) - ankouFuCorrection; var tsumoFu = 2 >> ronShiftAmount; // lowest bit of honorOr is 1 iff there is a wind pair var valueWindAdjuster = 1 + (_honorOr & windScoringData.DoubleValueWindBit); var valuePairFu = (honorWindShift & 1L) << (int)valueWindAdjuster; var fu = _baseAndMeldFu + closedRonFu + tsumoFu + (int)valuePairFu + fuM + fuP + fuS + (int)ankouFuJihai + (int)singleWaitFuJihai; var r = fu % 10; var rounding = r == 0 && fu != 20 ? 0 : 10 - r; return(result, fu + rounding); }