/// <summary> /// Declares a kan (opened). Does not discard a tile. Does not draw substitution tile. /// </summary> /// <param name="tile">The stolen tile.</param> /// <param name="stolenFrom">The wind which the tile has been stolen from.</param> /// <param name="fromOpenPon">The <see cref="TileComboPivot"/>, if the kan is called as an override of a previous pon call; <c>Null</c> otherwise.</param> /// <exception cref="ArgumentNullException"><paramref name="tile"/> is <c>Null</c>.</exception> /// <exception cref="InvalidOperationException"><see cref="Messages.InvalidCall"/></exception> internal void DeclareKan(TilePivot tile, WindPivot?stolenFrom, TileComboPivot fromOpenPon) { if (tile == null) { throw new ArgumentNullException(nameof(tile)); } if (fromOpenPon == null) { CheckTilesForCallAndExtractCombo(_concealedTiles.Where(t => t == tile), stolenFrom.HasValue ? 3 : 4, stolenFrom.HasValue ? tile : null, stolenFrom ); } else { int indexOfPon = _declaredCombinations.IndexOf(fromOpenPon); if (indexOfPon < 0 || stolenFrom.HasValue) { throw new InvalidOperationException(Messages.InvalidCall); } var concealedTiles = new List <TilePivot> { tile }; concealedTiles.AddRange(fromOpenPon.Tiles.Where(t => !ReferenceEquals(t, fromOpenPon.OpenTile))); _declaredCombinations[indexOfPon] = new TileComboPivot(concealedTiles, fromOpenPon.OpenTile, fromOpenPon.StolenFrom); _concealedTiles.Remove(tile); } }
/// <summary> /// Checks if <see cref="Yakus"/> and <see cref="YakusCombinations"/> have to be cancelled because of the temporary furiten rule. /// </summary> /// <param name="currentRound">The current round</param> /// <param name="playerIndex">The player index of the hand.</param> /// <returns><c>True</c> if temporary furiten; <c>False</c> otherwise.</returns> internal bool CancelYakusIfTemporaryFuriten(RoundPivot currentRound, int playerIndex) { if (currentRound == null) { return(false); } int i = 0; while (currentRound.PlayerIndexHistory.Count < i && currentRound.PlayerIndexHistory.ElementAt(i) == playerIndex.RelativePlayerIndex(-(i + 1)) && playerIndex.RelativePlayerIndex(-(i + 1)) != playerIndex) { // The tile discarded by the latest player is the tile we ron ! if (i > 0) { TilePivot lastFromDiscard = currentRound.GetDiscard(currentRound.PlayerIndexHistory.ElementAt(i)).LastOrDefault(); if (lastFromDiscard != null && IsCompleteFull(new List <TilePivot>(ConcealedTiles) { lastFromDiscard }, DeclaredCombinations.ToList())) { Yakus = null; YakusCombinations = null; return(true); } } i++; } return(false); }
/// <summary> /// Constructor. /// </summary> /// <param name="discardRank">The <see cref="DiscardRank"/> value.</param> /// <param name="isDaburu">The <see cref="IsDaburu"/> value.</param> /// <param name="tile">The <see cref="Tile"/> value.</param> /// <param name="opponentsVirtualDiscardRank">The <see cref="_opponentsVirtualDiscardRank"/> value.</param> /// <exception cref="ArgumentNullException"><paramref name="tile"/> is <c>Null</c>.</exception> /// <exception cref="ArgumentNullException"><paramref name="opponentsVirtualDiscardRank"/> is <c>Null</c>.</exception> /// <exception cref="ArgumentOutOfRangeException"><paramref name="discardRank"/> is out of range.</exception> /// <exception cref="ArgumentException"><see cref="Messages.InvalidDiscardRank"/></exception> /// <exception cref="ArgumentException"><see cref="Messages.InvalidOpponentsVirtualDiscardRank"/></exception> public RiichiPivot(int discardRank, bool isDaburu, TilePivot tile, IDictionary <int, int> opponentsVirtualDiscardRank) { if (opponentsVirtualDiscardRank == null) { throw new ArgumentNullException(nameof(opponentsVirtualDiscardRank)); } if (discardRank < 0) { throw new ArgumentOutOfRangeException(nameof(discardRank)); } if (isDaburu && discardRank > 0) { throw new ArgumentException(Messages.InvalidDiscardRank, nameof(discardRank)); } if (opponentsVirtualDiscardRank.Keys.Count != 3 || opponentsVirtualDiscardRank.Values.Any(v => v < 0)) { throw new ArgumentException(Messages.InvalidOpponentsVirtualDiscardRank, nameof(opponentsVirtualDiscardRank)); } DiscardRank = discardRank; IsDaburu = isDaburu; Tile = tile ?? throw new ArgumentNullException(nameof(tile)); _opponentsVirtualDiscardRank = new Dictionary <int, int>(opponentsVirtualDiscardRank); }
private bool IsDiscardedOrUnusable(TilePivot tile, int opponentPlayerIndex, List <TilePivot> deadtiles) { return(_round.GetDiscard(opponentPlayerIndex).Contains(tile) || ( tile.IsHonor && deadtiles.Count(t => t == tile) == 4 && deadtiles.GroupBy(t => t).Any(t => t.Key != tile && t.Key.IsHonor && t.Count() == 4) )); }
/// <summary> /// Declares a kan (concealed). Does not discard a tile. Does not draw substitution tile. /// </summary> /// <param name="tile">The tile, from the current hand, to make a square from.</param> /// <exception cref="ArgumentNullException"><paramref name="tile"/> is <c>Null</c>.</exception> /// <exception cref="InvalidOperationException"><see cref="Messages.InvalidCall"/></exception> internal void DeclareKan(TilePivot tile) { if (tile == null) { throw new ArgumentNullException(nameof(tile)); } CheckTilesForCallAndExtractCombo(_concealedTiles.Where(t => t == tile), 4, null, null); }
/// <summary> /// Declares a pon. Does not discard a tile. /// </summary> /// <param name="tile">The stolen tile.</param> /// <param name="stolenFrom">The wind which the tile has been stolen from.</param> /// <exception cref="ArgumentNullException"><paramref name="tile"/> is <c>Null</c>.</exception> /// <exception cref="InvalidOperationException"><see cref="Messages.InvalidCall"/></exception> internal void DeclarePon(TilePivot tile, WindPivot stolenFrom) { if (tile == null) { throw new ArgumentNullException(nameof(tile)); } CheckTilesForCallAndExtractCombo(_concealedTiles.Where(t => t == tile), 2, tile, stolenFrom); }
/// <summary> /// Sets <see cref="LatestPick"/> after a ron. /// </summary> /// <param name="ronTile">The ron tile.</param> internal void SetFromRon(TilePivot ronTile) { if (Yakus == null || YakusCombinations == null || ronTile == null) { return; } LatestPick = ronTile; }
/// <summary> /// Tries to discard the specified tile. /// </summary> /// <param name="tile">The tile to discard; should obviously be contained in <see cref="_concealedTiles"/>.</param> /// <param name="afterStealing">Optionnal; indicates if the discard is made after stealing a tile; the default value is <c>False</c>.</param> /// <returns><c>False</c> if the discard is forbidden by the tile stolen; <c>True</c> otherwise.</returns> /// <exception cref="ArgumentNullException"><paramref name="tile"/> is <c>Null</c>.</exception> /// <exception cref="InvalidOperationException"><see cref="Messages.ImpossibleDiscard"/></exception> /// <exception cref="ArgumentException"><see cref="Messages.ImpossibleStealingArgument"/></exception> internal bool Discard(TilePivot tile, bool afterStealing = false) { if (!CanDiscardTile(tile, afterStealing)) { return(false); } _concealedTiles.Remove(tile); return(true); }
/// <summary> /// Constructor. /// </summary> /// <param name="playerIndex">The <see cref="PlayerIndex"/> value.</param> /// <param name="tile">The <see cref="Tile"/> value.</param> /// <exception cref="ArgumentNullException"><paramref name="tile"/> is <c>Null</c>.</exception> /// <exception cref="ArgumentOutOfRangeException"><paramref name="playerIndex"/> is out of range.</exception> internal TileEventArgs(int playerIndex, TilePivot tile) { if (playerIndex < 0 || playerIndex > 3) { throw new ArgumentOutOfRangeException(nameof(playerIndex)); } PlayerIndex = playerIndex; Tile = tile ?? throw new ArgumentNullException(nameof(tile)); }
private IEnumerable <TilePivot> GetSujisFromDiscard(TilePivot tile, int opponentPlayerIndex) { if (tile.IsHonor) { return(Enumerable.Empty <TilePivot>()); } return(_round .GetDiscard(opponentPlayerIndex) .Where(_ => _.Family == tile.Family && (_.Number == tile.Number + 3 || _.Number == tile.Number - 3))); }
/// <summary> /// Picks a tile from the wall (or from the treasure as compensation of a kan) and adds it to the hand. /// </summary> /// <param name="tile">The tile picked.</param> /// <exception cref="ArgumentNullException"><paramref name="tile"/> is <c>Null</c>.</exception> /// <exception cref="InvalidOperationException"><see cref="Messages.InvalidDraw"/></exception> internal void Pick(TilePivot tile) { if (_concealedTiles.Count + _declaredCombinations.Count * 3 != 13) { throw new InvalidOperationException(Messages.InvalidDraw); } LatestPick = tile ?? throw new ArgumentNullException(nameof(tile)); _concealedTiles.Add(tile); _concealedTiles.Sort(); }
/// <summary> /// Checks if a tile can be discarded, but does not discard it. /// </summary> /// <param name="tile">The tile to discard; should obviously be contained in <see cref="_concealedTiles"/>.</param> /// <param name="afterStealing">Optionnal; indicates if the discard is made after stealing a tile; the default value is <c>False</c>.</param> /// <returns><c>False</c> if the discard is forbidden by the tile stolen; <c>True</c> otherwise.</returns> /// <exception cref="ArgumentNullException"><paramref name="tile"/> is <c>Null</c>.</exception> /// <exception cref="InvalidOperationException"><see cref="Messages.ImpossibleDiscard"/></exception> /// <exception cref="ArgumentException"><see cref="Messages.ImpossibleStealingArgument"/></exception> internal bool CanDiscardTile(TilePivot tile, bool afterStealing = false) { if (tile == null) { throw new ArgumentNullException(nameof(tile)); } if (!_concealedTiles.Contains(tile)) { throw new InvalidOperationException(Messages.ImpossibleDiscard); } if (afterStealing) { TileComboPivot lastCombination = _declaredCombinations.LastOrDefault(); if (lastCombination == null || lastCombination.IsConcealed) { throw new ArgumentException(Messages.ImpossibleStealingArgument, nameof(afterStealing)); } TilePivot stolenTile = lastCombination.OpenTile; if (stolenTile == tile) { return(false); } else if (lastCombination.IsSequence && tile.Family == lastCombination.Family && lastCombination.OpenTile.Number == lastCombination.SequenceFirstNumber && tile.Number == lastCombination.SequenceFirstNumber + 3) { return(false); } else if (lastCombination.IsSequence && tile.Family == lastCombination.Family && lastCombination.OpenTile.Number == lastCombination.SequenceLastNumber && tile.Number == lastCombination.SequenceLastNumber - 3) { return(false); } } return(true); }
/// <summary> /// Checks if any CPU player can make a pon call, and computes its decision if any. /// </summary> /// <returns>The player index who makes the call; <c>-1</c> is none.</returns> public int PonDecision() { int opponentPlayerId = _round.OpponentsCanCallPon(); if (opponentPlayerId > -1) { TilePivot tile = _round.GetDiscard(_round.PreviousPlayerIndex).Last(); // Call the pon if : // - the hand is already open // - it's valuable (see "HandCanBeOpened") var opponentHand = _round.GetHand(opponentPlayerId); if (!opponentHand.IsConcealed || HandCanBeOpened(opponentPlayerId, tile, opponentHand)) { return(opponentPlayerId); } opponentPlayerId = -1; } return(opponentPlayerId); }
/// <summary> /// Constructor. /// </summary> /// <param name="latestTile">The <see cref="LatestTile"/> value.</param> /// <param name="drawType">The <see cref="DrawType"/> value.</param> /// <param name="dominantWind">The <see cref="DominantWind"/> value.</param> /// <param name="playerWind">The <see cref="PlayerWind"/> value.</param> /// <param name="isFirstOrLast">Optionnal; indicates a win at the first turn without any call made (<c>True</c>) or at the last tile of the round (<c>Null</c>); default value is <c>False</c>.</param> /// <param name="isRiichi">Optionnal; indicates if riichi (<c>True</c>) or riichi at first turn without any call made (<c>Null</c>); default value is <c>False</c>.</param> /// <param name="isIppatsu">Optionnal; indicates if it's a win by ippatsu (<paramref name="isRiichi"/> can't be <c>False</c> in such case); default value is <c>False</c>.</param> /// <param name="useRenhou">Optionnal; the <see cref="_useRenhou"/> value; default value is <c>False</c>.</param> /// <exception cref="ArgumentNullException"><paramref name="latestTile"/> is <c>Null</c>.</exception> /// <exception cref="ArgumentException"><see cref="Messages.InvalidContextIppatsuValue"/></exception> public WinContextPivot(TilePivot latestTile, DrawTypePivot drawType, WindPivot dominantWind, WindPivot playerWind, bool?isFirstOrLast = false, bool?isRiichi = false, bool isIppatsu = false, bool useRenhou = false) { if (isRiichi == false && isIppatsu) { throw new ArgumentException(Messages.InvalidContextIppatsuValue, nameof(isIppatsu)); } LatestTile = latestTile ?? throw new ArgumentNullException(nameof(latestTile)); IsRoundLastTile = isFirstOrLast == null; IsRiichi = isRiichi != false; IsFirstTurnRiichi = isRiichi == null; IsIppatsu = isIppatsu; DominantWind = dominantWind; PlayerWind = playerWind; IsFirstTurnDraw = isFirstOrLast == true; DrawType = drawType; _useRenhou = useRenhou; IsNagashiMangan = false; }
/// <summary> /// Declares a chii. Does not discard a tile. /// </summary> /// <param name="tile">The stolen tile.</param> /// <param name="stolenFrom">The wind which the tile has been stolen from.</param> /// <param name="startNumber">The sequence first number.</param> /// <exception cref="ArgumentNullException"><paramref name="tile"/> is <c>Null</c>.</exception> /// <exception cref="ArgumentOutOfRangeException"><paramref name="startNumber"/> is out of range.</exception> /// <exception cref="InvalidOperationException"><see cref="Messages.InvalidCall"/></exception> internal void DeclareChii(TilePivot tile, WindPivot stolenFrom, int startNumber) { if (tile == null) { throw new ArgumentNullException(nameof(tile)); } if (startNumber < 1 || startNumber > 7) { throw new ArgumentOutOfRangeException(nameof(startNumber)); } if (tile.Number < startNumber || tile.Number > startNumber + 2) { throw new InvalidOperationException(Messages.InvalidCall); } CheckTilesForCallAndExtractCombo(Enumerable .Range(startNumber, 3) .Where(i => i != tile.Number) .Select(i => _concealedTiles.FirstOrDefault(t => t.Family == tile.Family && t.Number == i)) .Where(t => t != null), 2, tile, stolenFrom); }
private bool HandCanBeOpened(int playerId, TilePivot tile, HandPivot hand) { var valuableWinds = new[] { _round.Game.GetPlayerCurrentWind(playerId), _round.Game.DominantWind }; var canPonForYakuhai = IsDragonOrValuableWind(tile, valuableWinds); // >= 75% of the tile family var closeToChinitsu = hand.ConcealedTiles.Count(_ => _.Family == tile.Family) >= 11; // how much pair (or better) of valuable honors ? var valuableHonorPairs = hand.ConcealedTiles.GroupBy(_ => _) .Count(_ => _.Key.IsHonor && _.Count() >= 2 && IsDragonOrValuableWind(_.Key, valuableWinds)); if (!canPonForYakuhai && valuableHonorPairs < 2 && !closeToChinitsu) { return(false); } int dorasCount = hand.ConcealedTiles .Sum(t => _round.DoraIndicatorTiles .Take(_round.VisibleDorasCount) .Count(d => t.IsDoraNext(d))); int redDorasCount = hand.ConcealedTiles.Count(t => t.IsRedDora); var hasValuablePair = hand.ConcealedTiles.GroupBy(_ => _) .Any(_ => _.Count() >= 2 && _.Key != tile && IsDragonOrValuableWind(_.Key, valuableWinds)); // >= 66% of one family or honor var closeToHonitsu = new[] { FamilyPivot.Bamboo, FamilyPivot.Caracter, FamilyPivot.Circle } .Any(f => hand.ConcealedTiles.Count(t => t.Family == f || t.IsHonor) > 9); return(hasValuablePair || (dorasCount + redDorasCount) > 0 || closeToHonitsu || valuableWinds[0] == WindPivot.East); }
/// <summary> /// Constructor. /// </summary> /// <param name="concealedTiles">List of concealed tiles.</param> /// <param name="openTile">Optionnal; the <see cref="OpenTile"/> value; default value is <c>Null</c>.</param> /// <param name="stolenFrom">Optionnal; the <see cref="StolenFrom"/> value; default value is <c>Null</c>.</param> /// <exception cref="ArgumentNullException"><paramref name="concealedTiles"/> is <c>Null</c>.</exception> /// <exception cref="ArgumentException"><see cref="Messages.InvalidTilesCount"/></exception> /// <exception cref="ArgumentException"><see cref="Messages.InvalidCombination"/></exception> /// <exception cref="ArgumentException"><see cref="Messages.StolenFromNotSpecified"/></exception> public TileComboPivot(IEnumerable <TilePivot> concealedTiles, TilePivot openTile = null, WindPivot?stolenFrom = null) { if (concealedTiles is null) { throw new ArgumentNullException(nameof(concealedTiles)); } var tiles = new List <TilePivot>(concealedTiles); if (openTile != null) { tiles.Add(openTile); } if (tiles.Count() < 2 || tiles.Count() > 4) { throw new ArgumentException(Messages.InvalidTilesCount, nameof(concealedTiles)); } if (openTile != null && !stolenFrom.HasValue) { throw new ArgumentException(Messages.StolenFromNotSpecified, nameof(stolenFrom)); } OpenTile = openTile; StolenFrom = stolenFrom; // The sort is important here... _tiles = tiles.OrderBy(t => t).ToList(); // ...to check the validity of a potential sequence if (!IsValidCombination()) { throw new ArgumentException(Messages.InvalidCombination, nameof(tiles)); } }
// Assumes that all tiles are from the same family, and this family is caracter / circle / bamboo. // Also assumes that referenced tile is included in the list. private static List <TileComboPivot> GetCombinationsForTile(TilePivot tile, IEnumerable <TilePivot> tiles) { var combinations = new List <TileComboPivot>(); List <TilePivot> sameNumber = tiles.Where(t => t.Number == tile.Number).ToList(); if (sameNumber.Count > 1) { // Can make a pair. combinations.Add(new TileComboPivot(new List <TilePivot> { tile, tile })); if (sameNumber.Count > 2) { // Can make a brelan. combinations.Add(new TileComboPivot(new List <TilePivot> { tile, tile, tile })); } } TilePivot secondLow = tiles.FirstOrDefault(t => t.Number == tile.Number - 2); TilePivot firstLow = tiles.FirstOrDefault(t => t.Number == tile.Number - 1); TilePivot firstHigh = tiles.FirstOrDefault(t => t.Number == tile.Number + 1); TilePivot secondHigh = tiles.FirstOrDefault(t => t.Number == tile.Number + 2); if (secondLow != null && firstLow != null) { // Can make a sequence. combinations.Add(new TileComboPivot(new List <TilePivot> { secondLow, firstLow, tile })); } if (firstLow != null && firstHigh != null) { // Can make a sequence. combinations.Add(new TileComboPivot(new List <TilePivot> { firstLow, tile, firstHigh })); } if (firstHigh != null && secondHigh != null) { // Can make a sequence. combinations.Add(new TileComboPivot(new List <TilePivot> { tile, firstHigh, secondHigh })); } return(combinations); }
// Builds a pair, brelan or square of the specified tile. private static TileComboPivot Build(TilePivot tile, int k) { return(new TileComboPivot(Enumerable.Range(0, k).Select(i => tile))); }
/// <summary> /// Builds a square from the specified tile. /// </summary> /// <param name="tile">The tile.</param> /// <returns>The square.</returns> public static TileComboPivot BuildSquare(TilePivot tile) { return(Build(tile, 4)); }
/// <summary> /// Builds a brelan from the specified tile. /// </summary> /// <param name="tile">The tile.</param> /// <returns>The brelan.</returns> public static TileComboPivot BuildBrelan(TilePivot tile) { return(Build(tile, 3)); }
/// <summary> /// Builds a pair from the specified tile. /// </summary> /// <param name="tile">The tile.</param> /// <returns>The pair.</returns> public static TileComboPivot BuildPair(TilePivot tile) { return(Build(tile, 2)); }
// suji on the edge (safer) private bool IsOutsiderSuji(TilePivot tile, int opponentPlayerIndex) { return(GetSujisFromDiscard(tile, opponentPlayerIndex) .Any(_ => new[] { 4, 5, 6 }.Contains(_.Number))); }
private static bool IsDragonOrValuableWind(TilePivot tile, WindPivot[] winds) { return(tile.Family == FamilyPivot.Dragon || (tile.Family == FamilyPivot.Wind && winds.Contains(tile.Wind.Value))); }
// Creates a declared combination from the specified tiles private void CheckTilesForCallAndExtractCombo(IEnumerable <TilePivot> tiles, int expectedCount, TilePivot tile, WindPivot?stolenFrom) { if (tiles.Count() < expectedCount) { throw new InvalidOperationException(Messages.InvalidCall); } List <TilePivot> tilesPick = tiles.Take(expectedCount).ToList(); _declaredCombinations.Add(new TileComboPivot(tilesPick, tile, stolenFrom)); tilesPick.ForEach(t => _concealedTiles.Remove(t)); }
// a family is over-represented in the opponent discard (at least 9 overal, and at least 3 distinct values) private bool IsMaxedFamilyInDiscard(TilePivot tile, int opponentPlayerIndex) { return(!tile.IsHonor && _round.GetDiscard(opponentPlayerIndex).Count(_ => _.Family == tile.Family) >= 9 && _round.GetDiscard(opponentPlayerIndex).Where(_ => _.Family == tile.Family).Distinct().Count() >= 3); }
// suji on both side private bool IsDoubleInsiderSuji(TilePivot tile, int opponentPlayerIndex) { return(GetSujisFromDiscard(tile, opponentPlayerIndex).Distinct().Count() > 1); }