package examples.snake; import io.termd.core.tty.TtyConnection; import io.termd.core.util.Vector; import java.util.HashSet; import java.util.LinkedList; import java.util.Random; import java.util.concurrent.TimeUnit; import java.util.function.Consumer; /** * The snake game implementation, fully non blocking, one thread to handle all players : massive scalability */ public class SnakeGame implements Consumer<TtyConnection> { @Override public void accept(TtyConnection conn) { if (conn.size() != null) { new Game(conn).execute(); } else { conn.setSizeHandler(size -> new Game(conn).execute()); } } enum Direction { LEFT, RIGHT, UP, DOWN } /** * The game automaton state */ class GameState { final int width, height; HashSet<Vector> tiles; LinkedList<Vector> snake = new LinkedList<>(); Direction direction; GameState(int width, int height, int size) { this.width = width; this.height = height; tiles = new HashSet<>(); while (size > 0) { int x = new Random().nextInt(width); int y = new Random().nextInt(height); Vector tile = new Vector(x, y); if (tiles.add(tile)) { size--; } } snake.addFirst(new Vector(0, 0)); snake.addFirst(new Vector(1, 0)); snake.addFirst(new Vector(2, 0)); snake.addFirst(new Vector(3, 0)); direction = Direction.RIGHT; } /** * Update the state with one game iteration * * @throws Exception when user lose */ void update() throws Exception { Vector curr = snake.peekFirst(); Vector next = null; switch (direction) { case RIGHT: next = new Vector(curr.x() + 1, curr.y()); break; case LEFT: next = new Vector(curr.x() - 1, curr.y()); break; case UP: next = new Vector(curr.x(), curr.y() - 1); break; case DOWN: next = new Vector(curr.x(), curr.y() + 1); break; } if (next.x() < 0 || next.x() >= width || next.y() < 0 || next.y() >= height || snake.contains(next)) { throw new Exception("lost"); } if (!tiles.remove(next)) { // Eat a tile : grow snake.removeLast(); } snake.addFirst(next); } } /** * The game itself. */ class Game { final TtyConnection conn; GameState game; boolean interrupted; public Game(TtyConnection conn) { this.conn = conn; // When user resize the screen : launch a new game conn.setSizeHandler(this::reset); // Ctrl-C ends the game conn.setEventHandler((event, ch) -> { switch (event) { case INTR: interrupted = true; conn.close(); break; } }); // Keyboard handling conn.setStdinHandler(keys -> { if (keys.length == 3) { if (keys[0] == 27 && keys[1] == '[') { switch (keys[2]) { case 'A': game.direction = Direction.UP; break; case 'B': game.direction = Direction.DOWN; break; case 'C': game.direction = Direction.RIGHT; break; case 'D': game.direction = Direction.LEFT; break; } } } }); // Init current game reset(conn.size()); } /** * Execute one iteration of the game, at the end schedule the next iteration until user lose or hits Ctrl-C */ void execute() { if (interrupted) { return; } GameState game = this.game; // Compute the ANSI magic string that draws the game StringBuilder buf = new StringBuilder(); for (int y = 0;y < game.height;y++) { buf.append("\033[").append(y + 1).append(";1H\033[K"); } for (Vector tile : game.tiles) { buf.append("\033[").append(tile.y() + 1).append(";").append(tile.x() + 1).append("H").append("X"); } for (Vector tile : game.snake) { buf.append("\033[").append(tile.y() + 1).append(";").append(tile.x() + 1).append("H").append('0'); } buf.append("\033[").append(game.height).append(";").append(game.width).append("H\033[K"); // Update screen conn.write(buf.toString()); // Now update game and handle losing the game try { game.update(); } catch (Exception e) { conn.write("YOU LOST"); conn.close(); return; } // Schedule a new execution of the game conn.schedule(this::execute, 500, TimeUnit.MILLISECONDS); } private void reset(Vector size) { // Fill factory area / 25 game = new GameState(size.x(), size.y(), (size.x() * size.y()) / 10); } } }