package game;
import java.io.IOException;
import java.io.Writer;
import java.text.DateFormat;
import java.text.DecimalFormat;
import java.text.DecimalFormatSymbols;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.TreeMap;
import java.util.Map.Entry;
import java.util.logging.Level;
import java.util.logging.Logger;
import util.Utils;
import com.biotools.meerkat.Action;
import com.biotools.meerkat.Card;
import com.biotools.meerkat.GameInfo;
import com.biotools.meerkat.GameObserver;
import com.biotools.meerkat.Hand;
/**
* A HandHistoryWriter observes a game and on a {@link #gameOverEvent()} writes a hand-history-file to a given {@link Writer} in Full-Till format.<br>
* Please note that the format is not 100% FullTilt, but shortened to the most important information that importers (like PokerTracker and HoldemManager) need
* for correct processing.
*/
public class HandHistoryWriter implements GameObserver, HoleCardsObserver {
Logger log = Logger.getLogger(this.getClass().getName());
private GameInfo gameInfo;
private Writer outWriter;
private StringBuffer currentHistory = new StringBuffer();
private DecimalFormat moneyFormat = new DecimalFormat("0.00", new DecimalFormatSymbols(Locale.ENGLISH));
private ShowdownDataCache showdownDataCache;
// values could be taken from gameInfo, but this is not ready yet
private Map<Integer, Double> amountInPotThisRound = new TreeMap<Integer, Double>();
/**
* This writer will get a handhistory on each gameOverEvent
* @param outWriter
*/
public void setWriter(Writer outWriter) {
this.outWriter = outWriter;
}
@Override
public void actionEvent(int pos, Action act) {
double playerInPotThisRound = amountInPotThisRound.get(pos);
if (act.getType() == PublicGameInfo.SPECIAL_ACTION_RETURNUNCALLEDBET) {
currentHistory.append("Uncalled bet of $").append(moneyFormat.format(act.getAmount())).append(" returned to ").append(gameInfo.getPlayerName(pos))
.append("\n");
return;
}
currentHistory.append(gameInfo.getPlayerName(pos)).append(" ");
switch (act.getType()) {
case Action.SMALL_BLIND:
currentHistory.append("posts the small blind of $").append(moneyFormat.format(act.getAmount())).append("\n");
amountInPotThisRound.put(pos, Utils.roundToCents(playerInPotThisRound + act.getAmount()));
break;
case Action.BIG_BLIND:
currentHistory.append("posts the big blind of $").append(moneyFormat.format(act.getAmount())).append("\n");
currentHistory.append("The button is in seat #").append(gameInfo.getButtonSeat() + 1).append("\n");
amountInPotThisRound.put(pos, Utils.roundToCents(playerInPotThisRound + act.getAmount()));
break;
case Action.CALL:
currentHistory.append("calls $").append(moneyFormat.format(act.getToCall())).append("\n");
amountInPotThisRound.put(pos, Utils.roundToCents(playerInPotThisRound + act.getToCall()));
break;
case Action.RAISE:
currentHistory.append("raises to $").append(moneyFormat.format(playerInPotThisRound + act.getToCall() + act.getAmount())).append("\n");
amountInPotThisRound.put(pos, Utils.roundToCents(playerInPotThisRound + act.getToCall() + act.getAmount()));
break;
case Action.BET:
if (playerInPotThisRound > 0) {
// BigBlind bets, but in the history this should be treated as raise
currentHistory.append("raises to $").append(moneyFormat.format(act.getAmount() + playerInPotThisRound)).append("\n");
} else {
currentHistory.append("bets $").append(moneyFormat.format(act.getAmount())).append("\n");
}
amountInPotThisRound.put(pos, act.getAmount());
break;
case Action.CHECK:
currentHistory.append("checks\n");
break;
case Action.FOLD:
currentHistory.append("folds\n");
break;
case Action.MUCK:
currentHistory.append("mucks\n");
break;
default:
throw new IllegalStateException("Action " + act + " not supported (yet)");
}
}
@Override
public void dealHoleCardsEvent() {
currentHistory.append("*** HOLE CARDS ***\n");
}
@Override
public void gameOverEvent() {
writeSummaryGameInfo();
if (outWriter != null) {
try {
outWriter.write(currentHistory.toString());
outWriter.flush();
} catch (IOException e) {
// currently we don't rethrow so to not disturb the game ?
log.log(Level.SEVERE, "error writing handhistory", e);
}
} else {
log.severe("no writer set, can't write HandHistory");
}
currentHistory = new StringBuffer();
}
@Override
public void gameStartEvent(GameInfo gInfo) {
this.gameInfo = gInfo;
this.showdownDataCache = new ShowdownDataCache(gameInfo);
writeInitalGameInfo();
}
@Override
public void gameStateChanged() {
// not interesting
}
@Override
public void showdownEvent(int seat, Card c1, Card c2) {
currentHistory.append(gameInfo.getPlayerName(seat)).append(" shows ");
currentHistory.append("[").append(c1).append(" ").append(c2).append("]\n");
showdownDataCache.addShowDownCards(seat, c1, c2);
}
@Override
public void stageEvent(int stage) {
for (int i = 0; i < gameInfo.getNumSeats(); i++) {
amountInPotThisRound.put(i, Double.valueOf(0));
}
switch (stage) {
case 0:
break; // preflop not interesting
case 1:
currentHistory.append("*** FLOP *** ");
currentHistory.append(renderBoard(false)).append("\n");
break;
case 2:
currentHistory.append("*** TURN *** ");
currentHistory.append(renderBoard(true)).append("\n");
break;
case 3:
currentHistory.append("*** RIVER *** ");
currentHistory.append(renderBoard(true)).append("\n");
break;
default:
throw new IllegalStateException("stage " + stage + "not supported");
}
}
/**
* creates a string representation of the current board, like [8c 8d 8s]
* @param separateLastCard if true, the last card gets separated like [8c 8d 8s] [8h]
* @return
*/
public String renderBoard(boolean separateLastCard) {
Hand board = gameInfo.getBoard();
String renderString = "[";
for (int i = 0; i < board.size(); i++) {
renderString += board.getCard(i + 1);
if (separateLastCard && (i == board.size() - 2)) {
renderString += "] [";
} else if (i < board.size() - 1) {
renderString += " ";
}
}
renderString += "]";
return renderString;
}
@Override
public void winEvent(int pos, double amount, String handName) {
// Text to create:
// player5 wins the pot ($0.05)
// player6 ties for the pot ($x.xx)
if (!showdownDataCache.areFinalPotsInitialized()) {
showdownDataCache.initializeFinalPot(gameInfo);
}
currentHistory.append(gameInfo.getPlayerName(pos));
if (showdownDataCache.getAmountFromPot(pos, amount)) {
currentHistory.append(" ties for the pot ");
} else {
currentHistory.append(" wins the pot ");
}
currentHistory.append("($").append(moneyFormat.format(amount)).append(")\n");
}
/**
* Writes the beginning Header
*
* <pre>
* Full Tilt Poker Game #19342777650: Table Jay (shallow) - $0.01/$0.02 - No Limit Hold'em - 18:21:50 ET - 2010/03/17
* Seat 1: player1 ($0.33)
* Seat 3: player3 ($0.42)
* </pre>
*
*/
private void writeInitalGameInfo() {
// First line:
// Full Tilt Poker Game #19342777650: Table Jay (shallow) - $0.01/$0.02 - No Limit Hold'em - 18:21:50 ET - 2010/03/17
currentHistory.append("Full Tilt Poker Game #").append(gameInfo.getGameID());
currentHistory.append(": Table OpenTestBed - $");
currentHistory.append(moneyFormat.format(gameInfo.getSmallBlindSize()));
currentHistory.append("/$");
currentHistory.append(moneyFormat.format(gameInfo.getBigBlindSize()));
currentHistory.append(" - ").append(gameInfo.isNoLimit() ? "No " : gameInfo.isPotLimit() ? "Pot " : "").append("Limit Hold'em");
DateFormat dateFormat = new SimpleDateFormat("hh:mm:ss z - yyyy/MM/dd");
currentHistory.append(" - ").append(dateFormat.format(getGameTime()));
currentHistory.append("\n");
if (gameInfo.isActive(9)) {
throw new IllegalStateException("Full-Tilt format only supports 9 players, but 10 players are seated");
}
// Player Seats
// Seat 1: player1 ($0.33)
for (int playerId = 0; playerId < 9; playerId++) {
if (gameInfo.isActive(playerId)) {
currentHistory.append("Seat ").append(playerId + 1).append(": ");
currentHistory.append(gameInfo.getPlayerName(playerId));
currentHistory.append(" ($").append(moneyFormat.format(gameInfo.getBankRoll(playerId))).append(")\n");
}
}
}
/**
* writes the summary like
* <pre>
* *** SUMMARY ***
* Seat 1: redilts collected ($0.02)
* Seat 4: player4 showed [Ad As] and won ($2.30)
* </pre>
*/
private void writeSummaryGameInfo() {
currentHistory.append("*** SUMMARY ***\n");
for (Entry<Integer, Double> playerAmountWon : showdownDataCache.getAmountsWon().entrySet()) {
int seat = playerAmountWon.getKey();
double amount = playerAmountWon.getValue();
if (amount < 0.001) {
continue;
}
currentHistory.append("Seat ").append(seat + 1);
currentHistory.append(": ").append(gameInfo.getPlayerName(seat));
List<Card> cards = showdownDataCache.getHandsShown(seat);
if (cards != null) {
currentHistory.append(" showed [").append(cards.get(0)).append(" ").append(cards.get(1)).append("] and won ");
} else {
currentHistory.append(" collected ");
}
currentHistory.append("($").append(moneyFormat.format(amount)).append(")\n");
}
currentHistory.append("\n\n");
}
/**
* to be overridden by JUnit-Tests
* @return
*/
protected Date getGameTime() {
return new Date();
}
/**
* this helper class caches some results relevant for showdown and showdown summary.<br>
* For instance the GameInfo doesn't give us infos about which player got what amount
* from a certain pot and if this tied the pot (but in the output we have to provide this).
* Thats why on ShowDown we copy the pot-sizes into our cache and for each payout determine
* ourselfes if this was from a tied pot or a full pot.<br>
* Furthermore we cache all shown cards and player winnings for later printing in the summary.
*/
static class ShowdownDataCache {
/** pot sizes at showdown */
private List<Double> potsAtShowDown;
/** of a pot is tied */
private List<Boolean> tiedPotsAtShowDown;
private Map<Integer, List<Card>> handShown = new TreeMap<Integer, List<Card>>();
private Map<Integer, Double> amountWon = new TreeMap<Integer, Double>();
public ShowdownDataCache(GameInfo gameInfo) {
for (int i = 0; i < gameInfo.getNumSeats(); i++) {
amountWon.put(i, Double.valueOf(0));
}
}
public void addShowDownCards(int seat, Card c1, Card c2) {
handShown.put(seat, Arrays.asList(c1, c2));
}
/**
* @return returns the cards of the corresponding player, if he showed
* the cards (otherwise null)
*/
public List<Card> getHandsShown(int seat) {
return handShown.get(seat);
}
/**
* @return Map<Integer, Double> map<PlayerId, Amount> of player winnings
*/
public Map<Integer, Double> getAmountsWon() {
return amountWon;
}
/**
* reduces the given amount from the latest pot and returns, if the
* amount tied the pot or was the full pot<br>
* Furthermore this method accumulates each players winnings
* @param amount
* @return
*/
public boolean getAmountFromPot(int pos, double amount) {
boolean tied = false;
if (potsAtShowDown.size() == 0) {
throw new IllegalStateException("no pot left to get a payout from");
}
int currentPot = potsAtShowDown.size() - 1;
double currentSidePotAmount = potsAtShowDown.get(currentPot);
if (amount > currentSidePotAmount) {
throw new IllegalStateException("the current (side) pot doesn't contain enough money (" + currentSidePotAmount + ") for this payout (" + amount
+ ")\n" + "Please ensure, that payouts match the pot sizes\n");
}
// if the amount is smaller than the amount, this pot is marked
// as tied
if (amount < currentSidePotAmount) {
tiedPotsAtShowDown.set(currentPot, Boolean.TRUE);
}
// either this amount has tied the pot or a previous amount
if (tiedPotsAtShowDown.get(currentPot)) {
tied = true;
}
// reduce the potAmount in our cache. If the full pot
// was payed out, remove it
currentSidePotAmount = Utils.roundToCents(currentSidePotAmount - amount);
if (currentSidePotAmount < 0.001) {
potsAtShowDown.remove(currentPot);
tiedPotsAtShowDown.remove(currentPot);
} else {
potsAtShowDown.set(currentPot, Double.valueOf(currentSidePotAmount));
}
// update players winnings
double playerAmountWon = amountWon.get(pos);
amountWon.put(pos, Utils.roundToCents(playerAmountWon + amount));
return tied;
}
/**
* copies the pot sizes into internal structure
* @param gameInfo
*/
public void initializeFinalPot(GameInfo gameInfo) {
potsAtShowDown = new ArrayList<Double>();
tiedPotsAtShowDown = new ArrayList<Boolean>();
// remember main pot, not tied yet
potsAtShowDown.add(Double.valueOf(gameInfo.getMainPotSize()));
tiedPotsAtShowDown.add(Boolean.FALSE);
// side pots, not tied yet
for (int i = 0; i < gameInfo.getNumSidePots(); i++) {
potsAtShowDown.add(new Double(gameInfo.getSidePotSize(i)));
tiedPotsAtShowDown.add(Boolean.FALSE);
}
}
public boolean areFinalPotsInitialized() {
return potsAtShowDown != null;
}
}
@Override
public void holeCards(Card c1, Card c2, int seat) {
currentHistory.append("Dealt to ").append(gameInfo.getPlayerName(seat)).append(" ");
currentHistory.append("[").append(c1).append(" ").append(c2).append("]\n");
}
public String getCurrentHistory() {
return currentHistory.toString();
}
@Override
public String toString() {
return currentHistory.toString();
}
}