首页
学习
活动
专区
圈层
工具
发布
社区首页 >问答首页 >Tic Tac脚趾-第2阶段:控制台PvP + PvE + "EvE“

Tic Tac脚趾-第2阶段:控制台PvP + PvE + "EvE“
EN

Code Review用户
提问于 2016-03-04 16:37:08
回答 1查看 297关注 0票数 5

不久前,我打开了这个话题:Tac脚趾级1:控制台PvP

由于考试,我不得不搁置那个小项目,但几天前我又开始着手做了。现在可以与AI对抗,也可以让两个AI互相对抗。选择可以在菜单中进行。

当然,我同时也学习过,我相信我对面向对象设计的理解已经改进了xD(大喊答题)头第一设计模式。这对我有很大帮助,尽管我只学到了几种模式。我很期待能学到更多的东西!。话虽如此,我清楚地意识到,我仍然缺乏很多。

在我要求进行代码评审之前:我必须在github上打开一个新的存储库,因为我想继续在Eclipse上工作,但是我无法导入旧的存储库。

我现在有19门课,所以我想这篇文章会很长。如果您想查看github上的类,下面是链接:https://github.com/kebiro/TicTacToe/tree/GameMenu/TicTacToe/src/game

总的来说,我相信我成功地解决了我设计中的一些部分,而另一些地方却不是很好。我特别不确定我的输入/输出类和我的GameMenu是否设计正确。Board还需要适当的重构。我一直在尽我所能记住未来的想法,但我不确定我是否成功地保持了我的设计的适当可扩展性。

总之,下面是代码(一些类被排除在外,比如Main.java):

董事会,移动,定位

代码语言:javascript
复制
package game;

public class Move {
    private String move;

    public Move(String move) {
        if(move.length() != 1 || !"123456789".contains(move))
            throw new IllegalMoveException("move needs to be a digit (1-9)");

        this.move = move;
    }

    public int getMove() {
        return Integer.parseInt(move);
    }

    @Override
    public String toString() {
        return move;
    }
}
代码语言:javascript
复制
package game;

public class Position {
    private int row, col;

    public Position(Move move) {
        this.row = (move.getMove() - 1) / 3;
        this.col = (move.getMove() - 1) % 3;
    }

    public int getRow() {
        return row;
    }

    public int getCol() {
        return col;
    }
}
代码语言:javascript
复制
package game;

public class Board {
    private String[][] board;

    public Board() {
        init();
    }

    public String[][] getBoard() {
        return board;
    }

    private void init() {
        board = new String[3][3];
        int count = 1;

        for(int i=0; i < board.length; ++i) {
            for(int j=0; j < board[i].length; ++j) {
                // set the field positions, from 1 to 9
                board[i][j] = "" + count++;
            }
        }
    }

    public boolean isFull() {
        boolean isFull = true;

        for(int i=0; i < board.length; ++i) 
            for(int j=0; j < board.length; ++j)
                // still contains an unmarked field (indicated by a number)
                if("123456789".contains(board[i][j]))
                    isFull = false;

        return isFull;
    }

    // ???
    public void markField(Move move, Sign sign) {
        if(!fieldIsMarked(move)) 
            setValueAtPosition(move, sign + "");
    }

    // ???
    public boolean fieldIsMarked(Move move) {
        return !getValueAtPosition(move).equals(move.toString());
    }

    // ???
    private String getValueAtPosition(Move move) {
        Position pos = new Position(move);
        return board[pos.getRow()][pos.getCol()];
    }

    // ???
    private void setValueAtPosition(Move move, String val) {
        Position pos = new Position(move);
        board[pos.getRow()][pos.getCol()] = val;
    }

    public boolean hasThreeInARow() {
        boolean horizontal = 
                board[0][0].equals(board[0][1]) && board[0][1].equals(board[0][2])
                || ( board[1][0].equals(board[1][1]) && board[1][1].equals(board[1][2]) )
                || ( board[2][0].equals(board[2][1]) && board[2][1].equals(board[2][2]) );

        boolean vertical =
                board[0][0].equals(board[1][0]) && board[1][0].equals(board[2][0])
                || board[0][1].equals(board[1][1]) && board[1][1].equals(board[2][1])
                || board[0][2].equals(board[1][2]) && board[1][2].equals(board[2][2]);

        boolean diagonal = 
                board[0][0].equals(board[1][1]) && board[1][1].equals(board[2][2])
                || board[0][2].equals(board[1][1]) && board[1][1].equals(board[2][0]);

        return horizontal || vertical || diagonal;
    }

    @Override
    public String toString() {
        StringBuilder sb = new StringBuilder();
        int formatSize = 0;

        for(int i=0; i < board.length; ++i) {
            for(int j=0; j < board[i].length; ++j) {
                sb.append(board[i][j] + " | ");
            }

            // #_|_#_|_#_|_ - delete the last two blanks + |
            sb.delete(sb.length()-3, sb.length());
            // determines how many "-" to draw horizontally to make it look more aligned 
            formatSize = (formatSize == 0) ? sb.length() : formatSize;

            if(i < board.length-1) {
                sb.append(System.lineSeparator());
                // http://stackoverflow.com/questions/1235179/simple-way-to-repeat-a-string-in-java
                sb.append(new String(new char[formatSize]).replace("\0", "-"));
                sb.append(System.lineSeparator());
            }
        } 

        return sb.toString();
    }
}

我不确定我是否正确地使用了Position。我用// ???标记这些地方。我还注意到,在编写这篇文章时,markField(Move, Sign)可能可以从Board中提取出来。

hasThreeInARow()已经从Game移到了Board。除此之外,Board和以前几乎是一样的。

对策

代码语言:javascript
复制
package game;

import java.util.*;
import game.io.*;

public class Game {
    private Board board;
    private Player player1, player2;
    private Player activePlayer;
    private boolean hasThreeInARow;
    private ManagerFactory managerFactory;
    private Output outputManager;

    public Game(ManagerFactory managerFactory) {
        this.managerFactory = managerFactory;
        this.outputManager = managerFactory.createOutputManager();

        GameMenu menuManager = managerFactory.createMenuManager();
        menuManager.menu();
        board = menuManager.createBoard();
        player1 = menuManager.createPlayer1();
        player2 = menuManager.createPlayer2();
    }

    /**
     * play a Tic-Tac-Toe game 
     */
    public void playGame() {
        activePlayer = playerToStart();

        while(!board.isFull()) {
            outputManager.callForTurn(activePlayer);
            outputManager.showBoard(board);
            Move move = activePlayer.makeMove();
            board.markField(move, activePlayer.getSign());
            hasThreeInARow = board.hasThreeInARow();
            if(hasThreeInARow)
                break;
            changeTurns();
        }

        System.out.println();
        outputManager.showBoard(board);

        if(hasThreeInARow)
            outputManager.declareWinner(activePlayer);
        else
            outputManager.println("draw");

        outputManager.println("===============================================");
        new Game(managerFactory).playGame();
    }

    // what follows are auxiliary methods for playGame(); to make playGame() more readable

    private Player playerToStart() {
        return (new Random().nextInt(2) == 0) ? player1 : player2;
    }

    private void changeTurns() {
        activePlayer = (activePlayer == player1) ? player2 : player1;
    }
}

我从Game中提取了很多方法。我相信现在它已经变得相当有凝聚力了。我不确定的是我构建Game的方式是否正确。(但问题来源可能是GameMenu,如果有问题.)

此外,我也不确定playGame()循环是否会像现在这样对GUI工作。(循环后的Sysout很麻烦,我在每次迭代中都调用showBoard(Board),这是控制台所必需的)

参与者与策略

代码语言:javascript
复制
package game;

public class Player {
    private String name;
    private Sign sign;
    private MoveStrategy moveStrategy;


    public Player(String name, Sign sign, MoveStrategy moveStrategy) {
        this.name = name;
        this.sign = sign;
        this.moveStrategy = moveStrategy;
    }

    public Move makeMove() {
        return moveStrategy.makeMove();
    }

    public Sign getSign() {
        return sign;
    }

    @Override
    public String toString() {
        return name;
    }
}
代码语言:javascript
复制
package game;

public interface MoveStrategy {
    public Move makeMove();
}
代码语言:javascript
复制
package game;

import game.io.*;

public class HumanMoveStrategy implements MoveStrategy {
    private Board board;
    private Output outputManager;
    private Input inputManager;

    public HumanMoveStrategy(Board board, ManagerFactory managerFactory) {
        this.board = board;
        this.outputManager = managerFactory.createOutputManager();
        this.inputManager = managerFactory.createInputManager();
    }


    @Override
    public Move makeMove() {
        outputManager.print("Your move: ");

        Move move = null;
        do {
            try {
                move = new Move(inputManager.read());
                while(board.fieldIsMarked(move)) {
                    outputManager.print("That move has alreary been made, try again: ");
                    move = new Move(inputManager.read());
                }
            } catch(IllegalMoveException ime) {
                outputManager.print(ime.getMessage() + ", try again: ");
                move = null;
            }
        } while(move == null);

        return move;
    }
}
代码语言:javascript
复制
package game;

import game.io.*;
import java.util.Random;

public class AIrandomMoveStrategy implements MoveStrategy {
    private Board board;
    private Output outputManager;

    public AIrandomMoveStrategy(Board board, ManagerFactory managerFactory) {
        this.board = board;
        this.outputManager = managerFactory.createOutputManager();
    }

    @Override
    public Move makeMove() {
        Random rand = new Random();
        int movePos = Math.floorMod(rand.nextInt(), 9) + 1;

        Move move = new Move(movePos + "");
        while(board.fieldIsMarked(move)) {
            movePos = Math.floorMod(rand.nextInt(), 9) + 1;
            move = new Move(movePos + "");
        }

        outputManager.println("Move made: " + movePos);

        return move;
    }
}

我已经将makeMove()方法从Game中移出并放入Player中。我在考虑如何实现人工智能(计划在下一阶段进行)。我认为人类玩家和人工智能之间唯一的区别就是移动的方式。

这里我采用了战略模式,我认为这是正确的决定。因此,Player委托给MoveStrategy。最初,我计划在第二阶段开始之前等待,直到第一阶段完全完成。虽然这项战略使得人工智能的实现相当简单,所以我做到了。(不过,我会把Minimax推迟一段时间。)

Input/Output

代码语言:javascript
复制
package game.io;

public interface Input {
    public String read();
    public String readLine();
}
代码语言:javascript
复制
package game.io;

import java.util.*;

public class InputConsole implements Input {

    private Scanner scanner = new Scanner(System.in);

    @Override
    public String read() {
        return scanner.next();
    }

    @SuppressWarnings("resource")
    @Override
    public String readLine() {
        Scanner scanner = new Scanner(System.in);
        return scanner.nextLine();
    }

}
代码语言:javascript
复制
package game.io;

import game.Board;
import game.Player;

public interface Output {
    public void showBoard(Board board);
    public void callForTurn(Player activePlayer);
    public void declareWinner(Player winner);
    public void print(String s);
    public void println(String s);
}
代码语言:javascript
复制
package game.io;

import game.Board;
import game.Player;

public class OutputConsole implements Output {

    @Override
    public void showBoard(Board board) {
        System.out.println("=========");
        System.out.println(board);
        System.out.println("=========");
    }

    @Override
    public void callForTurn(Player activePlayer) {
        System.out.printf("%n%s's turn (%s)%n", activePlayer, activePlayer.getSign());
    }

    @Override
    public void declareWinner(Player winner) {
        System.out.printf("%s (%s) won%n", winner, winner.getSign());
    }

    @Override
    public void print(String s) {
        System.out.print(s);
    }

    @Override
    public void println(String s) {
        System.out.println(s);
    }

}

如前所述,我在这些课程上遇到了麻烦。这是我在上一个问题中提出的,我不知道我成功地实现了这个想法。然而,我注意到,这是战略模式。至少它和它非常相似。为了使输入/输出与GUI一起工作,我只需提供与GUI一起工作的具体实现。

游戏菜单

代码语言:javascript
复制
package game;

import game.io.*;

public abstract class GameMenu {
    protected Player player1, player2;
    protected ManagerFactory managerFactory;
    protected Output outputManager;
    protected Input inputManager;
    protected Board board = new Board();

    public abstract void menu();

    public abstract Player createPlayer1();

    public abstract Player createPlayer2();

    public Board createBoard() {
        return board;
    }
}
代码语言:javascript
复制
package game;

public class ConsoleGameMenu extends GameMenu {

    public ConsoleGameMenu() {
        this.managerFactory = new ConsoleManagerFactory();
        this.outputManager = managerFactory.createOutputManager();
        this.inputManager = managerFactory.createInputManager();
    }

    @Override
    public void menu() {
        outputManager.println("Welcome to Tic Tac Toe");
        outputManager.println("Please choose the gmae you would like to play: ");
        outputManager.println("\t(1) Singleplayer");
        outputManager.println("\t(2) Multiplayer");
        outputManager.println("\t(3) watch mode (Computer vs. Computer)");
        outputManager.println("-----------------------------------------------");
        outputManager.println("For help, press 'h'");
        outputManager.println("To exit, press 'x'");
        outputManager.println("===============================================");

        String input = inputManager.read();
        while(!input.matches("[1, 2, 3, h, x]")) {
            outputManager.println("Invalid input, please try again");
            input = inputManager.read();
        }


        switch(input) {
        case "1": initSingleplayer(); break;
        case "2": initMultiplayer(); break;
        case "3": initWatchMode(); break;
        case "h": showHelp(); break;
        case "x": exit(); break;
        default: // TODO

        outputManager.println("===============================================");
        outputManager.println("==================Tic Tac Toe==================");
        outputManager.println("===============================================");
        }
    }


    private void initSingleplayer() {
        outputManager.println("At what difficulty would you like to play?");
        outputManager.println("\t(1) easy");
        outputManager.println("\t(2) hard - not implemented yet");
        outputManager.println("===============================================");

        String input = inputManager.read();
        while(!input.matches("[1, 2]")) {
            outputManager.println("Invalid input, please try again");
            input = inputManager.read();
        }

        outputManager.print("Player 1 enter your name: ");
        player1 = new Player(inputManager.readLine(), Sign.X, new HumanMoveStrategy(board, managerFactory));

        switch(input) {
        case "1": player2 = new Player("Random AI", Sign.O, new AIrandomMoveStrategy(board, managerFactory)); break;
        case "2": break;
        default: // TODO
        }
    }


    private void initMultiplayer() {
        outputManager.print("Player 1 enter your name: ");
        player1 = new Player(inputManager.readLine(), Sign.X, new HumanMoveStrategy(board, managerFactory));
        outputManager.print("Player 2 enter your name: ");
        player2 = new Player(inputManager.readLine(), Sign.O, new HumanMoveStrategy(board, managerFactory));
    }

    private void initWatchMode() {
        player1 = new Player("AI#1", Sign.X, new AIrandomMoveStrategy(board, managerFactory));
        player2 = new Player("AI#2", Sign.O, new AIrandomMoveStrategy(board, managerFactory));
    }

    private void showHelp() {
        outputManager.println("This is the help menu");
        outputManager.println("I hope this was helpful");
        outputManager.print("To return back to the main menu, press 'r': ");

        String input = inputManager.read();
        while(!input.matches("[r, R]")) {
            input = inputManager.read();
        }

        outputManager.println("===============================================");
        menu();
    }

    private void exit() {
        outputManager.print("Are you sure that you want to end the game (y/n)? ");

        String input = inputManager.read();
        while(!input.matches("[y, Y, n, N]")) {
            outputManager.println("Invalid input. Please answer with yes (y/Y) or no (n/N)");
            input = inputManager.read();
        }

        if(input.equalsIgnoreCase("y")) {
            outputManager.println("Good bye!");
            System.exit(0);
        }
        else {
            outputManager.println("===============================================");
            menu();
        }
    }

    // TODO: PlayerNotInitializedException
    @Override
    public Player createPlayer1() {
        return player1;
    }

    @Override
    public Player createPlayer2() {
        return player2;
    }

}

这也给我带来了一些麻烦。最后,GameMenu变成了一个会说话的抽象工厂,因此它有两种责任(1:它扮演游戏菜单的角色,2:它返回两个PlayerBoard的实例)。但这可能是合理的,因为这些类的实例化取决于菜单调用。ConsoleGameMenu还没有完全完成,但这是基本结构。

经理与抽象工厂

代码语言:javascript
复制
package game;

import game.io.*;

public interface ManagerFactory {
    public Output createOutputManager();
    public Input createInputManager();
    public GameMenu createMenuManager();
}
代码语言:javascript
复制
package game;

import game.io.*;

public class ConsoleManagerFactory implements ManagerFactory {

    @Override
    public Output createOutputManager() {
        return new OutputConsole();
    }

    @Override
    public Input createInputManager() {
        return new InputConsole();
    }

    @Override
    public GameMenu createMenuManager() {
        return new ConsoleGameMenu();
    }
}

最后,我使用抽象工厂模式来强制执行Game总是用相应的类构造的,也就是说,强制执行类似new Game(new InputConsole(), new OutputGUI(), new GameMenuFile())这样的事情不会发生。(我确信,根据上下文的不同,在控制台上输入和在GUI上输出并不一定是错误的,但我不想这样做。)

在我讲完之前,我还有两个问题:

  • 是否有适当评论的地方?应该用什么方法将Javadoc附加到它们上?
  • 到目前为止我还没有写过任何测试。哪些零件一定要测试?哪里不需要测试?

我对此表示感谢,对此我感到非常抱歉。=/

EN

回答 1

Code Review用户

回答已采纳

发布于 2016-07-15 14:22:57

这将是一些挑剔/快速解决的小事情。

  • package game;是..。非常规的名字。包通常是以拥有的域的反向顺序命名的。因此,我(实际上不拥有该域)使用de.vogel612作为我为自己编写的所有包的前缀。然后我使用应用程序的名称,然后包层次结构开始。这个约定是为了防止包裹的名称冲突。
  • Move.getMove()重复工作。将一个字符串变成一个整数比将一个整数变成一个字符串更困难。您应该考虑使用以下代码。公共类移动{私有最终int迁移;公共移动(字符串移动){ this.move =Integer.parseInt(移动);} // .}这还包括使私有字段成为最终结果。您希望它永远不会更改,并在构造函数中初始化它。使其成为最终结果,编译器将验证该字段永不更改的断言。注意到,在这种情况下,这可能不是绝对必要的,但这是一个很好的习惯。
  • 关于最终字段的建议也适用于Position。有趣的是Board
  • 我们在Board的时候。我个人不喜欢init()方法,因为它们不算初始化块。这些块有一些次要的特殊规则,特别是在处理最终字段时:D. 我强烈建议您内联init方法,因为它基本上是.适得其反。
  • “我不确定我是否正确地使用了Position。我用//??标记了这些地方。”是啊。你实际上应该通过那里的位置。那些方法太多了,不知道它们的名字。
  • Player还将受益于final实例字段。

我刚刚快速浏览了代码的其余部分,但在我看来,它看起来很好,而且分离得很好。里面可能有一些隐藏的细节,但不幸的是,我没有时间进行更深入的审查。

票数 2
EN
页面原文内容由Code Review提供。腾讯云小微IT领域专用引擎提供翻译支持
原文链接:

https://codereview.stackexchange.com/questions/121906

复制
相关文章

相似问题

领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档