diff --git a/algorithms-miscellaneous-2/src/main/java/com/baeldung/algorithms/play2048/Board.java b/algorithms-miscellaneous-2/src/main/java/com/baeldung/algorithms/play2048/Board.java new file mode 100644 index 0000000000..ddb5901682 --- /dev/null +++ b/algorithms-miscellaneous-2/src/main/java/com/baeldung/algorithms/play2048/Board.java @@ -0,0 +1,208 @@ +package com.baeldung.algorithms.play2048; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.LinkedList; +import java.util.List; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class Board { + private static final Logger LOG = LoggerFactory.getLogger(Board.class); + + private final int[][] board; + + private final int score; + + public Board(int size) { + assert(size > 0); + + this.board = new int[size][]; + this.score = 0; + + for (int x = 0; x < size; ++x) { + this.board[x] = new int[size]; + for (int y = 0; y < size; ++y) { + board[x][y] = 0; + } + } + } + + private Board(int[][] board, int score) { + this.score = score; + this.board = new int[board.length][]; + + for (int x = 0; x < board.length; ++x) { + this.board[x] = Arrays.copyOf(board[x], board[x].length); + } + } + + public int getSize() { + return board.length; + } + + public int getScore() { + return score; + } + + public int getCell(Cell cell) { + int x = cell.getX(); + int y = cell.getY(); + assert(x >= 0 && x < board.length); + assert(y >= 0 && y < board.length); + + return board[x][y]; + } + + public boolean isEmpty(Cell cell) { + return getCell(cell) == 0; + } + + public List emptyCells() { + List result = new ArrayList<>(); + for (int x = 0; x < board.length; ++x) { + for (int y = 0; y < board[x].length; ++y) { + Cell cell = new Cell(x, y); + if (isEmpty(cell)) { + result.add(cell); + } + } + } + return result; + } + + public Board placeTile(Cell cell, int number) { + if (!isEmpty(cell)) { + throw new IllegalArgumentException("That cell is not empty"); + } + + Board result = new Board(this.board, this.score); + result.board[cell.getX()][cell.getY()] = number; + return result; + } + + public Board move(Move move) { + // Clone the board + int[][] tiles = new int[this.board.length][]; + for (int x = 0; x < this.board.length; ++x) { + tiles[x] = Arrays.copyOf(this.board[x], this.board[x].length); + } + + LOG.debug("Before move: {}", Arrays.deepToString(tiles)); + // If we're doing an Left/Right move then transpose the board to make it a Up/Down move + if (move == Move.LEFT || move == Move.RIGHT) { + tiles = transpose(tiles); + LOG.debug("After transpose: {}", Arrays.deepToString(tiles)); + } + // If we're doing a Right/Down move then reverse the board. + // With the above we're now always doing an Up move + if (move == Move.DOWN || move == Move.RIGHT) { + tiles = reverse(tiles); + LOG.debug("After reverse: {}", Arrays.deepToString(tiles)); + } + LOG.debug("Ready to move: {}", Arrays.deepToString(tiles)); + + // Shift everything up + int[][] result = new int[tiles.length][]; + int newScore = 0; + for (int x = 0; x < tiles.length; ++x) { + LinkedList thisRow = new LinkedList<>(); + for (int y = 0; y < tiles[0].length; ++y) { + if (tiles[x][y] > 0) { + thisRow.add(tiles[x][y]); + } + } + + LOG.debug("Unmerged row: {}", thisRow); + LinkedList newRow = new LinkedList<>(); + while (thisRow.size() >= 2) { + int first = thisRow.pop(); + int second = thisRow.peek(); + LOG.debug("Looking at numbers {} and {}", first, second); + if (second == first) { + LOG.debug("Numbers match, combining"); + int newNumber = first * 2; + newRow.add(newNumber); + newScore += newNumber; + thisRow.pop(); + } else { + LOG.debug("Numbers don't match"); + newRow.add(first); + } + } + newRow.addAll(thisRow); + LOG.debug("Merged row: {}", newRow); + + result[x] = new int[tiles[0].length]; + for (int y = 0; y < tiles[0].length; ++y) { + if (newRow.isEmpty()) { + result[x][y] = 0; + } else { + result[x][y] = newRow.pop(); + } + } + } + LOG.debug("After moves: {}", Arrays.deepToString(result)); + + // Un-reverse the board + if (move == Move.DOWN || move == Move.RIGHT) { + result = reverse(result); + LOG.debug("After reverse: {}", Arrays.deepToString(result)); + } + // Un-transpose the board + if (move == Move.LEFT || move == Move.RIGHT) { + result = transpose(result); + LOG.debug("After transpose: {}", Arrays.deepToString(result)); + } + return new Board(result, this.score + newScore); + } + + private static int[][] transpose(int[][] input) { + int[][] result = new int[input.length][]; + + for (int x = 0; x < input.length; ++x) { + result[x] = new int[input[0].length]; + for (int y = 0; y < input[0].length; ++y) { + result[x][y] = input[y][x]; + } + } + + return result; + } + + private static int[][] reverse(int[][] input) { + int[][] result = new int[input.length][]; + + for (int x = 0; x < input.length; ++x) { + result[x] = new int[input[0].length]; + for (int y = 0; y < input[0].length; ++y) { + result[x][y] = input[x][input.length - y - 1]; + } + } + + return result; + } + + @Override + public String toString() { + return Arrays.deepToString(board); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + Board board1 = (Board) o; + return Arrays.deepEquals(board, board1.board); + } + + @Override + public int hashCode() { + return Arrays.deepHashCode(board); + } +} diff --git a/algorithms-miscellaneous-2/src/main/java/com/baeldung/algorithms/play2048/Cell.java b/algorithms-miscellaneous-2/src/main/java/com/baeldung/algorithms/play2048/Cell.java new file mode 100644 index 0000000000..98ee23ac90 --- /dev/null +++ b/algorithms-miscellaneous-2/src/main/java/com/baeldung/algorithms/play2048/Cell.java @@ -0,0 +1,26 @@ +package com.baeldung.algorithms.play2048; + +import java.util.StringJoiner; + +public class Cell { + private int x; + private int y; + + public Cell(int x, int y) { + this.x = x; + this.y = y; + } + + public int getX() { + return x; + } + + public int getY() { + return y; + } + + @Override + public String toString() { + return new StringJoiner(", ", Cell.class.getSimpleName() + "[", "]").add("x=" + x).add("y=" + y).toString(); + } +} diff --git a/algorithms-miscellaneous-2/src/main/java/com/baeldung/algorithms/play2048/Computer.java b/algorithms-miscellaneous-2/src/main/java/com/baeldung/algorithms/play2048/Computer.java new file mode 100644 index 0000000000..5ef1d04368 --- /dev/null +++ b/algorithms-miscellaneous-2/src/main/java/com/baeldung/algorithms/play2048/Computer.java @@ -0,0 +1,27 @@ +package com.baeldung.algorithms.play2048; + +import java.security.SecureRandom; +import java.util.List; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class Computer { + private static final Logger LOG = LoggerFactory.getLogger(Computer.class); + + private final SecureRandom rng = new SecureRandom(); + + public Board makeMove(Board input) { + List emptyCells = input.emptyCells(); + LOG.info("Number of empty cells: {}", emptyCells.size()); + + double numberToPlace = rng.nextDouble(); + LOG.info("New number probability: {}", numberToPlace); + + int indexToPlace = rng.nextInt(emptyCells.size()); + Cell cellToPlace = emptyCells.get(indexToPlace); + LOG.info("Placing number into empty cell: {}", cellToPlace); + + return input.placeTile(cellToPlace, numberToPlace >= 0.9 ? 4 : 2); + } +} diff --git a/algorithms-miscellaneous-2/src/main/java/com/baeldung/algorithms/play2048/Human.java b/algorithms-miscellaneous-2/src/main/java/com/baeldung/algorithms/play2048/Human.java new file mode 100644 index 0000000000..d1b32f0309 --- /dev/null +++ b/algorithms-miscellaneous-2/src/main/java/com/baeldung/algorithms/play2048/Human.java @@ -0,0 +1,126 @@ +package com.baeldung.algorithms.play2048; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Comparator; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.apache.commons.math3.util.Pair; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class Human { + private static final Logger LOG = LoggerFactory.getLogger(Human.class); + + public Board makeMove(Board input) { + // For each move in MOVE + // Generate board from move + // Generate Score for Board + // Return board with the best score + // + // Generate Score + // If Depth Limit + // Return Final Score + // Total Score = 0 + // For every empty square in new board + // Generate board with "2" in square + // Calculate Score + // Total Score += (Score * 0.9) + // Generate board with "4" in square + // Calculate Score + // Total Score += (Score * 0.1) + // + // Calculate Score + // For each move in MOVE + // Generate board from move + // Generate score for board + // Return the best generated score + + return Arrays.stream(Move.values()) + .parallel() + .map(input::move) + .filter(board -> !board.equals(input)) + .max(Comparator.comparingInt(board -> generateScore(board, 0))) + .orElse(input); + } + + private int generateScore(Board board, int depth) { + if (depth >= 3) { + int finalScore = calculateFinalScore(board); + LOG.debug("Final score for board {}: {}", board,finalScore); + return finalScore; + } + + return board.emptyCells().stream() + .parallel() + .flatMap(cell -> Stream.of(new Pair<>(cell, 2), new Pair<>(cell, 4))) + .mapToInt(move -> { + LOG.debug("Simulating move {} at depth {}", move, depth); + Board newBoard = board.placeTile(move.getFirst(), move.getSecond()); + int boardScore = calculateScore(newBoard, depth + 1); + int calculatedScore = (int) (boardScore * (move.getSecond() == 2 ? 0.9 : 0.1)); + LOG.debug("Calculated score for board {} and move {} at depth {}: {}", newBoard, move, depth, calculatedScore); + return calculatedScore; + }) + .sum(); + } + + private int calculateScore(Board board, int depth) { + return Arrays.stream(Move.values()) + .parallel() + .map(board::move) + .filter(moved -> !moved.equals(board)) + .mapToInt(newBoard -> generateScore(newBoard, depth)) + .max() + .orElse(0); + } + + private int calculateFinalScore(Board board) { + List> rowsToScore = new ArrayList<>(); + for (int i = 0; i < board.getSize(); ++i) { + List row = new ArrayList<>(); + List col = new ArrayList<>(); + + for (int j = 0; j < board.getSize(); ++j) { + row.add(board.getCell(new Cell(i, j))); + col.add(board.getCell(new Cell(j, i))); + } + + rowsToScore.add(row); + rowsToScore.add(col); + } + + return rowsToScore.stream() + .parallel() + .mapToInt(row -> { + List preMerged = row.stream() + .filter(value -> value != 0) + .collect(Collectors.toList()); + + int numMerges = 0; + int monotonicityLeft = 0; + int monotonicityRight = 0; + for (int i = 0; i < preMerged.size() - 1; ++i) { + Integer first = preMerged.get(i); + Integer second = preMerged.get(i + 1); + if (first.equals(second)) { + ++numMerges; + } else if (first > second) { + monotonicityLeft += first - second; + } else { + monotonicityRight += second - first; + } + } + + int score = 1000; + score += 250 * row.stream().filter(value -> value == 0).count(); + score += 750 * numMerges; + score -= 10 * row.stream().mapToInt(value -> value).sum(); + score -= 50 * Math.min(monotonicityLeft, monotonicityRight); + return score; + }) + .sum(); + } +} diff --git a/algorithms-miscellaneous-2/src/main/java/com/baeldung/algorithms/play2048/Move.java b/algorithms-miscellaneous-2/src/main/java/com/baeldung/algorithms/play2048/Move.java new file mode 100644 index 0000000000..8678cec833 --- /dev/null +++ b/algorithms-miscellaneous-2/src/main/java/com/baeldung/algorithms/play2048/Move.java @@ -0,0 +1,8 @@ +package com.baeldung.algorithms.play2048; + +public enum Move { + UP, + DOWN, + LEFT, + RIGHT +} diff --git a/algorithms-miscellaneous-2/src/main/java/com/baeldung/algorithms/play2048/Play2048.java b/algorithms-miscellaneous-2/src/main/java/com/baeldung/algorithms/play2048/Play2048.java new file mode 100644 index 0000000000..ec5b3dca40 --- /dev/null +++ b/algorithms-miscellaneous-2/src/main/java/com/baeldung/algorithms/play2048/Play2048.java @@ -0,0 +1,73 @@ +package com.baeldung.algorithms.play2048; + +public class Play2048 { + private static final int SIZE = 3; + private static final int INITIAL_NUMBERS = 2; + + public static void main(String[] args) { + // The board and players + Board board = new Board(SIZE); + Computer computer = new Computer(); + Human human = new Human(); + + // The computer has two moves first + System.out.println("Setup"); + System.out.println("====="); + for (int i = 0; i < INITIAL_NUMBERS; ++i) { + board = computer.makeMove(board); + } + + printBoard(board); + do { + board = human.makeMove(board); + System.out.println("Human move"); + System.out.println("=========="); + printBoard(board); + + board = computer.makeMove(board); + System.out.println("Computer move"); + System.out.println("============="); + printBoard(board); + } while (!board.emptyCells().isEmpty()); + + System.out.println("Final Score: " + board.getScore()); + + } + + private static void printBoard(Board board) { + StringBuilder topLines = new StringBuilder(); + StringBuilder midLines = new StringBuilder(); + for (int x = 0; x < board.getSize(); ++x) { + topLines.append("+--------"); + midLines.append("| "); + } + topLines.append("+"); + midLines.append("|"); + + + for (int y = 0; y < board.getSize(); ++y) { + System.out.println(topLines); + System.out.println(midLines); + for (int x = 0; x < board.getSize(); ++x) { + Cell cell = new Cell(x, y); + System.out.print("|"); + if (board.isEmpty(cell)) { + System.out.print(" "); + } else { + StringBuilder output = new StringBuilder(Integer.toString(board.getCell(cell))); + while (output.length() < 8) { + output.append(" "); + if (output.length() < 8) { + output.insert(0, " "); + } + } + System.out.print(output); + } + } + System.out.println("|"); + System.out.println(midLines); + } + System.out.println(topLines); + System.out.println("Score: " + board.getScore()); + } +}