/** * Copyright (c) 2005-2017, KoLmafia development team * http://kolmafia.sourceforge.net/ * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * [1] Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * [2] Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in * the documentation and/or other materials provided with the * distribution. * [3] Neither the name "KoLmafia" nor the names of its contributors may * be used to endorse or promote products derived from this software * without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. */ package net.sourceforge.kolmafia.session; import java.util.ArrayList; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern; import net.sourceforge.kolmafia.KoLCharacter; import net.sourceforge.kolmafia.KoLConstants; import net.sourceforge.kolmafia.RequestLogger; import net.sourceforge.kolmafia.RequestThread; import net.sourceforge.kolmafia.request.GenericRequest; import net.sourceforge.kolmafia.request.MoneyMakingGameRequest; import net.sourceforge.kolmafia.utilities.StringUtilities; public class MoneyMakingGameManager { private static final Pattern PENDING_BETS_PATTERN = Pattern.compile( "Your Pending Bets:.*?<table>(.*?)</table>" ); private static final Pattern MY_BET_PATTERN = Pattern.compile( "<tr>.*?([0123456789,]+) Meat.*?betid value='(\\d*)'.*?</tr>" ); private static final Pattern RECENT_BETS_PATTERN = Pattern.compile( "(Last 20 Bets|Bets Found).*?<table.*?>(.*?)</table>" ); private static final Pattern OFFERED_BET_PATTERN = Pattern.compile( "<tr>.*?showplayer.*?<b>(.*?)</b> \\(#(\\d*)\\).*?([0123456789,]+) Meat.*?whichbet value='(\\d*)'.*?</tr>" ); // Babycakes (#311877) took your 1,000 Meat bet, and you lost. Better luck next time. // Babycakes (#311877) took your 1,000 Meat bet, and you won, earning you 1,998 Meat. private static final Pattern EVENT_PATTERN = Pattern.compile( "(?:.*- )?(.*) \\(#(\\d+)\\) took your ([1234567890,]*) Meat bet, and you (won|lost)(, earning you ([0123456789,]*) Meat)?" ); private static final Pattern TAKE_BET_PATTERN = Pattern.compile( "You take the ([0123456789,]*) bet" ); private static final Pattern WON_BET_PATTERN = Pattern.compile( "(You gain|have him deliver) ([0123456789,]*) Meat" ); // Database Locking. private static Object lock = new Object(); // Current bets offered by others private static ArrayList<Bet> offered = new ArrayList<Bet>(); // The amount I won or lost from the last bet I took private static int lastWinnings = 0; // Active bets I've made private static ArrayList active = new ArrayList(); // Bets I've made that are taken with no notification yet private static LinkedList<Bet> taken = new LinkedList<Bet>(); // The last bet I made private static Bet lastBet = null; // Events received but not yet matched private static LinkedList<Event> received = new LinkedList<Event>(); // Events received and matched to taken bets private static LinkedList<Event> resolved = new LinkedList<Event>(); // The last event handled private static Event lastEvent = null; // The following are needed to detect bets that are taken before we are // able to learn the bet ID and register them. // Initial ID for a dummy bet: already gone before KoL returns the list // of current bets. private static int dummyBetId = -1000; // The amount of the bet we are in the process of submitting public static int makingBet = 0; public static void reset() { synchronized ( MoneyMakingGameManager.lock ) { MoneyMakingGameManager.offered.clear(); MoneyMakingGameManager.lastWinnings = 0; MoneyMakingGameManager.makingBet = 0; MoneyMakingGameManager.dummyBetId = -1; MoneyMakingGameManager.active.clear(); MoneyMakingGameManager.taken.clear(); MoneyMakingGameManager.lastBet = null; MoneyMakingGameManager.received.clear(); MoneyMakingGameManager.resolved.clear(); MoneyMakingGameManager.lastEvent = null; } } public static final Bet getLastBet() { synchronized ( MoneyMakingGameManager.lock ) { return MoneyMakingGameManager.lastBet; } } public static final int getLastBetId() { synchronized ( MoneyMakingGameManager.lock ) { Bet bet = MoneyMakingGameManager.lastBet; return bet == null ? 0 : bet.getId(); } } private static final int [] getBets( final List list ) { Iterator it = list.iterator(); int [] bets = new int[ list.size() ]; int index = 0; while ( it.hasNext() ) { Bet bet = (Bet) it.next(); bets[ index++ ] = bet.getId(); } return bets; } private static final Bet findBet( final int id, List list ) { Iterator it = list.iterator(); while ( it.hasNext() ) { Bet bet = (Bet) it.next(); if ( bet.getId() == id ) { return bet; } } return null; } public static final int [] getOfferedBets() { synchronized ( MoneyMakingGameManager.lock ) { return MoneyMakingGameManager.getBets( offered ); } } public static final void parseOfferedBets( final String responseText ) { synchronized ( MoneyMakingGameManager.lock ) { // Find all currently active bets MoneyMakingGameManager.offered.clear(); Matcher recent = RECENT_BETS_PATTERN.matcher( responseText ); if ( recent.find() ) { Matcher betMatcher = OFFERED_BET_PATTERN.matcher( recent.group( 2 ) ); while ( betMatcher.find() ) { String player = betMatcher.group( 1 ); int playerId = StringUtilities.parseInt( betMatcher.group( 2 ) ); int amount = StringUtilities.parseInt( betMatcher.group( 3 ) ); int betid = StringUtilities.parseInt( betMatcher.group( 4 ) ); Bet bet = new Bet( betid, amount, player, playerId ); // Add to offered list MoneyMakingGameManager.offered.add( bet ); } } } } public static final int [] getActiveBets() { synchronized ( MoneyMakingGameManager.lock ) { return MoneyMakingGameManager.getBets( active ); } } public static final void parseMyBets( final String responseText, final boolean internal ) { synchronized ( MoneyMakingGameManager.lock ) { // Constructed list of currently outstanding bets ArrayList<Bet> current = new ArrayList<Bet>(); // Assume there is no newly placed bet MoneyMakingGameManager.lastBet = null; // Find all currently active bets Matcher pending = PENDING_BETS_PATTERN.matcher( responseText ); if ( pending.find() ) { Matcher betMatcher = MY_BET_PATTERN.matcher( pending.group( 1 ) ); while ( betMatcher.find() ) { int amount = StringUtilities.parseInt( betMatcher.group( 1 ) ); int betId = StringUtilities.parseInt( betMatcher.group( 2 ) ); Bet bet = MoneyMakingGameManager.findBet( betId, MoneyMakingGameManager.active ); if ( bet == null ) { // This is a new bet bet = new Bet( betId, amount, internal ); MoneyMakingGameManager.lastBet = bet; } current.add( bet ); } } // Move any bets that are gone to taken int count = MoneyMakingGameManager.active.size(); for ( int i = 0; i < count; ++i ) { Bet bet = (Bet) MoneyMakingGameManager.active.get( i ); if ( current.indexOf( bet) == -1 ) { // Bet is gone. Move to taken MoneyMakingGameManager.handleTakenBet( bet ); } } // KoL redirects us from the URL that submitted the bet // to, simply, bet.php. It is possible for our bet to // be taken before we get the response from that page. // When this happens, we will not detect a new bet. if ( MoneyMakingGameManager.makingBet != 0 && MoneyMakingGameManager.lastBet == null ) { // Make a dummy bet to match with the eventual // event - which could have arrived already. Bet bet = new Bet( MoneyMakingGameManager.dummyBetId--, MoneyMakingGameManager.makingBet, internal ); MoneyMakingGameManager.lastBet = bet; MoneyMakingGameManager.handleTakenBet( bet ); } // Finally, save new list of active bets MoneyMakingGameManager.active = current; } } private static final void handleTakenBet( final Bet bet ) { Event ev = MoneyMakingGameManager.findMatchingEvent( bet.getAmount() ); if ( ev != null ) { MoneyMakingGameManager.resolveEvent( ev, bet ); } else { MoneyMakingGameManager.taken.add( bet ); } } private static final void resolveEvent( final Event ev, final Bet bet ) { // A bet which is not internally generated was placed in the // Relay Browser, not via a request from an ASH script. // // Nobody will wait for such an event, so don't consume memory // keeping the event on the resolved list. if ( !bet.isInternal() ) { return; } ev.setBet( bet ); synchronized ( MoneyMakingGameManager.resolved ) { MoneyMakingGameManager.resolved.add( ev ); MoneyMakingGameManager.resolved.notify(); } } private static final Bet findBet( final int id ) { Bet bet = MoneyMakingGameManager.findBet( id, offered ); if ( bet != null ) { return bet; } bet = MoneyMakingGameManager.findBet( id, active ); if ( bet != null ) { return bet; } bet = MoneyMakingGameManager.findBet( id, taken ); if ( bet != null ) { return bet; } bet = MoneyMakingGameManager.getLastEventBet(); if ( bet != null && bet.getId() == id ) { return bet; } return null; } public static final String betOwner( final int id ) { synchronized ( MoneyMakingGameManager.lock ) { Bet bet = MoneyMakingGameManager.findBet( id ); return bet == null ? "" : bet.getPlayer(); } } public static final int betOwnerId( final int id ) { synchronized ( MoneyMakingGameManager.lock ) { Bet bet = MoneyMakingGameManager.findBet( id ); return bet == null ? 0 : bet.getPlayerId(); } } public static final int betAmount( final int id ) { synchronized ( MoneyMakingGameManager.lock ) { Bet bet = MoneyMakingGameManager.findBet( id ); return bet == null ? 0 : bet.getAmount(); } } public static final void makeBet( final String responseText ) { synchronized ( MoneyMakingGameManager.lock ) { Bet bet = MoneyMakingGameManager.lastBet; if ( bet == null ) { // Uh oh. return; } int amount = bet.getAmount(); if ( responseText.indexOf( "Meat has been taken from Hagnk's" ) != -1 ) { bet.setFromStorage( true ); KoLCharacter.addStorageMeat( -amount ); } else { ResultProcessor.processMeat( -amount ); } } } public static final void retractBet( final String urlString, final String responseText ) { synchronized ( MoneyMakingGameManager.lock ) { // See if we succeeded in retracting the bid if ( responseText.indexOf( "You retract your bid" ) == -1 ) { return; } // Get the bet id int betId = MoneyMakingGameRequest.getBetId( urlString ); if ( betId < 0 ) { return; } // Find the bet on the "taken" list, since it was moved // there when we didn't find it on the list of active // bets. Bet bet = MoneyMakingGameManager.findBet( betId, MoneyMakingGameManager.taken ); if ( bet == null ) { // Internal error return; } int amount = bet.getAmount(); // Put back meat to wherever it came from if ( bet.fromStorage() ) { // Add meat to storage KoLCharacter.addStorageMeat( amount ); } else { // Add meat to inventory ResultProcessor.processMeat( amount ); } // Remove the bet from the taken list int index = MoneyMakingGameManager.taken.indexOf( bet ); MoneyMakingGameManager.taken.remove( index ); } } public static final void takeBet( final String urlString, final String responseText ) { synchronized ( MoneyMakingGameManager.lock ) { MoneyMakingGameManager.lastWinnings = 0; } // Find bet amount. If we can't, we failed to take the bet for // some reason. Matcher takeMatcher = TAKE_BET_PATTERN.matcher( responseText ); if ( !takeMatcher.find() ) { return; } // Find out if used Meat from inventory or storage String from = MoneyMakingGameRequest.getFromString( urlString ); if ( from == null ) { return; } boolean storage = from.equals( "storage" ); int whichbet = MoneyMakingGameRequest.getWhichBet( urlString ); if ( whichbet < 0 ) { return; } int amount = StringUtilities.parseInt( takeMatcher.group( 1 ) ); String message = "Taking bet " + whichbet + " using " + KoLConstants.COMMA_FORMAT.format( amount ) + " meat from " + from; RequestLogger.printLine( message ); RequestLogger.updateSessionLog( message ); // We paid to gamble. Deduct the cost. int winnings = -amount; // If we won, add in the prize. Matcher wonMatcher = WON_BET_PATTERN.matcher( responseText ); if ( wonMatcher.find() ) { winnings += StringUtilities.parseInt( wonMatcher.group( 2 ) ); } // Adjust meat balance if ( storage ) { // Add meat to storage KoLCharacter.addStorageMeat( winnings ); } else { // Add meat to inventory ResultProcessor.processMeat( winnings ); } message = "You " + ( winnings >= 0 ? "gain " : "lose " ) + KoLConstants.COMMA_FORMAT.format( Math.abs( winnings ) ) + " Meat" + ( storage ? " from storage" : "" ); RequestLogger.printLine( message ); RequestLogger.updateSessionLog( message ); synchronized ( MoneyMakingGameManager.lock ) { MoneyMakingGameManager.lastWinnings = winnings; } } public static final int getLastWinnings() { synchronized ( MoneyMakingGameManager.lock ) { return MoneyMakingGameManager.lastWinnings; } } public static final void processEvent( final String eventText ) { synchronized ( MoneyMakingGameManager.lock ) { Matcher matcher = EVENT_PATTERN.matcher( eventText ); if ( !matcher.find() ) { return; } String player = matcher.group( 1 ); int playerId = StringUtilities.parseInt( matcher.group( 2 ) ); int amount = StringUtilities.parseInt( matcher.group( 3 ) ); boolean won = matcher.group( 4 ).equals( "won" ); int winnings = matcher.group( 5 ) != null ? StringUtilities.parseInt( matcher.group( 6 ) ) : 0; boolean storage = eventText.indexOf( "Hagnk's" ) != -1; if ( won ) { // Add meat to wherever it goes if ( storage ) { // Add meat to storage KoLCharacter.addStorageMeat( winnings ); } else { // Add meat to inventory ResultProcessor.processMeat( winnings ); } } Event ev = new Event( player, playerId, amount, winnings ); // A matching bet on the "taken" list goes with this // event. We resolve the first match. There can be more // than one and we can't tell which one went with this // event, but it doesn't matter. Bet bet = MoneyMakingGameManager.findMatchingBet( amount, MoneyMakingGameManager.taken ); if ( bet != null ) { MoneyMakingGameManager.taken.remove( bet ); MoneyMakingGameManager.resolveEvent( ev, bet ); return; } // A matching bet on the "active" list goes with this // event. There can be more than one, but we can't tell // which one went with this event - and it does // matter. We have to wait until the bet is moved to // the "taken" list. bet = MoneyMakingGameManager.findMatchingBet( amount, MoneyMakingGameManager.active ); if ( bet != null ) { MoneyMakingGameManager.received.add( ev ); return; } // If chat is active, the event signaling the taking of // a bet can arrive before the response from the // request that submitted it. if ( MoneyMakingGameManager.makingBet != 0 ) { MoneyMakingGameManager.received.add( ev ); return; } // No matching active or taken bet. Drop event. } } private static final Event findMatchingEvent( final int amount ) { Iterator it = MoneyMakingGameManager.received.iterator(); while ( it.hasNext() ) { Event ev = (Event) it.next(); if ( amount == ev.getAmount() ) { it.remove(); return ev; } } return null; } private static final Bet findMatchingBet( final int amount, final List list ) { Iterator it = list.iterator(); while ( it.hasNext() ) { Bet bet = (Bet) it.next(); if ( amount == bet.getAmount() ) { return bet; } } return null; } private static final GenericRequest VISIT = new MoneyMakingGameRequest(); private static final GenericRequest MAIN = new GenericRequest( "main.php" ); public static final int getNextEvent( final int ms ) { // Wait up to specified number of seconds to get an event. // Return bet ID, or 0 if timeout boolean waited = false; while ( true ) { // If we have any resolved events, take them first synchronized ( MoneyMakingGameManager.resolved ) { if ( !MoneyMakingGameManager.resolved.isEmpty() ) { Event ev = (Event) MoneyMakingGameManager.resolved.remove( 0 ); MoneyMakingGameManager.lastEvent = ev; return ev.getBetId(); } } boolean visit = false; synchronized ( MoneyMakingGameManager.lock ) { // If we have unresolved events, visit the bet // page to resolve the appropriate active bet. visit = !MoneyMakingGameManager.received.isEmpty(); // If we have no active or taken bets, no // events will come if ( !visit && MoneyMakingGameManager.taken.isEmpty() && MoneyMakingGameManager.active.isEmpty() ) { return -1; } } if ( visit ) { RequestThread.postRequest( VISIT ); VISIT.responseText = null; continue; } // If we have already waited and found nothing, bail if ( waited ) { return 0; } // Otherwise, wait synchronized ( MoneyMakingGameManager.resolved ) { try { MoneyMakingGameManager.resolved.wait( ms ); if ( !MoneyMakingGameManager.resolved.isEmpty() ) { continue; } waited = true; } catch ( InterruptedException e ) { } } synchronized ( MoneyMakingGameManager.lock ) { if ( !MoneyMakingGameManager.received.isEmpty() ) { continue; } } // If we still have no events, it's possible that chat // is not active and we just haven't detected any. RequestThread.postRequest( MAIN ); MAIN.responseText = null; } } public static final Event getLastEvent() { synchronized ( MoneyMakingGameManager.lock ) { return MoneyMakingGameManager.lastEvent; } } public static final Bet getLastEventBet() { synchronized ( MoneyMakingGameManager.lock ) { Event event = MoneyMakingGameManager.lastEvent; return event == null ? null : event.getBet(); } } public static final int getLastEventBetId() { synchronized ( MoneyMakingGameManager.lock ) { Event event = MoneyMakingGameManager.lastEvent; return event == null ? 0 : event.getBetId(); } } public static final String getLastEventPlayer() { synchronized ( MoneyMakingGameManager.lock ) { Event event = MoneyMakingGameManager.lastEvent; return event == null ? "" : event.getPlayer(); } } public static final int getLastEventPlayerId() { synchronized ( MoneyMakingGameManager.lock ) { Event event = MoneyMakingGameManager.lastEvent; return event == null ? 0 : event.getPlayerId(); } } public static final int getLastEventWinnings() { synchronized ( MoneyMakingGameManager.lock ) { Event event = MoneyMakingGameManager.lastEvent; return event == null ? 0 : event.getWinnings(); } } public static class Bet implements Comparable<Bet> { private final int betId; private final int amount; private final String player; private final int playerId; private boolean fromStorage; // true if this bet was internally generated from within // KoLmafia. I.e., from a MoneyMakingRequest, from ASH private boolean internal; public Bet( final int betId, final int amount, final String player, final int playerId ) { this.betId = betId; this.amount = amount; this.player = player; this.playerId = playerId; this.fromStorage = false; this.internal = false; } public Bet( final int betId, final int amount, final boolean internal ) { this( betId, amount, KoLCharacter.getUserName(), KoLCharacter.getUserId() ); this.internal = internal; } public int getId() { return this.betId; } public int getAmount() { return this.amount; } public String getPlayer() { return this.player; } public int getPlayerId() { return this.playerId; } public boolean fromStorage() { return this.fromStorage; } public boolean isInternal() { return this.internal; } public void setFromStorage( final boolean fromStorage ) { this.fromStorage = fromStorage; } @Override public boolean equals( final Object o ) { if ( o instanceof Bet ) { return this.compareTo( (Bet) o ) == 0; } return false; } @Override public int hashCode() { return this.betId; } public int compareTo( final Bet o ) { if ( o instanceof Bet ) { return this.betId - ((Bet) o).betId; } return -1; } @Override public String toString() { return "bet(" + this.betId + ", " + this.player + ", " + this.playerId + ", " + this.amount + ")"; } } public static class Event implements Comparable<Event> { private Bet bet; private final String player; private final int playerId; private final int amount; private final int winnings; public Event( final String player, final int playerId, final int amount, final int winnings ) { this.bet = null; this.player = player; this.playerId = playerId; this.amount = amount; this.winnings = winnings; } public Bet getBet() { return this.bet; } public int getBetId() { return this.bet == null ? 0 : this.bet.getId(); } public void setBet( final Bet bet ) { this.bet = bet; } public String getPlayer() { return this.player; } public int getPlayerId() { return this.playerId; } public int getAmount() { return this.amount; } public int getWinnings() { return this.winnings; } public int compareTo( final Event o ) { if ( !( o instanceof Event ) ) { return -1; } if ( this.bet == null ) { return 1; } return this.bet.compareTo( ((Event) o).bet ); } @Override public String toString() { return "event(" + this.getBetId() + ", " + this.player + ", " + this.playerId + ", " + this.winnings + ")"; } } }