import java.util.ArrayList; import java.util.Random; public class Runner { private static ArrayList<String> player1StateArray; private static ArrayList<String> player1CupContents; private static ArrayList<Integer> player1CupsDrawnFromArray; private static ArrayList<Integer> player1ColorDrawnFromCupArray; private static ArrayList<String> player2StateArray; private static ArrayList<String> player2CupContents; private static ArrayList<Integer> player2CupsDrawnFromArray; private static ArrayList<Integer> player2ColorDrawnFromCupArray; private static int maxBeadsPerCup; public static void main(String[] args) { //Code written by Eric Leschinski, idea nabbed from O'REILLY statistics hacks //Hack #52. //Usual disclaimer, no warranties, if it starts acting strangely, call Neo. //Tic Tac Toe index positions // 0 | 1 | 2 //----------- // 3 | 4 | 5 //----------- // 6 | 7 | 8 //A state of the tic-tac-toe board is called a state //for example a state looks like this: // X | O | //----------- // O | X | //----------- // | | O // 012345678 //The above state is represented with a string: "XO OX O" //The color of the beads in the cup are represented with integer 0 through 8 //We create states it on the fly so we don't make more than we have to. //CONCLUSIONS I LEARNED FROM THIS APPLICATION: //1. Whoever gets to start at Tic-tac-toe has a massive advantage to win. //2. If you get to start, the best place to start is a corner. //3. This application is not perfect, consider the following state, // X to move: // | O | // ---|---|--- // X | O | O // ---|---|--- // O | X | X //The choices are a Loss or a Draw. Position 2 is Draw, Position 0 is loss. //This algorithm punishes Player1 for draw and loss, so the choices are //reduced to the minimum and the cupContents become very small. //The problem is that this algorithm has no concept of "given a choice between //loss and draw, choose draw". It hates them both the same so it ends up //choosing randomly. //This is a list of all the states. player1StateArray and player1CupContents are the same length. player1StateArray = new ArrayList<String>(); //This is a list of the cup contents by state. player1CupContents = new ArrayList<String>(); //This is a list of all the states. player2StateArray and player2CupContents are the same length. player2StateArray = new ArrayList<String>(); //This is a list of the cup contents by state. player2CupContents = new ArrayList<String>(); player1CupsDrawnFromArray = new ArrayList<Integer>(); player1ColorDrawnFromCupArray = new ArrayList<Integer>(); player2CupsDrawnFromArray = new ArrayList<Integer>(); player2ColorDrawnFromCupArray = new ArrayList<Integer>(); maxBeadsPerCup = 100; //The number of beads in a cup won't exceed this number //If we put more than 100 in a cup, the new integer pushed in //deletes another memory spot. String currentState; //Have two learning algorithms play against eachother a bunch of times. char whoWon = ' '; boolean player1Learns = true; //Set one of these guys to false to see how much of an advantage learning gives you. boolean player2Learns = false; int player1Wins = 0; int player2Wins = 0; int player1Started = 0; int player2Started = 0; int counter = 0; //Play tic-tac-toe 4000 times: for(int i = 0; i < 4000; i++) { currentState = getBlankState(); System.out.println("============= NEW GAME ================="); player1CupsDrawnFromArray.clear(); player1ColorDrawnFromCupArray.clear(); player2CupsDrawnFromArray.clear(); player2ColorDrawnFromCupArray.clear(); //purpose of this is to give X and O equal chances at beginning //new games. Turns out whoever starts at tic-tac-toe has a //massive advantage at winning the game. if (counter % 2 == 0) { while (true) { player2Started++; //counting how often player2 got to start. currentState = addMarkToState(currentState, player2GetNextMove(currentState), 'O'); displayStateNicely(currentState); whoWon = testStateSeeWhoWins(currentState); if (whoWon != ' ') break; currentState = addMarkToState(currentState, player1GetNextMove(currentState), 'X'); displayStateNicely(currentState); whoWon = testStateSeeWhoWins(currentState); if (whoWon != ' ') break; } } else { while (true) { player1Started++; //counting how often player1 got to start. currentState = addMarkToState(currentState, player1GetNextMove(currentState), 'X'); displayStateNicely(currentState); whoWon = testStateSeeWhoWins(currentState); if (whoWon != ' ') break; currentState = addMarkToState(currentState, player2GetNextMove(currentState), 'O'); displayStateNicely(currentState); whoWon = testStateSeeWhoWins(currentState); if (whoWon != ' ') break; } } counter++; if (whoWon == 'O') { //Reward O by adding beads to it's winning strategy group, if there are more //than maxBeadsPerCup in the cup, rob from another memory location. //Punish X by removing beads to it's losing strategy group if there are more //than maxBeadsPerCup in the cup, rob from another memory location. System.out.println("player2 won"); if (player2Learns) { for(int x = 0; x < player2CupsDrawnFromArray.size(); x++) { player2_add_beads_to_cup(player2CupsDrawnFromArray.get(x), player2ColorDrawnFromCupArray.get(x), 8); } } if (player1Learns) { for(int x = 0; x < player1CupsDrawnFromArray.size(); x++) { player1RemoveBead(player1CupsDrawnFromArray.get(x), player1ColorDrawnFromCupArray.get(x), 8); } } player2Wins++; } else if (whoWon == 'X') { //Reward X by adding beads to it's winning strategy group.if there are more //than maxBeadsPerCup in the cup, rob from another memory location. //Punish O by removing beads to it's losing strategy group.if there are more //than maxBeadsPerCup in the cup, rob from another memory location. System.out.println("player1 X won"); if (player2Learns) { for(int x = 0; x < player2CupsDrawnFromArray.size(); x++) { player2RemoveBead(player2CupsDrawnFromArray.get(x), player2ColorDrawnFromCupArray.get(x), 8); } } if (player1Learns) { for(int x = 0; x < player1CupsDrawnFromArray.size(); x++) { player1_add_beads_to_cup(player1CupsDrawnFromArray.get(x), player1ColorDrawnFromCupArray.get(x), 8); } } player1Wins++; } else if (whoWon == 'D') { //Was a draw, punish both. if (player2Learns) for(int x = 0; x < player2CupsDrawnFromArray.size(); x++) { //Cycling through each Cup We drew from. player2RemoveBead(player2CupsDrawnFromArray.get(x), player2ColorDrawnFromCupArray.get(x), 9); } if (player1Learns) for(int x = 0; x < player1CupsDrawnFromArray.size(); x++) { //Cycling through each Cup We drew from. player1RemoveBead(player1CupsDrawnFromArray.get(x), player1ColorDrawnFromCupArray.get(x), 9); } } else { throw new RuntimeException("Bad response from testStateSeeWhoWins "); } } System.out.println(""); System.out.println(""); System.out.println("Player1 'X':"); for(int x = 0; x < player1StateArray.size(); x++) { displayStateNicely(player1StateArray.get(x)); System.out.println("X to move, cupContents: '" + player1CupContents.get(x) + "'"); } /* System.out.println(""); System.out.println("Player2 'O':"); for(int x = 0; x < player2StateArray.size(); x++) { displayStateNicely(player2StateArray.get(x)); System.out.println("O to move, cupContents: '" + player2CupContents.get(x) + "'"); } */ System.out.println(""); System.out.println("Player1Wins 'X': " + player1Wins); System.out.println("Player2Wins 'O': " + player2Wins); System.out.println("Player1Started: " + player1Started); System.out.println("Player2Started: " + player2Started); } public static void player1RemoveBead(int index, int beadColor, int cnt) { //Remove bead from cup if it's there, if not, don't do anything. for(int x = 0; x < cnt; x++) { if (player1CupContents.get(index).length() < 2) return; if (player1CupContents.get(index).lastIndexOf(beadColor + "") != player1CupContents.get(index).indexOf(beadColor + "")) player1CupContents.set(index, player1CupContents.get(index).replaceFirst(beadColor + "", "")); else break; } } public static void player2RemoveBead(int index, int beadColor, int cnt) { //Remove bead from cup if it's there, if not, don't do anything. for(int x = 0; x < cnt; x++) { if (player2CupContents.get(index).length() < 2) return; if (player2CupContents.get(index).lastIndexOf(beadColor + "") != player2CupContents.get(index).indexOf(beadColor + "")) player2CupContents.set(index, player2CupContents.get(index).replaceFirst(beadColor + "", "")); else break; } } public static void player2_add_beads_to_cup(int index, int beadColor, int cnt) { //if there is < maxBeadsPerCup in the state, just add it //if more than maxBeadsPerCup, then gotta delete to make room. if (player2CupContents.get(index).length() < maxBeadsPerCup) { for(int x = 0; x < cnt; x++) player2CupContents.set(index, player2CupContents.get(index) + beadColor); } else { //need to erase some memory to make room. Get the index of the //first spot that is not beadColor, strengthening the good knowledge for(int x = 0; x < player2CupContents.get(index).length(); x++) { if (Integer.parseInt(player2CupContents.get(index).charAt(x) + "") != Integer.toString(beadColor).charAt(0)) { char[] charArray = player2CupContents.get(index).toCharArray(); charArray[x] = Integer.toString(beadColor).charAt(0); player2CupContents.set(index, new String(charArray)); return; } } //if we get to this point, it means we have 100 positions full of the beadColor //we want in there. lol it seems pretty sure of itself. Let it be. } } public static void player1_add_beads_to_cup(int index, int beadColor, int cnt) { //if there is < maxBeadsPerCup in the state, just add it //if more than maxBeadsPerCup, then gotta delete to make room. if (beadColor < 0 || beadColor > 8) throw new RuntimeException("player1_add_beads_to_cup invalid beadColor: " + beadColor + beadColor + beadColor + beadColor); if (player1CupContents.get(index).length() < maxBeadsPerCup) { for(int x = 0; x < cnt; x++) player1CupContents.set(index, player1CupContents.get(index) + beadColor); } else { //If we are over the top, then we need to erase some memory to make room. //Get the index of the first spot that is not beadColor for(int x = 0; x < player1CupContents.get(index).length(); x++) { if (Integer.parseInt(player1CupContents.get(index).charAt(x) + "") != Integer.toString(beadColor).charAt(0)) { char[] charArray = player1CupContents.get(index).toCharArray(); charArray[x] = Integer.toString(beadColor).charAt(0); player1CupContents.set(index, new String(charArray)); return; } } //if we get to this point, it means we have 100 positions full of the beadColor //we want in there. lol it seems pretty sure of itself. Let it be. } } public static char testStateSeeWhoWins(String state) { //returns ' ' if nobody has won yet //returns 'X' if X has won. //returns 'O' if O has won. ArrayList<String> winningPositions = new ArrayList<String>(); //If the following spots have all the same 'X' or 'O' then that wins. winningPositions.add("012"); winningPositions.add("345"); winningPositions.add("678"); winningPositions.add("036"); winningPositions.add("147"); winningPositions.add("258"); winningPositions.add("048"); winningPositions.add("246"); for(int x = 0; x < winningPositions.size(); x++) { if (state.charAt(Integer.parseInt(winningPositions.get(x).charAt(0) + "")) != ' ' && state.charAt(Integer.parseInt(winningPositions.get(x).charAt(0) + "")) == state.charAt(Integer.parseInt(winningPositions.get(x).charAt(1) + "")) && state.charAt(Integer.parseInt(winningPositions.get(x).charAt(1) + "")) == state.charAt(Integer.parseInt(winningPositions.get(x).charAt(2) + ""))) { return state.charAt(Integer.parseInt(winningPositions.get(x).charAt(0) + "")); } } //Test to see if it's a draw. if (state.contains(" ") == false) return 'D'; return ' '; } public static String addMarkToState(String state, int position, char mark) { //This function puts mark into state at position if (position > 8 || position < 0) throw new RuntimeException("addMarkToState position out of range: " + position); if (mark != ' ' && mark != 'X' && mark != 'O') throw new RuntimeException("addMarkToState mark not correct"); char[] charArray = state.toCharArray(); charArray[position] = mark; return new String(charArray); } public static void displayStateNicely(String state) { System.out.println(""); System.out.println(" " + state.charAt(0) + " | " + state.charAt(1) + " | " + state.charAt(2)); System.out.println("---|---|---"); System.out.println(" " + state.charAt(3) + " | " + state.charAt(4) + " | " + state.charAt(5)); System.out.println("---|---|---"); System.out.println(" " + state.charAt(6) + " | " + state.charAt(7) + " | " + state.charAt(8)); } public static int player2GetNextMove(String currentState) { //player 2 is always 'O' (the letter). Given the passed in state, //it returns the index of where the 'O' should go to win. //If the state is unknown, we gotta add a default cup contents int stateIndex = player2_get_index_of_state(currentState); int randomMove = -1; if (testStateSeeWhoWins(currentState) != ' ') throw new RuntimeException("Your asking a player to move but someone has already won or there is a draw"); if (stateIndex == -1) { player2StateArray.add(currentState); player2CupContents.add(getInitialCupContentsByState(currentState)); stateIndex = player2CupContents.size()-1; } randomMove = player2_get_random_move_from_cup_contents(stateIndex); //We have to remember what color bead we picked from which cup so we can //reward or punish this player2 after we find out if we win or lose. player2CupsDrawnFromArray.add(stateIndex); player2ColorDrawnFromCupArray.add(randomMove); return randomMove; } public static int player1GetNextMove(String currentState) { //player 1 is always 'X'. Given the passed in state, //it returns the index of where the 'X' should go to win. if (testStateSeeWhoWins(currentState) != ' ') throw new RuntimeException("Your asking a player to move but someone has already won or there is a draw"); //If the state is unknown, we gotta add a default cup contents int stateIndex = player1_get_index_of_state(currentState); int randomMove = -1; if (stateIndex == -1) { player1StateArray.add(currentState); player1CupContents.add(getInitialCupContentsByState(currentState)); stateIndex = player1CupContents.size()-1; } randomMove = player1_get_random_move_from_cup_contents(stateIndex); //We have to remember what color bead we picked from which cup so we can //reward or punish this player2 after we find out if we win or lose. player1CupsDrawnFromArray.add(stateIndex); player1ColorDrawnFromCupArray.add(randomMove); return randomMove; } public static int player2_get_random_move_from_cup_contents(int stateIndex) { //returns the index of where we put our mark; //we know that we are X. //pick a random position of this string. Random r = new Random(); int randPosition = r.nextInt(player2CupContents.get(stateIndex).length()); //System.out.println("RandPosition: '" + randPosition + "'"); return Integer.parseInt(player2CupContents.get(stateIndex).charAt(randPosition) + ""); } public static int player1_get_random_move_from_cup_contents(int stateIndex) { //returns the index of where we put our mark; //we know that we are X. //pick a random position of this string. Random r = new Random(); int randPosition = r.nextInt(player1CupContents.get(stateIndex).length()); //System.out.println("RandPosition: '" + randPosition + "'"); return Integer.parseInt(player1CupContents.get(stateIndex).charAt(randPosition) + ""); } public static int player2_get_index_of_state(String state) { //checks player1StateArray for our state. //if the state does not exist return -1; for(int x = 0; x < player2StateArray.size(); x++) { if (state.equals(player2StateArray.get(x))) { return x; } } return -1; } public static int player1_get_index_of_state(String state) { //checks player1StateArray for our state. //if the state does not exist return -1; for(int x = 0; x < player1StateArray.size(); x++) { if (state.equals(player1StateArray.get(x))) { return x; } } return -1; } public static String getInitialCupContentsByState(String state) { //This is Initialization, simulates filling the cups with the //beads before we start teaching the algorithm. //The contents of the cup will be a string. //The maximum length of the string will be 100 characters //representing 100 beads. //0 = upper left //1 = upper middle //2 = upper right //3 = middle left //etc //"000011112222" means that there is a 33% chance we choose upper left spot for our mark. //The default cup contents are 4 beads of each valid move. if (testStateSeeWhoWins(state) == 'D') throw new RuntimeException("Error in getInitialCupContentByState, you shouldn't be looking at a state with a full board"); String cupContents = ""; char blank = ' '; int beadStartCnt = 10; for(int index = 0; index < state.length(); index++) { if (state.charAt(index) == blank) { for(int x = 0; x < beadStartCnt; x++) cupContents += "" + index; } } return cupContents; } public static String getBlankState() { // 012345678 return " "; } }