Last active
October 21, 2021 17:42
-
-
Save YashRE42/27e40a6d20181f34cba6d265b042c44e to your computer and use it in GitHub Desktop.
Program to play hex with reset (http://www.lutanho.net/play/hex.html) based on the input format at https://www.hackerearth.com/problem/algorithm/minimax-4/ and https://www.hackerearth.com/problem/algorithm/minimax-again/. Please see the comment after the gist.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import java.util.ArrayList; | |
import java.util.Arrays; | |
import java.util.HashMap; | |
import java.util.LinkedList; | |
import java.util.PriorityQueue; | |
import java.util.Queue; | |
import java.util.Scanner; | |
import java.util.Vector; | |
// rows are alphabetic | |
// columns are numeric | |
// player one is maximizing player | |
// player one is trying to join alphabetic sides | |
// reset[0] is player 1's reset counter (note: 0 indexing) | |
// reset[1] is player 2's reset counter (note: 0 indexing) | |
// reset counters are 0 if reset has not been used | |
public class SolutionToMinimaxAgain { | |
public static void main(String[] args) throws Exception { | |
int test_cases; | |
Scanner scan = new Scanner(System.in); | |
test_cases = scan.nextInt(); | |
while (test_cases-->0) { | |
int size = scan.nextInt(); | |
int max_depth = scan.nextInt(); | |
int[][] grid = new int[size][size]; | |
int count_player_one = 0; | |
int count_player_two = 0; | |
for (int i = 0; i < size; i++) { | |
for (int j = 0; j < size; j++) { | |
grid[i][j] = scan.nextInt(); | |
if (grid[i][j] == 1) { | |
count_player_one++; | |
} | |
if (grid[i][j] == 2) { | |
count_player_two++; | |
} | |
} | |
} | |
// Either player one has made 1 more move than player two | |
// or player one and player two have made the same number of moves | |
Boolean is_player_one_turn = count_player_one == count_player_two; | |
Hex hex = new Hex(size, max_depth, grid, is_player_one_turn); | |
hex.run(grid, is_player_one_turn); | |
} | |
scan.close(); | |
} | |
} | |
class Hex { | |
int size; | |
int max_depth; | |
int[][] grid; | |
Boolean is_player_one_turn; | |
public Hex (int size, int max_depth, int[][] grid, Boolean is_player_one_turn) { | |
this.size = size; | |
this.max_depth = max_depth; | |
this.grid = grid; | |
this.is_player_one_turn = is_player_one_turn; | |
} | |
void run (int[][] board, Boolean is_player_one_turn) throws Exception { | |
int[] resets = new int[]{0, 0}; | |
int winner = find_winner(board, resets); | |
while (winner == 0) { | |
int current_player = is_player_one_turn?1:2; | |
String current_move = minimax_decision(board, resets, is_player_one_turn); | |
if (current_move.length() > 3) { // ie if move is a reset | |
int[] indexes = generate_indexes_from_move_string(current_move); | |
board[indexes[0]][indexes[1]] = 0; | |
resets[current_player - 1] = 1; // resets array is 0 indexed | |
System.out.println(current_player + " reset " + alphabetize(current_move)); | |
} else { | |
int[] indexes = generate_indexes_from_move_string(current_move); | |
board[indexes[0]][indexes[1]] = current_player; | |
System.out.println(current_player + " " + alphabetize(current_move)); | |
} | |
winner = find_winner(board, resets); | |
if (!(current_move.length() > 3)) { // ie if move is not a reset | |
is_player_one_turn = !is_player_one_turn; | |
} | |
} | |
System.out.println("player " + winner + " wins"); | |
} | |
int find_winner (int[][] board, int[] resets) { | |
Queue<Point> queue = new LinkedList<Point>(); | |
HashMap<String, Integer> visited = new HashMap<String, Integer>(); | |
// player 1, we're trying to join first and last column; | |
for (int i = 0; i < size; i++) { | |
if (board[i][0] == 1) { | |
queue.add(new Point(i, 0)); | |
} | |
} | |
while(!queue.isEmpty()) { | |
Point top = queue.poll(); | |
if (top.j == size - 1) { | |
return 1; | |
} | |
// else | |
visited.put(top.toString(), 1); | |
ArrayList<Point> children = get_neighbours (top, visited); | |
for (Point e : children) { | |
if (board[e.i][e.j] == 1) { | |
queue.add(e); | |
} | |
} | |
} | |
queue = new LinkedList<Point>(); | |
visited = new HashMap<String, Integer>(); | |
// player 2, we're trying to join first and last row; | |
for (int i = 0; i < size; i++) { | |
if (board[0][i] == 2) { | |
Point p = new Point(0, i); | |
queue.add(p); | |
} | |
} | |
while(!queue.isEmpty()) { | |
Point top = queue.poll(); | |
if (top.i == size - 1) { | |
return 2; | |
} | |
// else | |
visited.put(top.toString(), 2); | |
ArrayList<Point> children = get_neighbours (top, visited); | |
for (Point e : children) { | |
if (board[e.i][e.j] == 2) { | |
queue.add(e); | |
} | |
} | |
} | |
return 0; | |
} | |
ArrayList<Point> get_neighbours (Point p, HashMap<String, Integer> visited) { | |
int i = p.i; | |
int j = p.j; | |
ArrayList<Point> children = new ArrayList<Point>(); | |
validate_and_add_to_list(children, new Point(i-1, j), visited); | |
validate_and_add_to_list(children, new Point(i-1, j+1), visited); | |
validate_and_add_to_list(children, new Point(i, j+1), visited); | |
validate_and_add_to_list(children, new Point(i, j-1), visited); | |
validate_and_add_to_list(children, new Point(i+1, j), visited); | |
validate_and_add_to_list(children, new Point(i+1, j-1), visited); | |
return children; | |
} | |
void validate_and_add_to_list (ArrayList<Point> list, Point p, HashMap<String, Integer> visited) { | |
if ( | |
p.i != -1 && p.j != -1 && | |
p.i != size && p.j != size && | |
!visited.containsKey(p.toString()) ) { | |
list.add(p); | |
} | |
} | |
String minimax_decision (int[][] board, int[] resets, Boolean is_player_one_turn) throws Exception { | |
Move_score best; | |
if (is_player_one_turn) { | |
best = new Move_score("", Integer.MIN_VALUE); | |
} else { | |
best = new Move_score("", Integer.MAX_VALUE); | |
} | |
Vector<String> moves = get_all_next_moves(board, resets, is_player_one_turn); | |
int[] best_indexes = new int[]{Integer.MAX_VALUE,Integer.MAX_VALUE}; | |
for (String move : moves) { | |
int score; | |
int[] indexes; | |
int current_player = is_player_one_turn?1:2; | |
if (move.length() > 3) { // ie if move is a reset | |
indexes = generate_indexes_from_move_string(move); | |
resets[current_player - 1] = 1; // resets array is 0 indexed | |
board[indexes[0]][indexes[1]] = 0; | |
score = minimax_value(1, board, resets, is_player_one_turn, Integer.MIN_VALUE, Integer.MAX_VALUE); | |
} else { | |
indexes = generate_indexes_from_move_string(move); | |
board[indexes[0]][indexes[1]] = current_player; | |
score = minimax_value(1, board, resets, !is_player_one_turn, Integer.MIN_VALUE, Integer.MAX_VALUE); | |
} | |
if (is_player_one_turn) { | |
if (score > best.score) { | |
best.move = move; | |
best.score = score; | |
best_indexes = indexes; | |
} else if (score == best.score) { | |
if (move.length() == best.move.length()) { | |
if (indexes[0] < best_indexes[0]) { | |
best.move = move; | |
} else if (indexes[0] == best_indexes[0]) { | |
if (indexes[1] < best_indexes[1]) { | |
best.move = move; | |
} | |
} | |
} else { | |
if (move.length() < best.move.length()) { | |
best.move = move; | |
} | |
} | |
} | |
} else { | |
if (score < best.score) { | |
best.move = move; | |
best.score = score; | |
best_indexes = indexes; | |
} else if (score == best.score) { | |
if (move.length() == best.move.length()) { | |
if (indexes[0] < best_indexes[0]) { | |
best.move = move; | |
} else if (indexes[0] == best_indexes[0]) { | |
if (indexes[1] < best_indexes[1]) { | |
best.move = move; | |
} | |
} | |
} else { | |
if (move.length() < best.move.length()) { | |
best.move = move; | |
} | |
} | |
} | |
} | |
// we undo any changes we made while trying this move, | |
// so that we can try the next move. | |
if (move.length() > 3) { // ie if move is a reset | |
int opponent = is_player_one_turn?2:1; | |
board[indexes[0]][indexes[1]] = opponent; | |
resets[current_player - 1] = 0; // resets array is 0 indexed | |
} else { | |
board[indexes[0]][indexes[1]] = 0; | |
} | |
} | |
return best.move; | |
} | |
Vector<String> get_all_next_moves (int[][] board, int[] resets, Boolean is_player_one_turn) { | |
int opponent = is_player_one_turn?2:1; | |
int current_player = is_player_one_turn?1:2; | |
Vector<String> moves = new Vector<String>(); | |
for (int i = 0; i < size; i++) { | |
for (int j = 0; j < size; j++) { | |
if (board[i][j] == 0) { | |
moves.add(i + " " + j); | |
} else if (resets[current_player - 1] == 0 && board[i][j] == opponent) { | |
moves.add(i + " " + j + " reset"); | |
} | |
} | |
} | |
return moves; | |
} | |
int[] generate_indexes_from_move_string (String move) { | |
int[] indexes = new int[]{move.charAt(0) - 48, move.charAt(2) - 48}; | |
return indexes; | |
} | |
int minimax_value (int depth, int[][] board, int[] resets, Boolean is_player_one_turn, int alpha, int beta) throws Exception { | |
int winner = find_winner(board, resets); | |
// 0 implies no winner | |
if (winner != 0) { | |
if (winner == 1) { | |
// 1 will attempt to maximize | |
return 1000; | |
} else { | |
// 2 will attempt to minimize | |
return -1000; | |
} | |
} | |
if (depth == max_depth) { | |
// heuristic evaluation | |
return board_eval(board, resets); | |
} | |
if (is_player_one_turn) { | |
int bestVal = Integer.MIN_VALUE; | |
Vector<String> moves = get_all_next_moves(board, resets, is_player_one_turn); | |
for (String move : moves) { | |
int value; | |
if (move.length() > 3) { // ie if move is a reset | |
int current_player = is_player_one_turn?1:2; | |
int opponent = is_player_one_turn?2:1; | |
int[] reset_indexes = generate_indexes_from_move_string(move); | |
resets[current_player - 1] = 1; // resets array is 0 indexed | |
board[reset_indexes[0]][reset_indexes[1]] = 0; | |
value = minimax_value(depth+1, board, resets, is_player_one_turn, alpha, beta); | |
// undo reset | |
board[reset_indexes[0]][reset_indexes[1]] = opponent; | |
resets[current_player - 1] = 0; // resets array is 0 indexed | |
} else { | |
int[] indexes = generate_indexes_from_move_string(move); | |
board[indexes[0]][indexes[1]] = 1; | |
value = minimax_value(depth+1, board, resets, !is_player_one_turn, alpha, beta); | |
// undo move | |
board[indexes[0]][indexes[1]] = 0; | |
} | |
bestVal = Integer.max(bestVal, value); | |
alpha = Integer.max(alpha, bestVal); | |
if (beta <= alpha) { | |
break; | |
} | |
} | |
return bestVal; | |
} else { | |
int bestVal = Integer.MAX_VALUE; | |
Vector<String> moves = get_all_next_moves(board, resets, is_player_one_turn); | |
for (String move : moves) { | |
int value; | |
if (move.length() > 3) { // ie if move is a reset | |
int current_player = is_player_one_turn?1:2; | |
int opponent = is_player_one_turn?2:1; | |
int[] reset_indexes = generate_indexes_from_move_string(move); | |
resets[current_player - 1] = 1; // resets array is 0 indexed | |
board[reset_indexes[0]][reset_indexes[1]] = 0; | |
value = minimax_value(depth+1, board, resets, is_player_one_turn, alpha, beta); | |
// undo reset | |
board[reset_indexes[0]][reset_indexes[1]] = opponent; | |
resets[current_player - 1] = 0; // resets array is 0 indexed | |
} else { | |
int[] indexes = generate_indexes_from_move_string(move); | |
board[indexes[0]][indexes[1]] = 2; | |
value = minimax_value(depth+1, board, resets, !is_player_one_turn, alpha, beta); | |
// undo move | |
board[indexes[0]][indexes[1]] = 0; | |
} | |
bestVal = Integer.min(bestVal, value); | |
beta = Integer.min(beta, bestVal); | |
if (beta <= alpha) { | |
break; | |
} | |
} | |
return bestVal; | |
} | |
} | |
int board_eval (int[][] board, int[] resets) { | |
// At most each player would need to place markers equal to | |
// the length of the side of the board to win | |
int min_markers_to_win_for_player_1 = size; | |
int min_markers_to_win_for_player_2 = size; | |
for (int i=0; i < size; ++i) { | |
for (int j=0; j< size; ++j) { | |
if (board[i][j] == 1) { | |
int res = find_shortest_path_to_win_for_player_1(new Point(i,j), board); | |
if (res < min_markers_to_win_for_player_1) { | |
min_markers_to_win_for_player_1 = res; | |
} | |
} | |
if (board[i][j] == 2) { | |
int res = find_shortest_path_to_win_for_player_2(new Point(i,j), board); | |
if (res < min_markers_to_win_for_player_2) { | |
min_markers_to_win_for_player_2 = res; | |
} | |
} | |
} | |
} | |
// player 1 wants to maximize this | |
// player 2 wants to minimize this | |
return min_markers_to_win_for_player_2 - min_markers_to_win_for_player_1 + size * (resets[1] - resets[0]); | |
} | |
int find_shortest_path_to_win_for_player_1(Point origin, int[][] board) { | |
PriorityQueue<PQ_node> queue = new PriorityQueue<PQ_node>(); | |
HashMap<String, Integer> visited = new HashMap<String, Integer>(); | |
queue.add(new PQ_node(origin, 0)); | |
PQ_node best_side_a = new PQ_node(origin, size); | |
PQ_node best_side_b = new PQ_node(origin, size); | |
while(!queue.isEmpty()) { | |
PQ_node top = queue.poll(); | |
if (top.p.j == size - 1) { | |
if (best_side_a.unmarked > top.unmarked) { | |
best_side_a = top; | |
} | |
} | |
if (top.p.j == 0) { | |
if (best_side_b.unmarked > top.unmarked) { | |
best_side_b = top; | |
} | |
} | |
visited.put(top.p.toString(), 1); | |
ArrayList<Point> children = get_neighbours (top.p, visited); | |
for (Point e : children) { | |
if (board[e.i][e.j] == 1) { | |
queue.add(new PQ_node(e, top.unmarked)); | |
} | |
if (board[e.i][e.j] == 0) { | |
if (top.unmarked < best_side_a.unmarked || top.unmarked < best_side_b.unmarked) { | |
queue.add(new PQ_node(e, top.unmarked + 1)); | |
} | |
} | |
} | |
} | |
int min_markers_player_1 = best_side_a.unmarked + best_side_b.unmarked; | |
return min_markers_player_1; | |
} | |
int find_shortest_path_to_win_for_player_2(Point origin, int[][] board) { | |
PriorityQueue<PQ_node> queue = new PriorityQueue<PQ_node>(); | |
HashMap<String, Integer> visited = new HashMap<String, Integer>(); | |
queue.add(new PQ_node(origin, 0)); | |
PQ_node best_side_a = new PQ_node(origin, size); | |
PQ_node best_side_b = new PQ_node(origin, size); | |
while(!queue.isEmpty()) { | |
PQ_node top = queue.poll(); | |
if (top.p.i == size - 1) { | |
if (best_side_a.unmarked > top.unmarked) { | |
best_side_a = top; | |
} | |
} | |
if (top.p.i == 0) { | |
if (best_side_b.unmarked > top.unmarked) { | |
best_side_b = top; | |
} | |
} | |
visited.put(top.p.toString(), 2); | |
ArrayList<Point> children = get_neighbours (top.p, visited); | |
for (Point e : children) { | |
if (board[e.i][e.j] == 2) { | |
queue.add(new PQ_node(e, top.unmarked)); | |
} | |
if (board[e.i][e.j] == 0) { | |
if (top.unmarked < best_side_a.unmarked || top.unmarked < best_side_b.unmarked) { | |
queue.add(new PQ_node(e, top.unmarked + 1)); | |
} | |
} | |
} | |
} | |
int min_markers_player_2 = best_side_a.unmarked + best_side_b.unmarked; | |
return min_markers_player_2; | |
} | |
String generate_move_string (int[][] a, int[][] b) { | |
for (int i = 0; i < size; i++) { | |
for (int j = 0; j < size; j++) { | |
if (a[i][j] != b[i][j]) { | |
return i + " " + j; | |
} | |
} | |
} | |
return ""; | |
} | |
String alphabetize (String current) { | |
String result = "" + (char)(Integer.valueOf(current.charAt(0)) + 17); | |
result += current.charAt(2); | |
return result; | |
} | |
void print_board (int[][] board) { | |
for (int i = 0; i < size; i++) { | |
for (int j = 0; j < size; j++) { | |
System.out.print(board[i][j] + " "); | |
} | |
System.out.print("\n"); | |
} | |
} | |
} | |
class Move_score { | |
String move; | |
int score; | |
public Move_score(String move, int score) { | |
this.move = move; | |
this.score = score; | |
} | |
} | |
class PQ_node implements Comparable<PQ_node> { | |
Point p; | |
int unmarked; | |
int f_cost; | |
PQ_node (Point p, int unmarked) { | |
this.p = p; | |
this.unmarked = unmarked; | |
this.f_cost = f_cost + unmarked; | |
} | |
@Override | |
public int compareTo(PQ_node o) { | |
if (this.f_cost == o.f_cost) { | |
return this.unmarked - o.unmarked; | |
} else { | |
return this.f_cost - o.f_cost; | |
} | |
} | |
} | |
class Point { | |
int i; | |
int j; | |
Point (int i, int j) { | |
this.i = i; | |
this.j = j; | |
} | |
Point (int i, int j, Point vec) { | |
this.i = i + vec.i; | |
this.j = j + vec.j; | |
} | |
@Override | |
public String toString() { | |
return this.i + " " + this.j; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Example of a Hex game:
The input and output format is as described at https://www.hackerearth.com/problem/algorithm/minimax-4/ and https://www.hackerearth.com/problem/algorithm/minimax-again/.
While this 500 line program might initially seem complex, understanding the bellow makes it much simpler:
(image taken from page 126 of Artificial Intelligence: A Modern Approach by Peter Norvig and Stuart J. Russell, available at https://www.cin.ufpe.br/~tfl2/artificial-intelligence-modern-approach.9780131038059.25368.pdf)
One difference between the above algorithm and this implementation is that we limit our search using the depth parameter, once the program reaches its max depth, it performs a heuristic board evaluation. In order to do this, we look at every marked point on the board and find the shortest path from that point to the closest edge on the board, it performs uniform-cost search, which is similar to Dijkstra's single source shortest path algorithm with each node having a fixed cost of 1.
Further more, to increase the efficiency of this program and reduce the time cost for certain inputs, this program implements Alpha-Beta pruning:
(image taken from page 132 of Artificial Intelligence: A Modern Approach by Peter Norvig and Stuart J. Russell, available at https://www.cin.ufpe.br/~tfl2/artificial-intelligence-modern-approach.9780131038059.25368.pdf)
We can verify that this code works at https://www.hackerearth.com/submission/64537557/.