// Game
public class Game {

    private final Grid grid;
    private final Map<Mark, Player> players;
    private final Mark playingMark;
    
    public Game(Map<Mark, Player> players) {
        this(new Grid(), players, Mark.x());
    }
    
    public Game(Grid grid, Map<Mark, Player> players, Mark playingMark) {
        this.grid = grid;
        this.playingMark = playingMark;
        this.players = players;
    }

    public Game submit(Placement placement) {
        placement.validate(playingMark, grid, this);
        return this;
    }
    
    public Game validPlacement(Placement placement) {
        placement.place(grid, this);
        return this;
    }
    
    public Game gridUpdated(Grid newGrid) {
        Mark nextPlayingMark = playingMark.opponent();
        Game newGame = new Game(newGrid, players, nextPlayingMark);
        
        for(Player p : players.values())
            p.updatedGameWithCurrentPlayer(newGame, players.get(nextPlayingMark));
        
        return this;
    }
    
    public Game invalidPlacementByMark(Placement placement, Mark mark) {
        players.get(mark).invalidPlacement(placement);
        return this;
    }

    public Game printOn(GameMedia gameMedia) {
        grid.printOn(gameMedia);
        return this;
    }

    public Game start() {
        players.get(playingMark).start(this);
        return this;
    }
}

// Placement
public class Placement {

    private final Mark mark;
    private final Position position;
        
    public Placement(Mark mark, Position position) {
        this.mark = mark;
        this.position = position;
    }
    
    public Placement validate(Mark playingMark, Grid grid, Game game) {
        mark.validateAndContinue(playingMark, this, grid, game);
        return this;
    }

    public Placement validMarkContinueWith(Grid grid, Game game) {
        grid.validate(position, this, game);
        return this;
    }
    
    public Placement validPositionContinueWith(Mark playingMark, Game game) {
        mark.validate(playingMark, this, game);
        return this;
    }
    
    public Placement validPosition(Game game) {
        game.validPlacement(this);
        return this;
    }
    
    public Placement invalidPosition(Game game) {
        game.invalidPlacementByMark(this, mark);
        return this;
    }
    
    public Placement validMark(Game game) {
        game.validPlacement(this);
        return this;
    }
    
    public Placement invalidMark(Game game) {
        game.invalidPlacementByMark(this, mark);
        return this;
    }
    
    public Placement place(Grid grid, Game game) {
        grid.place(mark, position, game);
        return this;
    }
    
    public Placement printOn(PlacementMedia placementMedia) {
        placementMedia.printMarkAndPosition(mark, position);
        return this;
    }
}

// Mark
public class Mark {

    private static Mark X = new Mark("X");
    private static Mark O = new Mark("O");
    private static Mark Empty = new Mark(" ");
    
    private final String symbol;
    
    private Mark(String symbol) {
        this.symbol = symbol;
    }

    public void printOn(MarkMedia markMedia) {
        markMedia.printMark(symbol);
    }

    public static Mark x() {
        return Mark.X;
    }

    public static Mark o() {
        return Mark.O;
    }

    public static Mark empty() {
        return Mark.Empty;
    }

    public Mark opponent() {
        return this.equals(x()) ? Mark.o() : Mark.x();
    }

    public Mark validateAndContinue(Mark playingMark, Placement placement, Grid grid, Game game) {
        if(this.equals(playingMark)) {
            placement.validMarkContinueWith(grid, game);
        }
        else {
            placement.invalidMark(game);
        }
        return this;
    }

    public Mark validate(Mark playingMark, Placement placement, Game game) {
        if(this.equals(playingMark)) {
            placement.validMark(game);
        }
        else {
            placement.invalidMark(game);
        }
        return this;
    }
}

// Grid
public class Grid {

    private final HashMap<Position, Mark> gridPositions;
    private final Mark nullMark;
    
    public Grid() {
        this(new HashMap<Position, Mark>());
    }
    
    public Grid(HashMap<Position, Mark> gridPositions) {
        this.nullMark = Mark.empty();
        this.gridPositions = gridPositions;
    }

    public Grid validate(Position position, Placement placement, Game game) {
        if(isPositionAvailable(position)) {
            placement.validPosition(game);
        }
        else {
            placement.invalidPosition(game);
        }
        return this;
    }
    
    public Grid validateAndContinue(Position position, Placement placement, Mark playingMark, Game game) {
        if(isPositionAvailable(position)) {
            placement.validPositionContinueWith(playingMark, game);
        }
        else {
            placement.invalidPosition(game);
        }
        return this;
    }

    private boolean isPositionAvailable(Position position) {
        return !gridPositions.containsKey(position) && gridPositions.get(position) == null;
    }

    public Grid place(Mark mark, Position position, Game game) {
        game.gridUpdated(new Grid(newPositionsWith(mark, position)));
        return this;
    }

    private HashMap<Position, Mark> newPositionsWith(Mark mark, Position position) {
        HashMap<Position, Mark> newPositions = new HashMap<Position, Mark>(gridPositions);
        newPositions.put(position, mark);
        return newPositions;
    }

    public Grid printOn(GridMedia gridMedia) {
        gridMedia.printRow(markAt(Position.TopLeft()), markAt(Position.TopCenter()), markAt(Position.TopRight()));
        gridMedia.printRowDivider();
        gridMedia.printRow(markAt(Position.MiddleLeft()), markAt(Position.MiddleCenter()), markAt(Position.MiddleRight()));
        gridMedia.printRowDivider();
        gridMedia.printRow(markAt(Position.BottomLeft()), markAt(Position.BottomCenter()), markAt(Position.BottomRight()));
        gridMedia.printBreak();
        return this;
    }
    
    private Mark markAt(Position position) {
        Mark mark = gridPositions.get(position);
        return mark != null ? mark : nullMark;
    }
}

// ConsolePlayer
public class ConsolePlayer implements PlacementMedia, MarkMedia, PositionMedia, Player {

    private final GameMedia gameMedia;
    private String symbol;
    private String position;
    private PlannedPlacements plannedPlacements;
    
    public ConsolePlayer(PlannedPlacements plannedPlacements) {
        this.plannedPlacements = plannedPlacements;
        gameMedia = new ConsoleGameMedia();
    }
    
    @Override
    public void invalidPlacement(Placement placement) {
        placement.printOn(this);
        System.out.printf("Invalid placement: %s at %s", symbol, position);
        System.out.println();
        System.out.println();
    }

    @Override
    public void printMarkAndPosition(Mark mark, Position position) {
        mark.printOn(this);
        position.printOn(this);
    }

    @Override
    public void printPosition(String position) {
        this.position = position;
    }

    @Override
    public void printMark(String symbol) {
        this.symbol = symbol;
    }

    @Override
    public void updatedGameWithCurrentPlayer(Game newGame, Player player) {
        newGame.printOn(gameMedia);
        plannedPlacements.iAmNotAnAISoDeferNextPlayToPlan(newGame);
    }

    @Override
    public void start(Game game) {
        plannedPlacements.iAmNotAnAISoDeferNextPlayToPlan(game);
    }
}

// Main
public class Main {

    public static void main(String[] args) {
        new Main().run();
    }

    private final Game game;
    
    public Main() {
        Map<Mark, Player> players = new HashMap<Mark, Player>();
        PlannedPlacements plannedPlacements = new PlannedPlacements();
        players.put(Mark.x(), new ConsolePlayer(plannedPlacements));
        players.put(Mark.o(), new ConsolePlayer(plannedPlacements));
        this.game = new Game(players);
    }

    private void run() {
        game.start();
    }
    
    public static class PlannedPlacements {
        private final LinkedList<Placement> plannedPlacements;
        
        public PlannedPlacements() {
            plannedPlacements = new LinkedList<Placement>();
            plannedPlacements.add(new Placement(Mark.x(), Position.TopLeft()));
            plannedPlacements.add(new Placement(Mark.o(), Position.MiddleCenter()));
            plannedPlacements.add(new Placement(Mark.o(), Position.TopRight())); // Try out of turn player, valid position
            plannedPlacements.add(new Placement(Mark.x(), Position.TopLeft())); // Try correct player, invalid position
        }
        
        public void iAmNotAnAISoDeferNextPlayToPlan(Game game) {
            if(plannedPlacements.isEmpty())
                System.exit(0);
            
            game.submit(plannedPlacements.pop());
        }
    }
}

// Output
 X |   |   
---+---+---
   |   |   
---+---+---
   |   |   

 X |   |   
---+---+---
   | O |   
---+---+---
   |   |   

Invalid placement: O at TR

 X |   |   
---+---+---
   | O |   
---+---+---
   |   |   

Invalid placement: X at TL

 X |   |   
---+---+---
   |   |   
---+---+---
   |   |