C, PHP, VB, .NET

Дневникът на Филип Петров


* Игра на камъчета с трима играчи – сървър и клиент

Публикувано на 26 януари 2014 в раздел ПИК3 Java.

Трябва да се моделира комбинаторната игра камъчета (stones). Играта се играе от трима играчи и се състои в следното:

  • Първият играч взима шепа камъчета и ги изсипва на земята;
  • Вторият играч определя число от 3 до 10 – нека това число е N;
  • Играчите започват да се редуват да взимат камъчета:
    • Третият играч започва играта, като има право да вземе от 1 до N камъчета;
    • Първият играч продължава, като също има право да вземе от 1 до N камъчета;
    • Вторият играч също взима от 1 до N камъчета;
    • Предишните три точки се повтарят до изчерпване на всички камъчета;
    • Играта печели този играч, който вземе последното камъче.

Съставете сървърно приложение, с което да може играчи да играят играта в мрежа:

  • Сървърът очаква връзка от клиент на порт 1234;
  • Свързва се нов клиент и му се подава съобщение да изчака опонентите си;
  • Свързва се втори клиент и също по подобие на първия получава съобщение на изчака
  • Свързва се трети клиент и за тримата се стартира отделна нишка StonesGame;
  • В основната нишка (main) сървърът очаква нови клиенти за нова игра.

Нишката StonesGame да има един конструктор с три параметъра – сокети за получаване и изпращане на информация към/от клиентските приложения. Останалите неща от структурата на класа можете да ги определите вие. Важно условие е сървъра да контролира играчите така, че те да дават валидни ходове:

  • Когато първият играч определя броя камъчета, той трябва да подаде цяло число в интервала [20, 100];
  • Вторият трябва да даде валидна стойност на N – число в интервала [3, 10];
  • След това при всяко взимане на камъчета трябва да се гарантира, че се взимат между 1 и N, но не повече или по-малко.

Ако един от тримата играчи подаде невалиден ход той трябва да бъде помолен да повтори хода си. Ако обаче той изпрати последователно три невалидни хода един след друг, той бива дисквалифициран от играта и другите двама играчи трябва да продължат да играят без него. Ако от играта бъдат дисквалифицирани двама играчи, тогава третият печели служебно.

Реализирайте възможно най-елементарно клиентско приложение, което да работи със създадения от вас сървър.

Решение: Представеното решение е значително усложнено спрямо това, което което покрива критериите за 6. По вече утвърдена традиция всичко е over-commented

Сървър:

package StonesServer;
import java.io.IOException;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.net.Socket;
import java.net.ServerSocket;
import java.util.LinkedList;

public class StonesServer{
  // Порт за връзка със сървъра
  static int port = 1234;
  public static void main(String [] args){

    // Стартираме сървъра
    ServerSocket servSock;
    try{
      servSock = new ServerSocket(port);
    }
    catch(IOException e){
      System.err.println("Can't start server");
      return;
    }
    System.out.println("Server started");

    // За удобство сме добавили id на различните игри
    int lastGameID = 1;

    // Ще записваме новодошли играчи
    LinkedList<User> usersQueue = new LinkedList<User>();
    // Предотвратява проблеми при изтриване на свързани,
    // но в последствие напуснали играчи
    synchronized(usersQueue){
      // Безкраен цикъл за приемане на нови играчи
      mainloop: while(true){
        // Тук ще запишем новодошлия
        User u = new User();
        try{
          // Приемаме нов клиент
          u.initSocket(servSock.accept());
          System.out.println("New user is connected");
          // Добавяме го в списъка
          usersQueue.add(u);
          // Казваме му да изчака
          u.send("SRV: Please wait for opponents");
        }
        catch(IOException e){
          // Ако има проблем - при връзката или при
          // изпращане на съобщението, - премахваме клиента
          System.out.println("Connection to user failed");
          usersQueue.remove(usersQueue.indexOf(u));
          continue mainloop;
        }

        // Ако вече има натрупани трима играчи
        if(usersQueue.size() == 3){
          // Изпращаме по едно съобщение на всеки.
          // Така се уверяваме, че всички са налични
          for(User tu: usersQueue){
            try{
              tu.send("SRV: Please wait a bit more...");
            }
            catch(IOException userIsGone){
              // Премахваме напуснал играч
              usersQueue.remove(usersQueue.indexOf(tu));
              // Тук можем да уведомим останалите, че
              // не успяхме да пуснем тяхната игра и да ги
              // помолим да изчакат още малко за нов играч
            }
          }

          // Ако са намаляли играчите, ще търсим нов
          if(usersQueue.size() != 3) continue mainloop;
          // Ако са налични, време е да пуснем игра!
          else{
            // Правим копие на списъка с играчите
            LinkedList<User> UQCopy = new LinkedList<User>();
            for(User i: usersQueue){
              UQCopy.add(i);
            }
            // Основната опашка я изпразваме
            usersQueue = new LinkedList<User>();
            // Стартираме играта с копието
            Thread t = new Thread(
                                  new StonesGame(lastGameID, UQCopy.get(0), 
                                                 UQCopy.get(1), UQCopy.get(2)));
            t.start();
            System.out.println("Game id "+lastGameID+" started");
            lastGameID++;
          }
        }
      }
    }
  }
}  

// Чрез този клас ще комуникираме с отделни играчи
class User{
  Socket s;
  DataInputStream in;
  DataOutputStream out;

  // Всъщност не ни трябва конструктор
  public User(){}

  // Тук инициализираме връзката
  void initSocket(Socket s) throws IOException{
    this.s = s;
    in = new DataInputStream(s.getInputStream());
    out = new DataOutputStream(s.getOutputStream());
  }

  // за изпращане на съобщение до играч
  void send(String msg)  throws IOException{
    this.out.writeUTF(msg);
  }

  // за получаване на отговор от играч
  int get() throws IOException{
    return this.in.readInt();
  }
}

// Клас за купчината камъни
class StonesPile{
  int stonesAmount; // Колко камъка са
  int takeLimit;    // N

  // Стойности по подразбиране
  public StonesPile(){
    this.stonesAmount = 30;
    this.takeLimit = 10;
  }

  // Set метод за stonesAmount
  public void initStonesPile(int stonesAmount) throws Exception{
    if(stonesAmount<20 || stonesAmount>100){
      throw new Exception("Stones must be in [20,100]");
    }
    else{
      this.stonesAmount = stonesAmount;
    }
  }

  // Set метод за takeLimit
  public void initTakeLimit(int takeLimit) throws Exception{
    if(takeLimit<3 || takeLimit>10){
      throw new Exception("Take limit must be in [3,10]");
    }
    else{
      this.takeLimit = takeLimit;
    }
  }

  // Метод за премахване на камъни от купчината
  public boolean takeStones(int toTake){
    if(toTake<1 || toTake>this.takeLimit){
      return false;
    }
    else{
      this.stonesAmount -= toTake;
      return true;
    }
  }

}

// Самата игра се контролира от тук
class StonesGame implements Runnable{
  int id;            // номер на играта
  int activePlayers; // колко активни играча има
  User[] users;      // списък с играчите
  int playerOnTurn;  // кой играч е на ход
  StonesPile SP;     // купчината камъни

  // Конструктор
  public StonesGame(int id, User u0, User u1, User u2){
    this.id = id;
    this.users = new User[3];
    this.users[0] = u0;
    this.users[1] = u1;
    this.users[2] = u2;
    this.activePlayers = 3;
    this.playerOnTurn = 0;
    // Първоначално ще го вземем по подразбиране
    SP = new StonesPile();
    this.sendToAll("SRV: Game started. "+
                   "Please wait for players 1 and 2.");
  }

  // Играта започва
  public void run(){
    // В този блок взимаме броя на камъните от играч 1
    try{
      users[0].send("SRV: You are player 1. "+
                    "Please send stones in the pile.");
      SP.initStonesPile(users[0].get());
    }
    catch(Exception e){
      sendToAll("SRV: Player 1 sent incorrect amount");
      sendToAll("SRV: We will use the default pile");
    }
    this.sendToAll("SRV: Stones amount is "+SP.stonesAmount);

    // Ред е на играч 2 да изпрати лимита за взимане
    this.playerOnTurn = 1;
    try{
      users[1].send("SRV: You are player 2. "+
                    "Please enter take limit");
      SP.initTakeLimit(users[1].get());
    }
    catch(Exception e){
      // Тук може и е удачно на P2 да се изпрати e.getMessage()
      // и евентуално да го накараме да изпрати отново
      // но в случая за простота ще го пропуснем
      sendToAll("SRV: Player 2 sent incorrect take limit");
      sendToAll("SRV: We will use the default limit");
    }
    this.sendToAll("SRV: Take limit is "+SP.takeLimit);

    // Вече сме готови да заиграем по същество
    this.playerOnTurn = 2;
    this.sendToAll("SRV: We are ready to go. Player 3's turn");

    // Всяка итерация на този цикъл ще е ход на играч
    runloop: while(true){
      // Ако имаме само един останал играч, той печели
      if(this.activePlayers<2){
        this.sendToAll("SRV: All players left. You win");
        this.sendToAll("SRV: BYE");
        break runloop;
      }

      // Ако вече е играл трети играч, връщаме на първи
      if(playerOnTurn > 2) playerOnTurn = 0;

      // Ако играчът е напуснал, пропускаме
      if(users[playerOnTurn]==null) continue runloop;

      // Вече можем да направим ход
      try{
        // С тази променлива проверяваме за валиден ход
        Boolean validMove = false;

        byte mistakes = 0;   // брой направени грешки
        int stonesTaken = 0; // колко камъка взима

        // С цикъл ще го караме да повтаря до валиден ход
        do{
          // Подканваме го да вземе камъни
          users[playerOnTurn].send("SRV: Please take stones");
          // Той подава своето желание - число, брой камъни
          stonesTaken = users[playerOnTurn].get();
          // Виждаме дали реално ги е взел или не
          validMove = SP.takeStones(stonesTaken);
          // Ако не може, увеличаваме грешките му за хода
          if(!validMove) mistakes++;
          // Ако са станали 3, ще го изгоним
          if(mistakes==3){
            sendToAll("SRV: Player "+(playerOnTurn+1)+
                      " is disconnected for too many wrong moves");
            this.removeUser(playerOnTurn);
          }
        }
        while(validMove==false);

        // Уведомяваме всички кой играч колко камъка взе
        this.sendToAll("SRV: Player "+
                       (playerOnTurn+1)+
                       " takes "+
                       ((SP.stonesAmount>0)?stonesTaken:" all")+
                       " stones. Stones in pile left: "+
                       ((SP.stonesAmount>0)?SP.stonesAmount:0)
                      );
      }
      // При каквато и да е грешка при комуникацията
      // изхвърляме играча от играта
      catch(IOException e){
        this.removeUser(playerOnTurn);
      }

      // Ако не са останали камъни, имаме победител!
      if(SP.stonesAmount <=0){
        this.sendToAll("SRV: We have a winner! It's Player "+
                       (playerOnTurn+1));
        this.sendToAll("SRV: BYE");
        break runloop;
      }

      // Иначе играта продължава
      playerOnTurn++;
    }

    // В края на програма изхвърляме всички потребители
    for(int i=0; i<users.length; i++){
      removeUser(i);
    }
    System.out.println("Game "+this.id+" ended");
  }

  // Метод за изпращане на съобщение до всички
  private void sendToAll(String msg){
    for(int i=0; i<users.length; i++){
      try{
        if(users[i]!=null) users[i].send(msg);
      }
      catch(IOException e){
        this.removeUser(i);
      } 
    }
  }

  // За изтриване на играч от масива
  private void removeUser(int i){
    if(users[i]==null) return;

    // Това е по-добре да се направи като метод
    // в клас users. Тук пряко нарушаваме капсулацията
    try{
      if(users[i].in!=null) users[i].in.close();
      if(users[i].out!=null) users[i].out.close();
      if(users[i].s!=null) users[i].s.close();
    }
    catch(IOException e){}

    // Премахваме играча
    this.users[i] = null;
    this.activePlayers--;

    // Може да стане рекурсия!
    // SendToAll също извиква removeUser :)
    this.sendToAll("SRV: Player "+
                   (i+1)+" has left the game");
  }
}

Клиент:

package StonesClient;
import java.util.Scanner;
import java.io.IOException;
import java.net.Socket;
import java.io.DataOutputStream;
import java.io.DataInputStream;

public class StonesClient{
  // Къде се свързваме?
  final static String host = "localhost";
  final static int port = 1234;
  public static void main(String[] args){
    // Опит за връзка...
    System.out.println("Connecting to server... ");
    Socket s;
    DataInputStream in;
    DataOutputStream out;
    Scanner keybIn = new Scanner(System.in);
    try{
      s = new Socket(host, port);
      in = new DataInputStream(s.getInputStream());
      out = new DataOutputStream(s.getOutputStream());
    }
    // Ако попаднем тук, значи неуспешен
    catch(IOException e){
      System.out.println("Cannot connect");
      return;
    }
    // А тук свързването е успешно!
    System.out.println("done!");

    // Ще записваме съобщенията от сървъра тук
    String msgFromServer;
    // А в тази променлива ще четем числа от клавиатурата
    // Трябва ни за индекси - ред и стълб при правене на ход
    int numToSend;

    try{
      // Докато не ни изхвърлят от играта...
      while(true){
        // Чета съобщението на сървъра
        msgFromServer = in.readUTF();
        // Отпечатвам го на екрана
        System.out.println(msgFromServer);

        // Ако то е нещо свързано с край на играта
        if(msgFromServer.equals("SRV: BYE")){
          // приключвам с безкрайния цикъл
          break;
        }
        // Иначе проверявам дали сървъра не ме подканва да дам ход...
        else if(msgFromServer.equals("SRV: Please take stones") ||
                msgFromServer.equals("SRV: You are player 1. Please send stones in the pile.") ||
                msgFromServer.equals("SRV: You are player 2. Please enter take limit")){
          // и ако е така - давам ход...
          System.out.print("Enter integer: ");
          // четейки ред от клавиатурата
          numToSend = keybIn.nextInt();
          // и изпращайки го до сървъра
          out.writeInt(numToSend);
          // Предполагам, че вече другия трябва да играе
          System.out.println("Client waits for other players");
        }
      }
    }
    // Ако по някаква причина изгубим връзка не по наша вина
    catch(IOException e){
      System.out.println("Connection lost");
    }
    // Затваряме си връзката, ако такава въобще има
    finally{
      try{
        if(in!=null) in.close();
        if(out!=null) out.close();
        if(s!=null) s.close();
      }
      catch(IOException e2){}
    } 
  }
}

Допълнителна задача: Помислете за таймер за ход! В момента един играч може да се откаже да прави ход, но да не затвори връзката и всички останали ще трябва да го чакат. Това определено няма да им е приятно.

П.П. Трябва най-накрая да сложа copy to clipboard линк над всеки blockquote :)

 



4 коментара


  1. Относно послеписа: трябва и едно синтактично оцветяване на текста и заспива.

  2. Аз не обичам да товаря сайта със скриптове - гледам да се зарежда максимално бързо... Кой всъщност ще чете кода на страницата - по-лесно е copy/paste и направо в средата :)

  3. Мен примерно ме мързи да пускам кода, искам да му хвърля едно око набързо и да го компилирам наум :D

Добави коментар

Адресът на електронната поща няма да се публикува


*