NickV
NickV

Reputation: 649

Linq - getting consecutive numbers in an array

I am creating a poker system and I am currently streamlining my hand calculator.

The following code works:

public enum CARDS
{
    None = 0,
    Two,
    Three,
    Four,
    Five,
    Six,
    Seven,
    Eight,
    Nine,
    Ten,
    Jack,
    Queen,
    King,
    Ace
};

public enum SUITS
{
    None = 0,
    Diamonds,
    Clubs,
    Hearts,
    Spades
};

public class Card
{
    public CARDS Val { get; set; }
    public SUITS Suit { get; set; }
}

public class IntIndex
{
    public int Count { get; set; }
    public int Index { get; set; }
}

static void Test()
{
    List<Card> cardList = new List<Card>();
    cardList.Add(new Card { Suit = SUITS.Diamonds, Val = CARDS.Two });
    cardList.Add(new Card { Suit = SUITS.Hearts, Val = CARDS.Four });
    cardList.Add(new Card { Suit = SUITS.Clubs, Val = CARDS.Five });
    cardList.Add(new Card { Suit = SUITS.Diamonds, Val = CARDS.Six });
    cardList.Add(new Card { Suit = SUITS.Spades, Val = CARDS.Six });
    cardList.Add(new Card { Suit = SUITS.Hearts, Val = CARDS.Seven });
    cardList.Add(new Card { Suit = SUITS.Clubs, Val = CARDS.Eight });

    // I have a processor that iterates through the above card list and creates
    // the following array based on the Card.Val as an index
    int[] list = new int[] {0,0,0,1,1,2,1,1,0,0,1,0,0,0};
    List<IntIndex> indexList =
        list.Select((item, index) => new IntIndex { Count = item, Index = index })
        .Where(c => c.Count > 0).ToList();

    List<int> newList = (from i in indexList
                         join j in indexList on i.Index equals j.Index + 1
                         where j.Count > 0
                         select i.Index).ToList();

    // Add the previous index since the join only works on n+1
    // Note - Is there a way to include the first comparison card?
    newList.Insert(0, newList[0] - 1);

    // Nice! - got my straight card list
    List<CARDS> cards = (from l in newList
                         select (CARDS)l).ToList();
}

However, I want to make it more compact as in:

static void Test()
{
    List<Card> cardList = new List<Card>();
    cardList.Add(new Card { Suit = SUITS.Diamonds, Val = CARDS.Two });
    cardList.Add(new Card { Suit = SUITS.Hearts, Val = CARDS.Four });
    cardList.Add(new Card { Suit = SUITS.Clubs, Val = CARDS.Five });
    cardList.Add(new Card { Suit = SUITS.Diamonds, Val = CARDS.Six });
    cardList.Add(new Card { Suit = SUITS.Spades, Val = CARDS.Six });
    cardList.Add(new Card { Suit = SUITS.Hearts, Val = CARDS.Seven });
    cardList.Add(new Card { Suit = SUITS.Clubs, Val = CARDS.Eight });

    List<Card> newList1 = (from i in cardList
                           join j in cardList on i.Val equals j.Val + 1
                           select i).ToList();

    // Add the previous index since the join only works on n+1
    // Similar to: newList1.Insert(0, newList1[0] - 1);
    // However, newList1 deals with Card objects so I need
    // To figure how to get the previous, non-duplicate card
    // from the original cardList (unless there is a way to return the
    // missing card!)
}

The problem is that the Sixes are being repeated. Distinct as well as a custom compare function does not work since this will break the n+1 join clause.

Another problem is with the following card list:

    List<Card> cardList = new List<Card>();
    cardList.Add(new Card { Suit = SUITS.Diamonds, Val = CARDS.Two });
    cardList.Add(new Card { Suit = SUITS.Hearts, Val = CARDS.Three });
    cardList.Add(new Card { Suit = SUITS.Clubs, Val = CARDS.Five });
    cardList.Add(new Card { Suit = SUITS.Diamonds, Val = CARDS.Six });
    cardList.Add(new Card { Suit = SUITS.Spades, Val = CARDS.Six });
    cardList.Add(new Card { Suit = SUITS.Hearts, Val = CARDS.Seven });
    cardList.Add(new Card { Suit = SUITS.Clubs, Val = CARDS.Eight });
    cardList.Add(new Card { Suit = SUITS.Diamonds, Val = CARDS.Jack });

I get a return list of 3Hearts, 6Diamond, 7Hearts, 8Hearts since 2 and 3 are consecutive.

What I really want is a list that returns consecutive cards of 5 or greater, or better yet, the top 5 cards of a continuous sequence. So the above list will return empty since there are no 5 consecutive cards in the input list.

Upvotes: 4

Views: 3139

Answers (6)

NickV
NickV

Reputation: 649

Stupid is as stipid does! Whay was I so worried about using LINQ when the quickest soulution was a simple bit mask:

    List<Card> cardList = new List<Card>();
    cardList.Add(new Card { Suit = SUITS.Diamonds, Val = RANK.Two });
    cardList.Add(new Card { Suit = SUITS.Hearts, Val = RANK.Three });
    cardList.Add(new Card { Suit = SUITS.Clubs, Val = RANK.Five });
    cardList.Add(new Card { Suit = SUITS.Diamonds, Val = RANK.Seven });
    cardList.Add(new Card { Suit = SUITS.Hearts, Val = RANK.Four });
    cardList.Add(new Card { Suit = SUITS.Clubs, Val = RANK.King });
    cardList.Add(new Card { Suit = SUITS.Diamonds, Val = RANK.Ace });

    int card = 0;
    foreach (Card c in cardList)
    {
        card |= 1 << (int)c.Val - 1;
    }

    bool isStraight = false;
    RANK high = RANK.Five;
    int mask = 0x1F;
    while (mask < card)
    {
        ++high;

        if ((mask & card) == mask)
        {
            isStraight = true;
        }
        else if (isStraight)
        {
            --high;
            break;
        }

        mask <<= 1;
    }

    // Check for Ace low
    if ((!isStraight) && ((0x100F & card) == 0x100F))
    {
        isStraight = true;
        high = RANK.Five;
    }

    return card;

Upvotes: 0

Matthew Strawbridge
Matthew Strawbridge

Reputation: 20600

The following method should get the best straight hand when supplied with seven cards (including the edge case of A-5), but I haven't tested it thoroughly.

The key point is that if you sort the cards into descending order and remove any duplicate values, there are only a few possible ways for the straight to be arranged (and you only need to check the extremities):

  • If the first and fifth cards are four apart, they represent the highest straight (since we know the cards between them have no duplicate values).
  • The same is true, in order, for the second and sixth cards and for the third and seventh cards (if there are that many unique values left).
  • The only other possibility is if we have an Ace at the start of the sorted list and the cards Five to Two at the end, representing a A-5 straight.

Here's the code:

public static IEnumerable<Card> GetBestStraight(IEnumerable<Card> sevenCards)
{
    if (sevenCards.Count() != 7)
    {
        throw new ArgumentException("Wrong number of cards", "sevenCards");
    }

    List<Card> ordered = sevenCards.OrderByDescending(c => c.Val).ToList();
    List<Card> orderedAndUnique = ordered.Where((c, i) => i == 0 || ordered[i].Val != ordered[i - 1].Val).ToList();

    if (orderedAndUnique.Count < 5)
    {
        // not enough distinct cards for a straight
        return Enumerable.Empty<Card>();
    }

    if (orderedAndUnique[0].Val == orderedAndUnique[4].Val + 4)
    {
        // first five cards are a straight
        return orderedAndUnique.Take(5);
    }
    else if (5 < orderedAndUnique.Count && orderedAndUnique[1].Val == orderedAndUnique[5].Val + 4)
    {
        // next five cards are a straight
        return orderedAndUnique.Skip(1).Take(5);
    }
    else if (6 < orderedAndUnique.Count && orderedAndUnique[2].Val == orderedAndUnique[6].Val + 4)
    {
        // last five cards are a straight
        return orderedAndUnique.Skip(2).Take(5);
    }

    // if there's an A-5 straight, the above won't have found it (because Ace and Two are not consecutive in the enum)
    if (orderedAndUnique[0].Val == CARDS.Ace && orderedAndUnique[orderedAndUnique.Count - 4].Val == CARDS.Five)
    {
        return orderedAndUnique.Where(c => c.Val == CARDS.Ace || c.Val <= CARDS.Five);
    }

    return Enumerable.Empty<Card>();
}

Upvotes: 0

Charles Lambert
Charles Lambert

Reputation: 5132

Use the Aggregate method. You can modify the example code below to return various lengths of card lists, and change the while clause to check for the amount of cards that must match. (e.g. my version checks for Count == 5, you could check for Count >= 5, etc.)

public class Card {
    public CARDS Val { get; set; }
    public SUITS Suit { get; set; }

    // added ToString for program below
    public override string ToString() {
        return string.Format("{0} of {1}", Val, Suit);
    }
}

class Program {

    static IEnumerable<Card> RandomList(int size) {
        var r = new Random((int)DateTime.Now.Ticks);
        var list = new List<Card>();
        for (int i = 0; i < size; i++) {
            list.Add(new Card {
                Suit = (SUITS)r.Next((int)SUITS.Diamonds, (int)SUITS.Spades),
                Val = (CARDS)r.Next((int)CARDS.Two, (int)CARDS.Ace)
            });
        }
        return list.OrderBy(c => c.Val);
    }

    // generates a random list of 5 cards untill
    // the are in sequence, and then prints the
    // sequence
    static void Main(string[] args) {

        IEnumerable<Card> consecutive = null;

        do {
            // generate random list
            var hand = RandomList(5);

            // Aggreate:
            // the passed in function is run for each item
            // in hand. acc is the accumulator value.
            // It is passed in to each call. The new List<Card>()
            // parameter is the initial value of acc when the lambda
            // is called on the first item in the list

            // in the lambda we are checking to see if the last
            // card in the accumulator value is one less
            // than the current card. If so, add it to the
            // accumulator, otherwise do not.
            consecutive = hand.Aggregate(new List<Card>(), (acc, card) => {
                var size = acc.Count != 0
                    ? ((int)card.Val) - ((int)acc[acc.Count - 1].Val)
                    : 1;
                if (size == 1)
                    acc.Add(card);
                return acc;
            });
        } while (consecutive.Count() != 5);
        foreach (var card in consecutive) {
            Console.WriteLine(card);
        }
        Console.ReadLine();
    }
}

Upvotes: 0

Adam S
Adam S

Reputation: 3125

If you are interested in getting the subset of cards from cardList that has the highest range of sequential card values, regardless of suit, consider this approach.

//Extension method to find a subset of sequential consecutive elements with at least the specified count of members.
//Comparisions are based on the field value in the selector.
//Quick implementation for purposes of the example...
//Ignores error and bounds checking for purposes of example.  
//Also assumes we are searching for descending consecutive sequential values.
public static IEnumerable<T> FindConsecutiveSequence<T>(this IEnumerable<T> sequence, Func<T, int> selector, int count)
{
    int start = 0;
    int end = 1;
    T prevElement = sequence.First();

    foreach (T element in sequence.Skip(1))
    {
        if (selector(element) + 1 == selector(prevElement))
        {
            end++;
            if (end - start == count)
            {
                return sequence.Skip(start).Take(count);
            }
        }
        else
        {
            start = end;
            end++;
        }

        prevElement = element;
    }
    return sequence.Take(0);
}


//Compares cards based on value alone, not suit.
//Again, ignores validation for purposes of quick example.
public class CardValueComparer : IEqualityComparer<Card>
{
    public bool Equals(Card x, Card y)
    {
        return x.Val == y.Val ? true : false;
    }
    public int GetHashCode(Card c)
    {
        return c.Val.GetHashCode();
    }
}

Given the above, the approach would be to first sort the cards based on the value of the card, not the suit, giving you the cards in descending order. Then create a subset of the distinct cards, again based only on card value, not suit. Then call into FindConsecutiveSequence specifying the Val property for comparison and the amount of elements you need for a valid sequence.

//Sort in descending order based on value of the card.
cardList.Sort((x,y) => y.Val.CompareTo(x.Val));

//Create a subset of distinct card values.
var distinctCardSet = cardList.Distinct(new CardValueComparer());

//Create a subset of consecutive sequential cards based on value, with a minimum of 5 cards.
var sequentialCardSet = distinctCardSet.FindConsecutiveSequence(p => Convert.ToInt32(p.Val), 5);

I think this should cover what you asked in the question and give you something to build on. However, if this if for poker, this logic will fail in the case where Ace can be a low value -> {A,2,3,4,5}. I didn't see mention of Ace specific logic needed, so perhaps you handle it outside of the scope of the question.

Upvotes: 3

Chuck Savage
Chuck Savage

Reputation: 11945

Is this what you want?

First a random list of suits, but sequential list of cards values

Random random = new Random((int)(DateTime.Now.ToBinary() % Int32.MaxValue));
List<Card> hand = new List<Card>();

for(int card = (int)CARDS.Five;card <= (int)CARDS.Nine;card++)
{
    SUIT suit = (SUITS)(random.Next(4)+1);
    hand.Add(new Card { Suit = suit, Val = (CARDS)card });
}

Or a sequential list of suits would be...

for(int card = (int)CARDS.Five, int suit = (int)SUITS.Diamonds;card <= (int)CARDS.Nine;card++, suit++)
{
    if(suit > (int)SUITS.Spades)
        suit = (int)SUITS.Diamonds;
    hand.Add(new Card { Suit = (SUITS)suit, Val = (CARDS)card });
}

Upvotes: 0

Jahan Zinedine
Jahan Zinedine

Reputation: 14864

Order the Cards by Number then Take 1 and Skip 3 of them to select what you want.

Upvotes: 1

Related Questions