/** * Copyright (C) 2017 Jan Schäfer (jansch@users.sourceforge.net) * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.jskat.ai.mjl; import java.util.Random; import org.jskat.player.ImmutablePlayerKnowledge; import org.jskat.util.Card; import org.jskat.util.CardList; import org.jskat.util.GameType; import org.jskat.util.Player; import org.jskat.util.Rank; import org.jskat.util.Suit; import org.jskat.util.rule.NullRule; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * @author Markus J. Luzius (markus@luzius.de) * */ public class OpponentPlayer extends AbstractCardPlayer { /** * log */ private static Logger log = LoggerFactory.getLogger(OpponentPlayer.class); private final String name; private final Random rand = new Random(); /** * */ OpponentPlayer(final CardList cards, final String name) { super(cards); this.name = name; log.debug("Constructing a new opponent player called <" + name + ">..."); } /** * Gets the next card that the player wants to play * * @param knowledge * all necessary information about the game * @return index of the card to play * @see org.jskat.ai.mjl.CardPlayer#playNextCard(ImmutablePlayerKnowledge) */ @Override public Card playNextCard(final ImmutablePlayerKnowledge knowledge) { log.debug("Play next card with trick size " + knowledge.getTrickCards().size()); if (knowledge.getGameType() == GameType.NULL) { return playNextCardNullGame(knowledge); } int bestToBePlayed = -1; log.debug(".playNextCard(): Processing hand [" + cards + "] with trick [" + knowledge.getTrickCards() + "]. Game type is " + knowledge.getGameType() + "."); if (knowledge.getTrickCards().size() > 1) { bestToBePlayed = findRearhandCard(knowledge); } else if (knowledge.getTrickCards().size() > 0) { bestToBePlayed = findMiddlehandCard(knowledge); } else { // No card on the table yet if (knowledge.getNoOfTricks() < 1) { bestToBePlayed = findFirstInitial(knowledge); } else { bestToBePlayed = findInitial(knowledge); } log.debug(".playNextCard(): (in forehand) " + name + ": " + cards.get(bestToBePlayed)); } if (bestToBePlayed < 0 || bestToBePlayed > cards.size() - 1) { log.debug("----- Error in finding a good opponent card: " + bestToBePlayed + " -----"); bestToBePlayed = 0; } log.debug("Playing " + cards.get(bestToBePlayed)); if (bestToBePlayed < 0) { log.warn("Can't find a suitable card!"); return null; } return cards.remove(bestToBePlayed); } /** * @param knowledge * @return the index of the card to play by a middlehand opponent player */ private int findMiddlehandCard(final ImmutablePlayerKnowledge knowledge) { GameType gameType = knowledge.getGameType(); Card initialCard = (knowledge.getTrickCards().size() > 0 ? knowledge .getTrickCards().get(0) : null); Suit trumpSuit = gameType.getTrumpSuit(); int bestToBePlayed; // Only one card is played in this trick yet // the player is forced to play after the color // of the first card // At first: Is trump played? if (initialCard.getSuit() == trumpSuit || initialCard.getRank() == Rank.JACK) { // Trump is played log.debug(".playNextCard(): first card is trump..."); // Does the player have trump? if (Helper.hasTrump(cards, trumpSuit)) { // Play the highest trump // Check whether there is a Jack in the cards or not if (cards.contains(Card.CJ)) { bestToBePlayed = cards.getIndexOf(Card.CJ); } else if (cards.contains(Card.SJ)) { bestToBePlayed = cards.getIndexOf(Card.SJ); } else if (cards.contains(Card.HJ)) { bestToBePlayed = cards.getIndexOf(Card.HJ); } else if (cards.contains(Card.DJ)) { bestToBePlayed = cards.getIndexOf(Card.DJ); } else { // No Jack in the cards log.debug(".playNextCard(): ... but i don't have a jack..."); bestToBePlayed = cards.getLastIndexOfSuit(trumpSuit, false); } } else { // Player doesn't have trump // it doesn't matter what card is played // just play the first card in the CardList bestToBePlayed = findLowCard(cards, trumpSuit); log.debug(".playNextCard(): ... but i don't have any trumps..."); } } else { log.debug(".playNextCard(): first card is a color card..."); // If trump is not played the player is forced // to play the color of the first card if (cards.hasSuit(gameType, initialCard.getSuit())) { // Player has the color // check if it's ours or if I can beat if (Helper.isSinglePlayerWin(knowledge)) { bestToBePlayed = Helper.isAbleToBeat(cards, initialCard, initialCard, gameType); if (bestToBePlayed < 0) { log.debug(".playNextCard(): ...which i can't beat..."); bestToBePlayed = cards.getLastIndexOfSuit( initialCard.getSuit(), false); } } else { // Play the card with the highest value log.debug(".playNextCard(): ...to which i try to put some value..."); bestToBePlayed = cards.getFirstIndexOfSuit( initialCard.getSuit(), false); } } else { log.debug(".playNextCard(): ...which i don't have..."); // Player doesn't have the color // is there any trump in the cards if (Helper.hasTrump(cards, trumpSuit)) { // Play the highest trump // Check whether there is a Jack in the cards or not if (cards.contains(Card.CJ)) { bestToBePlayed = cards.getIndexOf(Card.CJ); } else if (cards.contains(Card.SJ)) { bestToBePlayed = cards.getIndexOf(Card.SJ); } else if (cards.contains(Card.HJ)) { bestToBePlayed = cards.getIndexOf(Card.HJ); } else if (cards.contains(Card.DJ)) { bestToBePlayed = cards.getIndexOf(Card.DJ); } else { // No Jack in the cards bestToBePlayed = cards.getLastIndexOfSuit(trumpSuit, false); } log.debug(".playNextCard(): ...so i take it..."); } else { // it doesn't matter what card is played // possible improvement: check card value! bestToBePlayed = cards.size() - 1; log.debug(".playNextCard(): ...but i can't take it..."); } } } log.debug(".playNextCard(): player " + name + ": " + cards.get(bestToBePlayed)); return bestToBePlayed; } /** * @param knowledge * @return the card to play by a rearhand opponent player */ private int findRearhandCard(final ImmutablePlayerKnowledge knowledge) { GameType gameType = knowledge.getGameType(); Card initialCard = (knowledge.getTrickCards().size() > 0 ? knowledge .getTrickCards().get(0) : null); Suit trumpSuit = gameType.getTrumpSuit(); int bestToBePlayed; // I'm in rearhand // 1: check if player can match initial suit if (Helper.isAbleToMatch(cards, initialCard, gameType)) { // 1.1 if yes (i.e. I can match the initial suit): check if // necessary to beat log.debug(".playNextCard(): I can match the demanded color"); if (Helper.isSinglePlayerWin(knowledge)) { log.debug(".playNextCard(): Trick is SinglePlayerWin"); // 1.1.1: if yes: can beat? Card currentWinner; if (knowledge.getTrickCards().get(1) .beats(gameType, initialCard)) { currentWinner = knowledge.getTrickCards().get(1); } else { currentWinner = initialCard; } bestToBePlayed = Helper.isAbleToBeat(cards, currentWinner, initialCard, gameType); if (bestToBePlayed > -1) { // 1.1.1.1: if I can beat: do it log.debug(".playNextCard(): ...but I can beat it..."); } else { log.debug(".playNextCard(): ...which I can't beat..."); // 1.1.1.2: if I can't beat: find lowest matching card if (initialCard.isTrump(gameType)) { bestToBePlayed = cards.getFirstIndexOfSuit( gameType.getTrumpSuit(), false); if (bestToBePlayed < 0) { log.debug(".playNextCard(): Damn! I have to play a Jack..."); bestToBePlayed = 0; while (cards.size() > bestToBePlayed + 1 && cards.get(bestToBePlayed + 1).getRank() == Rank.JACK) { bestToBePlayed++; } } } else { bestToBePlayed = cards.getFirstIndexOfSuit( initialCard.getSuit(), false); } } } else { log.debug(".playNextCard(): I can match the demanded color, and it's our trick already..."); // 1.1.2: if no: find highest matching card if (initialCard.isTrump(gameType)) { bestToBePlayed = cards.getLastIndexOfSuit( gameType.getTrumpSuit(), false); if (bestToBePlayed < 0) { log.debug(".playNextCard(): Damn! I have to play a Jack..."); bestToBePlayed = 0; while (cards.size() > bestToBePlayed + 1 && cards.get(bestToBePlayed + 1).getRank() == Rank.JACK) { bestToBePlayed++; } } } else { bestToBePlayed = cards.getLastIndexOfSuit( initialCard.getSuit(), false); } } } else { // 1.2: if no (i.e. I can't match the initial suit): is ours? if (Helper.isSinglePlayerWin(knowledge)) { // 1.2.1: if no: do I have trump? log.debug(".playNextCard(): I cannot match and trick is SinglePlayerWin"); if (cards.hasTrump(gameType)) { // 1.2.1.1: if yes: do I want to trump if (knowledge.getTrickCards().getTotalValue() > 3) { log.debug(".playNextCard(): But I can and will take it..."); // 1.2.1.1.1: if yes: find a good trump bestToBePlayed = findValuableTrump(cards, trumpSuit); } else { log.debug(".playNextCard(): I could trump, but I don't want to..."); // 1.2.1.1.2: if no: find a low card bestToBePlayed = findLowCard(cards, trumpSuit); } } else { log.debug(".playNextCard(): I'd love to have it, but I can't match the initial suit..."); // 1.2.1.2: if no: find a low card bestToBePlayed = findLowCard(cards, trumpSuit); } } else { log.debug(".playNextCard(): I cannot match but it's our trick already..."); // 1.2.2: if yes: find highest value card (but no ace) bestToBePlayed = findHighCard(cards, trumpSuit); log.debug(".playNextCard(): got back value " + bestToBePlayed); } } return bestToBePlayed; } /** * Gets the next card that the player wants to play in a null game * * @param knowledge * @return the index of the card that should be played */ private Card playNextCardNullGame(final ImmutablePlayerKnowledge knowledge) { int bestToBePlayed = -1; log.debug(".playNextCardNullGame(): cards: [" + cards + "]"); if (knowledge.getTrickCards().size() > 0) { Card initialCard = knowledge.getTrickCards().get(0); if (!cards.hasSuit(knowledge.getGameType(), initialCard.getSuit())) { // TODO null game: abwerfen log.debug(".playNextCardNullGame(): abwerfen..."); bestToBePlayed = 0; } else { // do I have a lower card? int toBePlayed = findLowerCard(cards, initialCard); if (toBePlayed < 0) { // no: take the highest one of that suit bestToBePlayed = cards.getFirstIndexOfSuit( initialCard.getSuit(), false); } else { bestToBePlayed = toBePlayed; } } } else { int toBePlayed = findInitialForNullGame(cards); log.debug(".playNextCardNullGame(): (initial for null): " + toBePlayed); bestToBePlayed = toBePlayed; } log.debug(".playNextCardNullGame(): playing: [" + cards.get(bestToBePlayed) + "]"); if (bestToBePlayed < 0) { return null; } return cards.remove(bestToBePlayed); } /** * Chooses the highest valued trump card * * @param cards * @param trump * @return index of the card */ private int findValuableTrump(final CardList cards, final Suit trump) { // should be improved: consider, which other trumps have already been // played if (cards.size() < 1) { return 0; } int highCard = 0; int index = 0; while (++index < cards.size()) { // if(cards.get(index).isTrump(GameType.SUIT, trump) && // cards.get(index).getPoints() > cards.get(highCard).getPoints()) { // highCard = index; // log.debug(" highest card set to "+index); // } } return (highCard < cards.size() ? highCard : 0); } /** * Chooses the most valuable matching card of the given suit (considering * the jacks if the suit is trump) * * @param cards * @param suit * @param trump * @return index of the card */ private int findMostValuableMatchingCard(final CardList cards, final Suit suit, final Suit trump) { if (cards.size() < 1) { return 0; } int highCard = 0; int index = 0; while (++index < cards.size()) { // if(!cards.get(index).isTrump(GameType.SUIT, trump) // && cards.get(index).getPoints() > cards.get(highCard).getPoints() // && cards.get(index).getRank() != Rank.ACE // && cards.get(index).getSuit() == suit) { // highCard = index; // } } return (highCard < cards.size() ? highCard : 0); } /** * Chooses the highest valued card that is not trump (and not an ace) * * @param cards * @param trump * @return index of the card */ private int findHighCard(final CardList cards, final Suit trump) { // TODO: add a flag whether aces should be included // or just consider the CardMemory if (cards.size() < 1) { return 0; } int highCard = 0; int index = 0; while (++index < cards.size()) { // if(cards.get(highCard).isTrump(GameType.SUIT, trump) && // !cards.get(index).isTrump(GameType.SUIT, trump) && // cards.get(index).getRank() != Rank.ACE) { // highCard = index; // } // else if(!cards.get(index).isTrump(GameType.SUIT, trump) && // cards.get(index).getPoints() > cards.get(highCard).getPoints() && // cards.get(index).getRank() != Rank.ACE) { // highCard = index; // } } return (highCard < cards.size() ? highCard : 0); } /** * Selects the first card of the hand that has no value and is not trump * * @param cards * @param trump * @return index of the card */ private int findLowCard(final CardList cards, final Suit trump) { if (cards.size() < 2) { return 0; } int lowCard = 0; int index = 0; boolean found = false; while (!found && ++index < cards.size()) { // if(!cards.get(index).isTrump(GameType.SUIT, trump) && // cards.get(index).getPoints() < cards.get(lowCard).getPoints()) { // lowCard = index; // } // if(cards.get(lowCard).getPoints() == 0) found = true; } return (index < cards.size() ? index : 0); } /** * Tries to find a lower card than the given card (should only be used in * null games) * * @param cards * @param initialCard * @return index of the card, -1 if none is found */ private int findLowerCard(final CardList cards, final Card initialCard) { int index = -1; boolean lowerCardFound = false; Card bestCard = null; NullRule rules = new NullRule(); for (int i = 0; i < cards.size(); i++) { Card currCard = cards.get(i); if (rules .isCardAllowed(GameType.NULL, initialCard, cards, currCard)) { if (bestCard == null) { // no card found yet bestCard = currCard; index = i; } else if (!lowerCardFound && currCard.getNullOrder() < bestCard.getNullOrder()) { // lower card found bestCard = currCard; index = i; lowerCardFound = true; } } } if (index == -1) { // still no good card found // --> play a random card index = rand.nextInt(cards.size()); } log.debug(".findLowerCard(): " + index); return index; } /** * Finds an initial card in a null game (not implemented yet --> just takes * the last one) * * @param cards * @return index of the card */ private int findInitialForNullGame(final CardList cards) { // TODO initial card for null game return cards.size() - 1; } /** * Finds an initial card to play (from the second trick onward) * * @param knowledge * @return index of the card */ private int findInitial(final ImmutablePlayerKnowledge knowledge) { GameType gameType = knowledge.getGameType(); int[] rating = new int[cards.size()]; // First, look for any aces that are not trump for (int x = 0; x < cards.size(); x++) { Card c = cards.get(x); if (knowledge.getDeclarer() == Player.MIDDLEHAND) { // add to the rating the number of remaining cards } else { // subtract from the rating the number of remaining cards } if (c.getRank() == Rank.ACE && c.getSuit() != gameType.getTrumpSuit()) { if (knowledge.couldHaveSuit(knowledge.getDeclarer(), c.getSuit())) { rating[x] += 20; if (knowledge.getDeclarer() == Player.MIDDLEHAND) { rating[x] += cards.getSuitCount(c.getSuit(), false); } else { rating[x] -= cards.getSuitCount(c.getSuit(), false); } } else { rating[x] -= 20; } } if (c.getRank() == Rank.JACK) { rating[x] -= 20; } if (c.getRank() == Rank.TEN && knowledge.isCardOutstanding(Card.getCard(c.getSuit(), Rank.ACE))) { rating[x] -= 40; } if (c.getRank() != Rank.JACK && c.getSuit() == gameType.getTrumpSuit()) { rating[x] -= 30; } if (!knowledge.couldHaveSuit(knowledge.getDeclarer(), c.getSuit())) { rating[x] += (10 + c.getRank().ordinal() + cards.getSuitCount( c.getSuit(), false)); } } StringBuilder sb = new StringBuilder(); int result = 0; sb.append("[" + rating[0] + "]"); for (int i = 1; i < rating.length; i++) { sb.append("[" + rating[i] + "]"); if (rating[i] > rating[result]) { result = i; } } log.debug("Rating={" + sb + "}"); return result; } /** * Finds an initial card to play on the very first trick of the game * * @param knowledge * @return index of the card */ private int findFirstInitial(final ImmutablePlayerKnowledge knowledge) { GameType gameType = knowledge.getGameType(); log.debug("Opening the game..."); // First, look for any aces that are not trump int store = -1; for (int x = 0; x < cards.size(); x++) { if (cards.get(x).getRank() == Rank.ACE) { if (cards.get(x).getSuit() != gameType.getTrumpSuit()) { if (store >= 0) { if (knowledge.getDeclarer() == Player.MIDDLEHAND && cards.getSuitCount(cards.get(x).getSuit(), false) > cards.getSuitCount( cards.get(store).getSuit(), false)) { store = x; } else if (knowledge.getDeclarer() == Player.REARHAND && cards.getSuitCount(cards.get(x).getSuit(), false) < cards.getSuitCount( cards.get(store).getSuit(), false)) { store = x; } } else { store = x; } } } } if (store > 0) { return store; } if (knowledge.getDeclarer() == Player.MIDDLEHAND) { // If you don't have any, look for longest color // "kurzer Weg, lange Farbe" Suit maxSuit = null; for (Suit s : Suit.values()) { if (maxSuit == null) { maxSuit = s; continue; } if (s.equals(gameType.getTrumpSuit())) { continue; } if (cards.getSuitCount(s, false) > cards.getSuitCount(maxSuit, false)) { maxSuit = s; } } if (cards.get(cards.getFirstIndexOfSuit(maxSuit, false)).getRank() == Rank.ACE) { return cards.getFirstIndexOfSuit(maxSuit, false); } return cards.getLastIndexOfSuit(maxSuit, false); } else { // If you don't have any, look for shortest color // "langer Weg, kurze Farbe" Suit minSuit = null; for (Suit s : Suit.values()) { if (minSuit == null) { minSuit = s; continue; } if (s.equals(gameType.getTrumpSuit())) { continue; } if (cards.getSuitCount(s, false) < cards.getSuitCount(minSuit, false)) { minSuit = s; } } if (cards.get(cards.getFirstIndexOfSuit(minSuit, false)).getRank() == Rank.ACE) { return cards.getFirstIndexOfSuit(minSuit, false); } return cards.getLastIndexOfSuit(minSuit, false); } } }