package com.idunnolol.sotm;
import android.util.Pair;
import com.danlew.utils.Log;
import com.idunnolol.sotm.data.Card;
import com.idunnolol.sotm.data.Card.Type;
import com.idunnolol.sotm.data.Db;
import com.idunnolol.sotm.data.GameSetup;
import com.idunnolol.sotm.data.Prefs;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.Set;
public class Randomizer {
// Plus/minus this value for difficulty
private static final int DIFFICULTY_FUZZ = 4;
// Randomization timeout in nanoseconds (200 ms)
private static final long RANDOMIZE_TIMEOUT = 200 * 1000000;
// Chance that we'll pick an advanced villain (if allowed)
private static final double ADVANCED_VILLAIN_CHANCE = .15;
private Random mRand;
// The base game setup; when we randomize, we fill in any cards set to RANDOM
private GameSetup mBaseGameSetup;
// All cards that can be picked by the randomizer for a given type
private Map<Type, Set<Card>> mValidCards;
// These are the cards, minus alts, to give each card equal weight
private Map<Type, List<Card>> mCardBanks;
public Randomizer(GameSetup baseGameSetup) {
mBaseGameSetup = baseGameSetup;
mRand = new Random();
mValidCards = new HashMap<Type, Set<Card>>();
mCardBanks = new HashMap<Type, List<Card>>();
}
public GameSetup randomize() {
GameSetup gameSetup = new GameSetup(mBaseGameSetup);
// Go through all heroes, villains and environments
// and randomly select cards to fill in when the
// card is "random"
List<Card> heroes = gameSetup.getHeroes();
Set<Card> usedCards = new HashSet<Card>();
int size = heroes.size();
// Fill the initial used cards
for (Card hero : heroes) {
usedCards.addAll(Db.getCardAndAlternates(hero));
}
for (int a = 0; a < size; a++) {
Card hero = heroes.get(a);
if (hero.isRandom()) {
Card card;
do {
card = getRandomCard(hero);
}
while (usedCards.contains(card));
gameSetup.setHero(a, card);
usedCards.addAll(Db.getCardAndAlternates(card));
}
}
Card villain = gameSetup.getVillain();
if (villain.isRandom()) {
if (!villain.isTeam()) {
gameSetup.setVillain(getRandomCard(villain));
}
villain = gameSetup.getVillain();
// If advanced is allowed, and there are enough advanced stats, then give it a chance to pick advanced.
if (Prefs.isAdvancedAllowed() && villain.canBeAdvanced() && mRand.nextDouble() < ADVANCED_VILLAIN_CHANCE) {
villain = new Card(villain);
villain.makeAdvanced();
gameSetup.setVillain(villain);
}
// If we randomly selected a team, make sure to randomize the team as well
if (villain.isTeam()) {
int teamSize = 0;
if (villain.getId().equals("Vengeance Five")
|| villain.getId().equals("Villains of the Multiverse")) {
teamSize = gameSetup.getHeroCount();
}
else {
throw new RuntimeException("Don't know how to handle this team: " + villain);
}
gameSetup.setVillainTeam(getRandomTeam(villain, teamSize));
}
}
Card environment = gameSetup.getEnvironment();
if (environment.isRandom()) {
gameSetup.setEnvironment(getRandomCard(environment));
}
return gameSetup;
}
/**
* Here's how we do randomization with a target win %. It is extremely
* lazy and cheap.
*
* First, we have a fuzzing range for the acceptable points. This
* is to introduce a bit of variety, otherwise you will commonly
* end up with the same setups for various difficulties.
*
* We randomly fill in the base set until we hit an acceptable point
* value. We keep retrying for a certain amount of time. This method
* is used because it's easy and I'm lazy.
*
* If we fail to fill the random slots, then we fallback to the closest
* game setup we found before failing.
*
* @param targetWinPercent
* @return
*/
public GameSetup randomize(int targetWinPercent) {
int minAcceptableWinPercent = targetWinPercent - DIFFICULTY_FUZZ;
int maxAcceptableWinPercent = targetWinPercent + DIFFICULTY_FUZZ;
Pair<Integer, Integer> pair = Db.getPointRange(minAcceptableWinPercent, maxAcceptableWinPercent);
int minPoints = pair.first;
int maxPoints = pair.second;
long start = System.nanoTime();
GameSetup currGameSetup;
int minDistance = Integer.MAX_VALUE;
GameSetup bestGameSetup = null;
int numTimes = 0;
do {
numTimes++;
currGameSetup = randomize();
int currPoints = currGameSetup.getPoints();
if (currPoints > maxPoints) {
if (currPoints - maxPoints < minDistance) {
minDistance = currPoints - maxPoints;
bestGameSetup = currGameSetup;
}
}
else if (currPoints < minPoints) {
if (minPoints - currPoints < minDistance) {
minDistance = minPoints - currPoints;
bestGameSetup = currGameSetup;
}
}
else {
bestGameSetup = currGameSetup;
break;
}
}
while (System.nanoTime() - start < RANDOMIZE_TIMEOUT);
Log.i("Range[" + minPoints + ", " + maxPoints + "] - found " + bestGameSetup.getPoints() + " (after "
+ numTimes + " tries)");
return bestGameSetup;
}
private Card getRandomCard(Card baseCard) {
Type type = baseCard.getType();
if (!mValidCards.containsKey(type)) {
List<Card> validCards = Db.getCards(type);
mValidCards.put(type, new HashSet<Card>(validCards));
Set<Card> cardBank = new HashSet<Card>(validCards);
// Remove alternates so that all cards have equal weight
Iterator<Card> iterator = cardBank.iterator();
while (iterator.hasNext()) {
Card card = iterator.next();
Set<Card> alts = Db.getCardAndAlternates(card);
for (Card alt : alts) {
if (!card.equals(alt) && cardBank.contains(alt)) {
iterator.remove();
break;
}
}
}
// Add to card banks
mCardBanks.put(type, new ArrayList<Card>(cardBank));
}
List<Card> cards = mCardBanks.get(type);
int index = mRand.nextInt(cards.size());
Card card = cards.get(index);
// Check if there are alternates; if there are, pick one of them
List<Card> alts = new ArrayList<Card>(Db.getCardAndAlternates(card));
int size = alts.size();
if (size > 1) {
Set<Card> validCards = mValidCards.get(type);
index = mRand.nextInt(size);
while (true) {
card = alts.get(index);
if (validCards.contains(card)) {
break;
}
else {
index = (index + 1) % size;
}
}
}
return card;
}
private List<Card> getRandomTeam(Card card, int teamSize) {
List<Card> team = new ArrayList<Card>(teamSize);
List<Card> teamChoices = new ArrayList<Card>(card.getTeamMembers());
if (teamChoices.size() < teamSize) {
throw new RuntimeException("Not enough team members to choose from for card " + card);
}
for (int a = 0; a < teamSize; a++) {
Card teamMember = teamChoices.remove(mRand.nextInt(teamChoices.size()));
team.add(teamMember);
}
return team;
}
}