Skip to content

Instantly share code, notes, and snippets.

@YashRE42
Last active October 21, 2021 17:42
Show Gist options
  • Save YashRE42/27e40a6d20181f34cba6d265b042c44e to your computer and use it in GitHub Desktop.
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.
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;
}
}
@YashRE42
Copy link
Author

YashRE42 commented Oct 16, 2021

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/.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment