First off, since the last post, a new condition for the game was added: passing. Passing occurs right after the hands are dealt but before the first player makes their move, wherein both players trade a single card to each other.
Now then, much like with Minesweeper, all the AI logic used here is entirely my own, which feels more natural given that Minesweeper makes heavy use of algorithms, where Gin is more of a weighted system when determining which card you want to pick up or discard.
I won’t include a results section, as there’s no real test I can run purely against my own AI. Every time I want to play, I have to play against another classmate’s AI (or the really awful default AI), and my success can only be measured in comparison to theirs.
That said, my initial testing (and I tested against every single AI available to me) was fairly positive. I did very well against most people, with two individuals (Alan, who took first place in Minesweeper, and James, who placed first in the initial dry run (which to be fair is very preliminary, many AIs were not ready at the time)) I measured specifically against. With Alan, his AI is all over the place, and I’m not sure why. One series of games it would go from doing very well to doing very poorly, with no set pattern. James was more consistent, but in my favor, I was able to beat him 4 out of 5 times.
For this post, partly because of length, but mainly because posting code feels rather pointless (It’s our professor’s custom built engine, so while readable, copy-pasting would be useless), I’m just going to cover the step by step process I take through playing Gin.
Disclaimer: I will include parts that are not in use (but if I improved the AI further might’ve been). This AI is not fully optimized, it does not take probability into occurrence, nor does it find pairs among two separated cards (5 of spades and 7 of spades are not considered special by the AI unless a 6 of spades appears).
Notable Variables, Structs, & Functions
- UINT myPlayerNum, keeps track of the player number (wow who could’ve guessed)
- bool justReset, boolean for determining when the game was just reset, gets turned on once and then off in the first draw phase after an update
- vector<Card> hasBeenSeen, a master list of all cards seen (Does update, but is not used; could be for probability)
- vector<SetOrRun> lockedSORs, a list of all sets or runs, all cards in this list are not allowed to leave the hand
- vector<Pair> heldPairs, a list of all pairs (both of potential runs and sets), cards in this list are only discarded if there are no “single” cards
- vector<PCard> handPriority, a list of all cards in hand, each is given a priority number of 0, 1, or 2 (high to low priority)
- Pair, keeps track of 2 cards, whether they’re the start of a set or run, and overloads the == operator to compare pairs
- SetOrRun, keeps track of 3+ cards, whether they’re part of a set or run, and overloads the == operator to compare SORs
- PCard, a struct that holds a card and a priority number
- bool completesOrAddsToSetOrRun(Card c), determines whether a given card completes or adds to a pair or a SOR, true if it does
- void cullDiscardFromPairs(Card c), removes all pairs that contain the discarded card from heldPairs
- Card getCardToDiscard(), browses the hand and finds the highest priority card (a 2 if possible) with the highest value
- void setHandPriority(Hand h), adds all cards in hand to handPriority with an associated priority number, clears the previous handPriority list when called
Step by Step
- Upon resetting the game
- Clear all lists (hasBeenSeen, lockedSORs, heldPairs, handPriority)
- Add the discard to hasBeenSeen
- Set justReset to true
- Determine passing step
- updateOnReset, which:
- Sets the player number
- Adds your hand to hasBeenSeen
- Finds any sets or runs in hand and adds them to lockedSORs
- Finds any pairs and adds them to heldPairs (includes pairs that are apart of SORs)
- Removes any pairs that are in SORs (so as to not trick your own system)
- Set hand priority
- Get the card to discard
- Cull the discard card from pairs
- Return the chosen card
- When it comes to your turn, determine whether you’ll draw from the deck or discard
- Update, which does everything that updateOnReset does except for setting the player number. However, it also checks for duplicates on each list, and does not add any cards to hasBeenSeen, pairs to heldPairs, or SORs to lockedSORs if they already exist within the list. It also assures that if pickedUpInitialDiscard is true, then do not access the top of the discard (because there is none)
- If completesOrAddsToSetOrRun is true, take the discard. Otherwise take the deck
- Flip a few reset variables (will only happen once)
- If justReset is true and you chose the discard, flip pickedUpInitialDiscard to true. This was a fix for this particular system, but it avoids the issue in the next update wherein you check the top card of the discard, and if its empty (because you just picked it up), it won’t throw an error
- If justReset is false and pickedUpInitialDiscard is true, flip pickedUpInitialDiscard back to false
- If justReset is true, flip it to false
- Draw your card
- Now you must discard
- Update to redo calculations for your newly drawn card
- Get the card to discard
- Remove that card from your hand and score your hand. If you are under the knock threshold, take the knock (set to true), doesn’t matter what value (our default value is 10)
- Cull the discard from pairs
- Discard your card
- Repeat steps 3 and 4 until the game ends
It’s a lot more compact that I expected to write, but there’s a lot of backend functionality that doesn’t warrant a spot on the list.
Functions that determine sets and runs from your hand, pairs, checks for duplicates among lists, checks the best form of scoring (whether it be from taking the 5 of hearts, 5 of diamonds and 5 of clubs, or the 5 of clubs, 6 of clubs, and 7 of clubs as your set/run), breaks apart a list of cards returned as SORs into the actual SOR struct, a lot goes on to actually run the base update function, which is where most of the information cataloging is done.
The deterministic functions, which get the card to discard or checks if the card completes a set or run end up using those lists of information to check whether they want a given card or not.
I predicted it last time, and I’ll be bold and predict it again: I think my AI will do well. I’m quietly hoping that it will place first this time, but just as Alan dark horsed his way in last time, there’s always that possibility that someone will have a strong AI that gives challenge.
I also don’t have a unique algorithm that’s leading the charge, just a series of logical choices that I as a player would make (albeit as a player I might use more nuance in some decisions, but god damn if nuance isn’t the trickiest thing to program), so I feel less confident that I will do well this time (but still fairly confident).
I guess we’ll just have to see how it all shakes down tomorrow or Friday, whenever the actual competition is. Till the next post, thanks for reading.