Uma implementação simples de Snakes and Ladders em Java

Sep 22 2020

Eu criei uma implementação de snakes and ladders bastante direta em Java apenas para começar. O programa pode ser resumido como-

  1. Objeto de placa contém uma matriz de Tipo Ladrilho para representar a placa real.

  2. O objeto do tipo Tile contém um membro de dados Enum para identificar se é um ladrilho regular ou uma cobra / escada.

    Também contém um bloco de destino para identificar o destino da cobra / escada.

  3. O objeto Player contém o nome do jogador e a posição do jogador.

  4. Na inicialização, por meio de randoms, eu gero blocos aleatórios para serem cobras e escadas. Em seguida, o usuário é solicitado a inserir não. de jogadores e, em seguida, continue lançando os dados até que um jogador alcance a 100ª peça.

O código:

Classe BoardImpl:

import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

public class BoardImpl implements Board{

    Tile[] board = new Tile[101];         //A array to represent the 'board'
    List<Player> players = new ArrayList<>();
    SecureRandom secureRandom = new SecureRandom();
    Set<Integer> occupiedPositions = new HashSet<>();

    public void setPlayers(List<Player> players) {
        this.players = players;
    }

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

    public List<Player> getPlayers() {
        return players;
    }

    public void addPlayers(Player player) {
        this.players.add(player);
    }

    @Override
    public void initialize() {
//this function generates the board, placing random snakes and ladders and
//filling the rest with normal tiles
        System.out.println("Initializing board");

        int snakes = secureRandom.nextInt(15) + 1; //generates snakes & 
        int ladders = secureRandom.nextInt(15) + 1; //ladders at random
                                                    //positions
        setSnakes(snakes);
        setLadders(ladders);

        for(int i = 0; i < 101; i++){
            if(board[i] != null){
                continue;
            }
            Tile t  = new TileImpl(TileType.NORMAL);
            board[i] = t;
        }


    }

    @Override
    public void movePlayer(Player player, int places) {
        int currentPosition = player.getPosition();
        String checkPos = checkEndingPosition(currentPosition, places);
        if(checkPos.equals("winner")){
                winner(player);
        }
        if(checkPos.equals("outOfBoard")){
            System.out.println("That move cannot be made from your position! " +
                    "Roll a different number next time");
        }
        if(checkPos.equals("valid")){
                Tile t = getTile(currentPosition + places);
                if(t.getType().equals(TileType.NORMAL)){
                    player.setPosition(currentPosition + places);
                    System.out.println("Player " + player.getName() +
                            " moves to position "+ player.getPosition());
                }else {
                    player.setPosition(t.getDestination());
                    System.out.println(" Player " + player.getName() +
                            " encountered a "+ t.getType()+ "!!, moves to position "+ player.getPosition());
                }
        }
    }

    @Override
    public Tile getTile(int n) {
        return board[n];
    }

    public String checkEndingPosition(int currentPosition, int places){

        if((currentPosition + places) > 100){
            return "outOfBoard";
        }else if((currentPosition+ places) == 100){
            return "winner";
        }
        else{
            return "valid";
        }
    }

    public void winner(Player player){

        System.out.print("Player " + player.getName() + " has won the game !");
        System.exit(0);
    }

    public void setSnakes(int n){
//this function generates snakes at random positions
        for(int i = 0; i < n; i ++){
            boolean flag = true;
            int start = 0;
            int dest = 0;

            while(flag) {
                 start = secureRandom.nextInt(98);
                if(!occupiedPositions.contains(start)){
                    occupiedPositions.add(start);
                    break;
                }
            }

            while(flag) {
 //Setting the destination for a snake tile to lower than its position
                 dest = (int)(start * secureRandom.nextDouble());
                if(!occupiedPositions.contains(dest)){
                    occupiedPositions.add(dest);
                    break;
                }
            }

            Tile tile = new TileImpl(TileType.SNAKE, dest);
            board[start] = tile;

            System.out.println("Created snake " + "[" + start+ "," + dest + "]");
        }

    }

    public void setLadders(int n) {
//this function generates ladders randomly
        for (int i = 0; i < n; i++) {
            boolean flag = true;
            int start = 0;
            int dest = 0;

            while (flag) {
                start = secureRandom.nextInt(80);
                if (!occupiedPositions.contains(start)) {
                    occupiedPositions.add(start);
                    break;
                }
            }

            while (flag) {
//this step places the destination for the ladder tile to greater than its position
                dest = (int) (start + secureRandom.nextDouble() * 10);
                if (!occupiedPositions.contains(dest)) {
                    occupiedPositions.add(dest);
                    break;
                }
            }

            Tile tile = new TileImpl(TileType.LADDER, dest);
            board[start] = tile;
            System.out.println("Created ladder " + "[" + start+ "," + dest + "]");
        }
    }
}

Classe PlayerImpl:

public class PlayerImpl implements Player{


    private int position = 0;
    private String name = null;

    public PlayerImpl(String name){
        this.name = name;
    }

    @Override
    public int getPosition() {
        return position;
    }

    @Override
    public String getName() {
        return name;
    }

    @Override
    public void setPosition(int currentPosition) {
        this.position = currentPosition;
    }

    @Override
    public void setName(String playerName) {
        this.name = playerName;
    }
}

Classe TileImpl:

public class TileImpl implements Tile {

    private TileType type;
    private int position;
    private int destination;

    public TileImpl(TileType type) {
        this.type = type;
    }

    public TileImpl(TileType type, int dest) {
        this.type = type;
        this.destination = dest;
    }

    public int getPosition() {
        return position;
    }

    public int getDestination() {
        return destination;
    }

    public void setType(TileType type) {
        this.type = type;
    }

    public void setPosition(int position) {
        this.position = position;
    }

    public void setDestination(int destination) {
        this.destination = destination;
    }

    @Override
    public TileType getType() {
        return type;
    }

}

Classe SnakesAndLadders:

import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.List;
import java.util.Scanner;

public class SnakesAndLadders {

    public static void main(String args[]){

        Board board = new BoardImpl();
        List<Player> players = new ArrayList<>();
        System.out.println("How many players ?");

        Scanner scan = new Scanner(System.in);
        int noOfPlayers = scan.nextInt();

        for(int i = 0; i < noOfPlayers; i++){
            System.out.println("Enter name for player "+ i);
            String name = scan.next();
            Player player = new PlayerImpl(name);
            players.add(player);

        }

        board.initialize();
        int counter = 0;
        System.out.println("Lets Play!");
        String choice = "";
        SecureRandom random = new SecureRandom();
//simulating a game through the do while loop
        do{
            if(counter >= noOfPlayers) counter = 0;
            Player currPlayer = players.get(counter);
            System.out.println(" Player " + currPlayer.getName() + " turn to play!");
            System.out.println(" R = Roll the dice, Q = quit");
            choice = scan.next();
            if(choice.equalsIgnoreCase("R")){
                int places = random.nextInt(6) + 1;
                board.movePlayer(currPlayer, places);
                counter++;

            }
        }while(!choice.equalsIgnoreCase("Q"));
    }
}

Acho que o jogo é muito direto. Eu queria saber particularmente se

  1. Existe uma maneira melhor de gerar os números aleatórios de cobras e escadas.

  2. Uma estrutura de dados melhor e mais apropriada para representar o 'tabuleiro'

  3. Se meu código pode ser melhorado para conter quaisquer paradigmas / práticas importantes.

Respostas

2 Marc Sep 23 2020 at 09:48

Bem-vindo à revisão do código. Boa implementação e boa estrutura do código, encontre minhas sugestões na linha.

  1. Existe uma maneira melhor de gerar os números aleatórios de cobras e escadas.

Os métodos setSnakese setLaddersdiferem por apenas algumas linhas de código, o que é um indicador de duplicação de código. Este é o método setLadders:

public void setLadders(int n) {
//this function generates ladders randomly
    for (int i = 0; i < n; i++) {
        boolean flag = true;
        int start = 0;
        int dest = 0;

        while (flag) {
            start = secureRandom.nextInt(80);
            if (!occupiedPositions.contains(start)) {
                occupiedPositions.add(start);
                break;
            }
        }

        while (flag) {
//this step places the destination for the ladder tile to greater than its position
            dest = (int) (start + secureRandom.nextDouble() * 10);
            if (!occupiedPositions.contains(dest)) {
                occupiedPositions.add(dest);
                break;
            }
        }

        Tile tile = new TileImpl(TileType.LADDER, dest);
        board[start] = tile;
        System.out.println("Created ladder " + "[" + start+ "," + dest + "]");
    }
}

Uma abordagem é criar um método para gerar os números aleatórios e usá-lo para gerar escadas e cobras. Por exemplo:

private void setRandomLadder() {
    while (true) {
        int[] segment = getRandomSegment(1,99);
        int start = segment[0];
        int end = segment[1];
        if (isTileFree(start) && isTileFree(end)) {
            board[start] = new TileImpl(TileType.LADDER, end);
            board[end] = new TileImpl(TileType.OCCUPIED);
            break;
        }
    }
}

Este método pode ser invocado nvárias vezes, dependendo de quantas escadas você deseja adicionar ao tabuleiro. O método getRandomSegmentretorna dois pontos aleatórios entre 1 e 99. Além disso, alterei o nome de setLadderspara setRandomLadder, para torná-lo autodescritivo. Finalmente, tenha em mente que se você adicionar muitas escadas, esses métodos nunca serão concluídos, então considere verificar se há blocos livres antes.


  1. Uma estrutura de dados melhor e mais apropriada para representar o 'tabuleiro'

Usando o método anterior, podemos remover a lista occupiedPositions, o que foi possível adicionando TileType.OCCUPIED.

O array de Tileestá ok como estrutura de dados, uma alternativa é usar ArrayList.


  1. Se meu código pode ser melhorado para conter quaisquer paradigmas / práticas importantes.
  • Saída do console :

    How many players ?
    2
    Enter name for player 0
    

    É confuso para o usuário ver o jogador 0, começar a partir do jogador 1.

  • Números mágicos : new Tile[101], secureRandom.nextInt(98), etc. Criar constantes, por exemplo:

    private static final int SIZE = 101;
    
  • Modificadores : os métodos setSnakese setLaddersdevem ser privados, mesmo para todas as variáveis ​​de instância.

  • Inicialize no construtor : o BoardImpltem um método initialize()mas e um construtor vazio. Você pode usar diretamente o construtor.

  • Encapsulamento : a interface Boardaceita um Playercom um público setPosition, o que significa que qualquer pessoa pode alterá-lo. Apenas o tabuleiro deve mudar o estado do jogador. Uma interface mais segura pode ser:

    public interface Board {
      void movePlayer(String playerId, int places);
      //..
    }
    
  • Evite variáveis ​​temporárias : em vez de

    Tile tile = new TileImpl(TileType.LADDER, dest);
    board[start] = tile;
    

    Você pode escrever:

    board[start] = new TileImpl(TileType.LADDER, dest);
    
  • Cheiro de código :

        public String checkEndingPosition(int currentPosition, int places){
    
          if((currentPosition + places) > 100){
              return "outOfBoard";
          }else if((currentPosition+ places) == 100){
              return "winner";
          }
          else{
              return "valid";
          }
      }
    

    Retornar uma String é muito sujeito a erros aqui. Por exemplo, você pode retornar um booleano e, eventualmente, uma exceção.

  • Teste e reutilização : as winnerchamadas de método System.exit(0), o que torna impossível o teste. Além disso, há muitos System.out.printlnna BoardImplclasse. É muito difícil testar uma classe como essa. O ideal é que a classe Boardnunca seja impressa para o usuário, apenas para depuração. O maindeve ser responsável por interagir com o usuário.

3 user985366 Sep 22 2020 at 15:53

Legibilidade e estilo

Execute seu código por meio de um formatador para torná-lo mais legível e seguir os padrões.

Este pode funcionar:

https://www.tutorialspoint.com/online_java_formatter.htm

Bandeira?

            boolean flag = true;
            int start = 0;
            int dest = 0;

            while (flag) {
                start = secureRandom.nextInt(80);
                if (!occupiedPositions.contains(start)) {
                    occupiedPositions.add(start);
                    break;
                }
            }

Você não está usando, verificando ou alterando o sinalizador aqui. Este loop deve ser feito while(true)e o próximo loop faz a mesma coisa.

If-else-

if(checkPos.equals("winner")){
                winner(player);
        }

Quando você está verificando vários casos exclusivos e comparando com uma string, acho que uma switchinstrução é a maneira mais apropriada / padrão de fazer isso. Também deixa mais claro que os casos são exclusivos.

Estrutura de dados

Set<Integer> occupiedPositions = new HashSet<>();

Isso parece desnecessário / antinatural para mim. Eu acrescentaria à classe Ladrilho a boolpara marcar um ladrilho como ocupado, e então essa informação pode estar contida no próprio ladrilho / quadro em vez de ter um separado Setpara contabilidade.