ConMan
ConMan watches every card that goes through his hand, calling BS when a play isn't possible due to where the cards are.
Plays truth when able, but lies intelligently using the last card if a victory were to happen.
I spent a long time tuning a technique to call BS when the probability was high that the opponent was lying, or when calling BS was beneficial (such as getting useful cards from the discard pile), but in practice, not calling BS at all netted me the most points.
package players;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import controller.*;
import java.text.DecimalFormat;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Iterator;
public class PlayerConMan extends Player {
private enum Location {
PLAYER_0,
PLAYER_1,
PLAYER_2,
PLAYER_3,
DISCARD,
UNKNOWN
};
private class MyCard {
private final int number;
private Location location;
private double confidence;
protected Card card;
public MyCard(int x) {
this.number = x;
location = Location.UNKNOWN;
confidence = 1.0;
}
@Override
public String toString() {
if (confidence > 0.75) {
return ""+number;
} else if (confidence > 0.25) {
return number+"*";
} else {
return number+"_";
}
}
}
private final ArrayList<ArrayList<MyCard>> theDeck = new ArrayList();
private Location myLocation;
private ArrayList<Player> players;
private final ArrayList<MyCard> myHand = new ArrayList();
private final HashMap<Location, Integer> sizes = new HashMap();
private ArrayList<Integer> lies = new ArrayList();
private ArrayList<Integer> truths = new ArrayList();
// Constructor
public PlayerConMan() {
for (int i = 0; i < 13; ++i) {
ArrayList<MyCard> set = new ArrayList();
for (int j = 0; j < 4; ++j) {
set.add(new MyCard(i));
}
theDeck.add(set);
}
sizes.put(Location.PLAYER_0, 13);
sizes.put(Location.PLAYER_1, 13);
sizes.put(Location.PLAYER_2, 13);
sizes.put(Location.PLAYER_3, 13);
sizes.put(Location.DISCARD, 13);
sizes.put(Location.UNKNOWN, 39);
}
//Gets the MyCard for this card, updating a MyCard with the lowest confidence if not already created
private MyCard getCard(Card c) {
ArrayList<MyCard> set = theDeck.get(c.getNumber());
MyCard unknown = null;
double confidence = 1.0;
for (MyCard m : set) {
if (m.card == c) {
return m;
}
if (m.card == null) {
if (m.location == Location.UNKNOWN) {
unknown = m;
confidence = 0.0;
} else if (m.confidence < confidence || unknown == null) {
unknown = m;
confidence = m.confidence;
}
}
}
unknown.card = c;
return unknown;
}
//Returns the Location of a player
private Location getLocation(Player p) {
return Location.values()[players.indexOf(p)];
}
@Override
protected void initialize(Controller controller) {
super.initialize(controller);
players = new ArrayList(controller.getPlayers());
for (Player p : players) {
if (p == this) {
myLocation = getLocation(p);
}
}
for (Location loc : Location.values()) {
sizes.put(loc, 0);
}
}
private ArrayList<Integer>[] getTruthesAndLies(Player player, int card, ArrayList<MyCard> myHand) {
//Determine our next plays
int offset = players.indexOf(player);
int myOffset = players.indexOf(this);
int nextCard = (card + (myOffset - offset + 4) % 4)%13;
ArrayList<Integer> truths = new ArrayList();
ArrayList<Integer> lies = new ArrayList();
ArrayList<MyCard> cardsLeft = new ArrayList(myHand);
while (!cardsLeft.isEmpty()) {
boolean isLie = true;
Iterator<MyCard> it = cardsLeft.iterator();
while (it.hasNext()) {
MyCard m = it.next();
if (m.number == nextCard) {
it.remove();
isLie = false;
}
}
if (isLie) {
lies.add(nextCard);
} else {
truths.add(nextCard);
}
nextCard = (nextCard + 4)%13;
}
return new ArrayList[]{truths, lies};
}
private void updateDeck(Player player, int card, int numberOfCards, Controller controller) {
Location loc = getLocation(player);
//Update from BS
if (sizes.get(Location.DISCARD) + numberOfCards != controller.getDiscardPileSize()) {
//Move all cards from DISCARD to the losing player
// Losing player defaults to player playing, in the rare case of a tie
Location losingPlayer = loc;
Location winningPlayer = null;
for (Player p : players) {
Location pLoc = getLocation(p);
int size = p.handSize();
if (pLoc == loc) size += numberOfCards;
if (p.handSize() > sizes.get(pLoc)) {
losingPlayer = pLoc;
} else if (size < sizes.get(pLoc)) {
winningPlayer = pLoc;
}
}
if (winningPlayer == null) {
debug(losingPlayer+" lost a BS");
} else {
debug(losingPlayer+" lied and "+winningPlayer+" lost a card");
}
//Move the cards from the discard to the player
ArrayList<MyCard> winnersHand = new ArrayList();
for (ArrayList<MyCard> set : theDeck) {
for (MyCard m : set) {
if (m.location == Location.DISCARD) {
if (losingPlayer == myLocation) {
//If we lost, update the discard cards to unknown;
// They'll be updated when we look at our hand
m.location = Location.UNKNOWN;
m.confidence = 1.0;
} else {
//Move to the losing player
m.location = losingPlayer;
}
} else if (m.location == myLocation && winningPlayer == myLocation) {
//Update our old cards to the discard pile, in case we won
m.location = Location.DISCARD;
m.confidence = 1.0;
} else if (m.location == winningPlayer) {
//Add the card to the winner's hand for later processing
winnersHand.add(m);
}
}
}
//If someone else won, adjust the probabilities on their cards
if (winningPlayer != myLocation && winningPlayer != null) {
int winningSize = players.get(winningPlayer.ordinal()).handSize();
if (winningPlayer == loc) winningSize += numberOfCards;
for (MyCard m : winnersHand) {
m.confidence *= 1-(1/winningSize);
}
}
}
sizes.put(Location.DISCARD, controller.getDiscardPileSize());
//Update player handSize
for (Player p : players) {
sizes.put(getLocation(p), p.handSize());
}
//Detect if my hand size has changed to speed processing
if (myHand.size() != handSize()) {
//Update values from my hand
myHand.clear();
for (Card c : getHand()) {
MyCard m = getCard(c);
m.location = myLocation;
m.confidence = 1.0;
myHand.add(m);
}
//Determine our next plays
ArrayList<Integer> tl[] = getTruthesAndLies(player, card, myHand);
truths = tl[0];
lies = tl[1];
debug("Truthes: "+truths);
debug("Lies: "+lies);
}
}
@Override
protected List<Card> requestCards(int card, Controller controller) {
updateDeck(this, card, 0, controller);
ArrayList<Card> ret = new ArrayList();
int pick = card;
boolean all = true;
if (truths.get(0) != card) {
pick = truths.get(truths.size()-1);
all = false;
}
for (MyCard m : myHand) {
if (m.number == pick) {
m.location = Location.DISCARD;
ret.add(m.card);
if (!all) break;
}
}
sizes.put(Location.DISCARD, controller.getDiscardPileSize() + ret.size());
sizes.put(myLocation, myHand.size() - ret.size());
printTheDeck();
return ret;
}
@Override
protected boolean bs(Player player, int card, int numberOfCards, Controller controller) {
updateDeck(player, card, numberOfCards, controller);
Location loc = getLocation(player);
//Get total number of unknown cards and total number of cards the player must have
int handSize = player.handSize() + numberOfCards;
ArrayList<MyCard> playerHand = new ArrayList();
ArrayList<MyCard> discardPile = new ArrayList();
double totalUnknown = 0;
double playerUnknown = handSize;
double cardsHeld = 0;
double cardsNotHeld = 0;
for (ArrayList<MyCard> set : theDeck) {
for (MyCard m : set) {
if (m.location == Location.UNKNOWN) {
totalUnknown++;
} else if (m.location == loc) {
playerHand.add(m);
playerUnknown -= m.confidence;
totalUnknown += 1.0 - m.confidence;
if (m.number == card) {
cardsHeld += m.confidence;
}
} else {
if (m.location == Location.DISCARD) {
discardPile.add(m);
}
totalUnknown += 1.0 - m.confidence;
if (m.number == card) {
cardsNotHeld += m.confidence;
}
}
}
}
boolean callBS = false;
double prob;
int possible = (int)Math.round(4-cardsNotHeld);
int needed = (int)Math.round(numberOfCards - cardsHeld);
if (needed > possible) {
//Player can't possibly have the cards
prob = 0.0;
debug("impossible");
callBS = true;
} else if (needed <= 0) {
//Player guaranteed to have the cards
prob = 1.0;
debug("guaranteed");
} else {
//The probability that player has needed or more of the possible cards
double successes = 0;
for (int i = (int)needed; i <= (int)possible; i++) {
successes += choose(possible, i) * choose(totalUnknown-possible, playerUnknown-i);
}
double outcomes = choose(totalUnknown, playerUnknown);
prob = successes / outcomes;
if (Double.isNaN(prob)) {
prob = 0;
callBS = true;
}
debug("prob = "+new DecimalFormat("0.000").format(prob));
}
//Update which cards they may have put down
// Assume they put down as many as they could truthfully
int cardsMoved = 0;
Iterator<MyCard> it = playerHand.iterator();
while (it.hasNext()) {
MyCard m = it.next();
if (m.number == card) {
it.remove();
m.location = Location.DISCARD;
discardPile.add(m);
cardsMoved++;
if (cardsMoved >= numberOfCards) {
break;
}
}
}
//We can't account for all the cards they put down
// Adjust existing probabilities and move our lowest confidence cards to the discard
if (cardsMoved < numberOfCards) {
// Reduce the confidence of all remaining cards, in case they lied
// Assumes they lie at random
double cardsLeft = handSize-cardsMoved;
double cardsNeeded = numberOfCards-cardsMoved;
double probChosen = 1 * choose(cardsLeft-1, cardsNeeded-1) / choose(cardsLeft, cardsNeeded);
if (Double.compare(cardsLeft, cardsNeeded) == 0) {
//They're gonna win, call their bluff
callBS = true;
for (MyCard m : playerHand) {
m.location = Location.DISCARD;
}
} else {
for (MyCard m : playerHand) {
m.confidence *= (1-probChosen) * (1-prob) + prob;
}
}
// Move any UNKNOWN cards they could have played, assuming they told the truth
Collections.sort(theDeck.get(card), new Comparator<MyCard>() {
@Override
public int compare(MyCard o1, MyCard o2) {
double p1 = o1.confidence - (o1.location == Location.UNKNOWN ? 10 : 0);
double p2 = o2.confidence - (o2.location == Location.UNKNOWN ? 10 : 0);
return (int)Math.signum(p1-p2);
}
});
for (MyCard m : theDeck.get(card)) {
if (m.location == Location.UNKNOWN || m.confidence < prob) {
m.location = Location.DISCARD;
m.confidence = prob;
cardsMoved++;
discardPile.add(m);
if (cardsMoved >= numberOfCards) break;
}
}
}
//Get the confidence of the discardPile
double discardPileConfidence = 1.0;
for (MyCard m : discardPile) {
discardPileConfidence *= m.confidence;
}
discardPileConfidence *= Math.pow(0.5, controller.getDiscardPileSize() - discardPile.size());
//Call BS if the cards in the discard pile consists only of cards we need / will play
if (discardPileConfidence > 0.5 && discardPile.size() == controller.getDiscardPileSize()) {
double truthCount = 0;
double lieCount = 0;
double unknownCount = 0;
for (MyCard m : discardPile) {
if (truths.contains(m.number)) {
truthCount += m.confidence;
unknownCount += 1-m.confidence;
} else if (lies.contains(m.number)) {
lieCount += m.confidence;
unknownCount += 1-m.confidence;
} else {
unknownCount += 1;
break;
}
}
if (lieCount > 0 && unknownCount < 1) {
debug("Strategic BS");
//callBS = true;
}
}
//What's the worst that could happen?
//Test the decks'
ArrayList<MyCard> worstHand = new ArrayList<MyCard>(myHand);
worstHand.addAll(discardPile);
ArrayList<Integer> loseCase[] = getTruthesAndLies(player, card, worstHand);
int winPlaysLeft = truths.size() + lies.size();
int losePlaysLeft = loseCase[0].size() + loseCase[1].size();
double randomPlaysLeft = Math.max(losePlaysLeft,7);
double expectedPlaysLeft = losePlaysLeft * discardPileConfidence + randomPlaysLeft * (1-discardPileConfidence);
double threshold = 0.0 - (expectedPlaysLeft - winPlaysLeft)/13.0;
debug("winPlaysLeft = "+winPlaysLeft);
debug("expectedPlaysLeft = "+expectedPlaysLeft);
debug("Threshold = "+threshold);
if(lies.isEmpty()) {
threshold /= 2;
}
//callBS = callBS || prob < threshold;
printTheDeck();
return callBS;
}
static double logGamma(double x) {
double tmp = (x - 0.5) * Math.log(x + 4.5) - (x + 4.5);
double ser = 1.0 + 76.18009173 / (x + 0) - 86.50532033 / (x + 1)
+ 24.01409822 / (x + 2) - 1.231739516 / (x + 3)
+ 0.00120858003 / (x + 4) - 0.00000536382 / (x + 5);
return tmp + Math.log(ser * Math.sqrt(2 * Math.PI));
}
static double gamma(double x) {
return Math.exp(logGamma(x));
}
static double factorial(double x) {
return x * gamma(x);
}
static double choose(double n, double k) {
if (Double.compare(n, k) == 0 || Double.compare(k, 0) == 0) return 1.0;
if (k < 0 || k > n) {
return 0.0;
}
return factorial(n) / (factorial(n-k) * factorial(k));
}
public String toString() {
return "ConMan";
}
public void printTheDeck() {
HashMap<Location, ArrayList<MyCard>> map = new HashMap();
for (Location loc : Location.values()) {
map.put(loc, new ArrayList());
}
for (ArrayList<MyCard> set : theDeck) {
for (MyCard m : set) {
map.get(m.location).add(m);
}
}
String ret = "";
for (Player p : players) {
ret += p.toString()+": "+map.get(getLocation(p))+"\n";
}
ret += "Discard pile: "+map.get(Location.DISCARD)+"\n";
ret += "Unknown: ("+map.get(Location.UNKNOWN).size()+" cards)\n";
debug(ret);
}
public void debug(Object s) {
}
}
13You have got to be kidding me. I have an almost finished controller for BS lying around for the sole purpose of creating this exact challenge. Well, I guess I have to find some other way to spend my time now. – IchBinKeinBaum – 2015-04-06T01:22:04.017
3It might be advisable not to expose
Controller.toString()
to public, as it returns the hands of all players and the discard pile. – es1024 – 2015-04-06T04:20:09.717@IchBinKeinBaum, if your controller can communicate with STDIN/STDOUT, you might consider publishing the challenge with your controller for all the non-Java people. – Logic Knight – 2015-04-06T05:28:44.197
@CarpetPython: It does. It also uses slightly differen rules. If that doesn't count as a duplicate, I will. – IchBinKeinBaum – 2015-04-06T11:31:43.250
I just finished creating a multi-language controller. The usage is in Program.cs. You can find it here.
– LegionMammal978 – 2015-04-06T13:31:08.630@LegionMammal978 That's pretty cool. I'll check it out – soktinpk – 2015-04-06T15:45:06.000
Can you post some of the methods that aren't there? How do I see the size of the discard pile? How do I know who took the pile if BS is called? How do I know what cards they played if it was BS (usually in a real game, you flip them to see if it was BS or not.) – mbomb007 – 2015-04-06T18:06:37.840
@mbomb007 The size of the discard pile is
controller.getDiscardPileSize()
(controller
is the last argument). – soktinpk – 2015-04-06T19:48:52.430@mbomb007 I'll look into making a method that tells you who took the pile if BS was called. – soktinpk – 2015-04-06T20:10:34.073
@mbomb007 You can always deduce who took the discard pile after a BS based on the hand size of all the players (public information). – soktinpk – 2015-04-06T20:24:13.913
Thanks, I didn't have time to look at the controller or anything, just the description above. I'll try to improve my program within the next couple days, but may not get time before then. Also, do you keep track of the cards? I couldn't "construct" a
new Card()
and return it, right? You might want to think if this is possible, and it shouldn't be. – mbomb007 – 2015-04-06T21:54:06.450@mbomb007 I think the
Card
constructor is visible only to the package, and all players should be in the other package. I don't think it's possible to construct anew Card()
without reflection (but I may be wrong -- I'm not great with all the visibility stuff in Java) – soktinpk – 2015-04-06T22:02:25.580You really need to add a method to the Player class that is called after each round called
void update(Controller controller)
or something. Maybe one forinitialize()
that is called once at the start. It would definitely be helpful. – mbomb007 – 2015-04-08T01:02:20.823@mbomb007 Done. Sorry for taking so long – soktinpk – 2015-04-08T01:09:32.443
@soktinpk Just updated my controller to be compliant with your changes, pushed it to the repository – LegionMammal978 – 2015-04-08T12:06:02.123
Error on
Controller.java
line 98-99: only the first card is checked for BS. Also, only the cards are not checked against the deck, so it's possible for a player to accidentally play duplicate cards, or even cards she doesn't have. – Wasmoo – 2015-04-09T15:05:56.327@Manu Hmm.. that's strange. Can you supply a seed to the
Random
at the very beginning of the program that reproduces the problem and tell us what seed you used? – soktinpk – 2015-04-09T19:10:07.570@Wasmoo Woops. Fixed first issue. However, if a player plays duplicate cards, they will be eliminated when I wrap it in a
HashSet
. Also, theCard
constructor isn't public, but I guess it's still possible to return a card you don't have by saving a reference to a card, playing it, and playing it again. I'll add a check. – soktinpk – 2015-04-09T19:12:34.247