C, PHP, VB, .NET

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


* Tic-tac-toe сървър и клиент

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

Трябва да се създаде сървър за игра на “морски шах” (tic-tac-toe). Играта се играе от двама души върху дъска с девет квадратчета. Първият играч отбелязва знак „Х“ в квадратче по негов избор, след което втория отбелязва знак „О“ и така се редуват до изчерпване на квадратчетата. Целта е един от играчите да подреди три от неговите знаци по хоризонтала, вертикала или диагонал. Примерни изходи от играта са следните:

1Първи играч печели

2Втори играч печели

3Равна игра

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

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

Нишката XOGame да има един конструктор с два параметъра – сокети за връзка със съответните играчи. Защитена член променлива да бъде двумерен масив с размерност 3×3, в който ще се отбелязват ходовете, а самата нишка трябва да работи по следния интерфейс:

interface XOInterface{
  // проверява дали играч 1 е победил
  public boolean hasXWon();
  // проверява дали играч 2 е победил
  public boolean hasOWon();
  // добавя X на позиция i,j в масива. Връща false при невъзможно поставяне
  public Boolean addX(int i, int j);
  // добавя O на позиция i,j в масива. Връща false при невъзможно поставяне
  public Boolean addO(int i, int j);
}

Играта на двамата играчи протича по следния начин:

  • Първи играч подава две числа – i и j – за позиция, на която иска да постави знак X;
  • Втори играч подава две числа – i и j – за позиция, на която иска да постави знак O;
  • Горните се повтарят до изчерпване на възможните ходове (запълване на масива) или победа на един от двамата играчи. На всеки ход сървърът прави проверка за победа.

Ако един от двамата играчи подаде невалиден ход (вече заета клетка или числа извън индексите на масива), той трябва да бъде помолен да повтори хода си. Ако обаче той изпрати последователно три невалидни хода един след друг, неговия опонент трябва да спечели играта служебно. Помислете внимателно как сървъра ще уведомява кой играч е на ход и дали даден ход е извършен коректно.
Реализирайте възможно най-елементарно клиентско приложение, което да работи със създадения от вас сървър.

Решение: Взаимствано като конструкция и стил от предадена контролна работа (оценена за 6), разбира се пооправена така, че да се компилира и да работи (както и на едно-две места добавени „хитрости“ за пестене на дублиращ се код). Съществените разлики са при обработката на try-catch блоковете (които в предаденото решение осезаемо липсваха на много места), но това е нормално и очаквано като за контролна работа. Всичко е нарочно over-commented, за да няма въпроси от типа „това какво е“ :)

Сървър:

// Това е tic-tac-toe сървърът
// Кодът трябва да се поизчисти :)
package XOServer;
import java.io.IOException;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.net.Socket;
import java.net.ServerSocket;

public class XOServer{
  // Порт за връзка със сървъра
  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;

    while(true){
      try{
        // Приемаме двама нови клиенти
        Socket XUser = servSock.accept();
        System.out.println("User X connected");
        Socket OUser = servSock.accept();
        System.out.println("User O connected");
        // Стартираме игра за тях
        Thread t = new Thread(new XOGame(lastGameID, XUser, OUser));
        System.out.println("Game id "+lastGameID+" started");
        t.start();
        lastGameID++;
      }
      catch(IOException e){
        System.err.println("ERR: Lost connection with user: "+e.getMessage());
      }
    }
  }
}

interface XOInterface{
  // проверява дали играч 1 е победил
  public boolean hasXWon();
  // проверява дали играч 2 е победил
  public boolean hasOWon();
  // добавя X на позиция i,j в масива. Връща false при невъзможно поставяне
  public Boolean addX(int i, int j);
  // добавя O на позиция i,j в масива. Връща false при невъзможно поставяне
  public Boolean addO(int i, int j);
}

class XOGame implements Runnable, XOInterface{
  int id;                // id на играта
  DataInputStream Xin;   // за получаване от X
  DataInputStream Oin;   // за получаване от O
  DataOutputStream Xout; // за изпращане до X
  DataOutputStream Oout; // за изпращане до O
  char[][] board;        // XO таблото
  byte Xfailures;        // брой грешки на X
  byte Ofailures;        // брой грешки на O

  public XOGame(int id, Socket X, Socket O) throws IOException{
    this.id = id;
    this.Xin = new DataInputStream(X.getInputStream());
    this.Oin = new DataInputStream(O.getInputStream());
    this.Xout = new DataOutputStream(X.getOutputStream());
    this.Oout = new DataOutputStream(O.getOutputStream());
    this.board = new char[3][3];
    Xfailures = 0;
    Ofailures = 0;
  }

  public void run(){
    /* 
     * Ще се прихващат два вида Exceptions:
     * 1. IOException - при непредвиден проблем с връзката към играч
     * 2. Exception - при предвиден проблем с връзката към играч
     * Доста странно и неясно е така и трябва да се преработи!
     */
    try{
      // Известяваме играчите кой с какво играе
      Xout.writeChar('X');
      Oout.writeChar('O');
      // Променлива обозначаваща край на играта
      boolean hasWon = false;
      // В XO има максимум 9 хода
      for(int i=0; i<9; i++){
        // при четен ход играе X
        if(i%2 == 0){
          // казваме на O да чака
          this.sendTo('O', "SRV: Please wait until X makes his move");
          // изпращаме текущото табло на X
          this.sendGameStatusTo('X');
          // взимаме хода на Х
          this.getXMove();
          // проверяваме дали X печели
          hasWon = this.hasXWon();
          if(hasWon){
            // уведомяваме играчите, че има победител
            // и прекратяваме играта
            notifyWin('X');
            System.out.println("Game "+this.id+" ended");
            return;
          }
          // отново изпращаме статуса на играта на Х
          this.sendGameStatusTo('X');
        }      
        // при нечетен ход играе O - повтаря операциите от горе
        else{
          this.sendTo('X', "SRV: Please wait until O makes his move");
          this.sendGameStatusTo('O');
          this.getOMove();
          hasWon = this.hasOWon();
          if(hasWon){
            notifyWin('O');
            System.out.println("Game "+this.id+" ended");
            return;
          }
          this.sendGameStatusTo('O');
        }
      }
      // Направени са 9 хода и няма победител
      sendTo('X', "Draw game");
      sendTo('O', "Draw game");
    }
    // В този catch ще се влезе при непредвидена грешка
    // например connection reset by peer и подобни
    catch(IOException userConnectionLost){
      try{
        sendTo('X', "SRV: Your opponent connection is lost");
      }
      catch(IOException e){}
      try{
        sendTo('O', "SRV: Your opponent connection is lost");
      }
      catch(IOException e){}
    }
    // Тук ще се влезе при предвидена грешка
    catch(Exception otherError){
      try{
        sendTo('X', "SRV: " +otherError.getMessage());
      }
      catch(IOException e){}
      try{
        sendTo('O', "SRV: " +otherError.getMessage());
      }
      catch(IOException e){}
    }
    // Ако има незатворена връзка, затваряме я
    finally{
      try{
        if(Xin!=null) Xin.close();
        if(Xout!=null) Xout.close();
        if(Oin!=null) Oin.close();
        if(Oout!=null) Oout.close();
      }
      catch(IOException e2){}
    }
    System.out.println("Game "+this.id+" ended");
  }

  // Метод за изпращане на текстови низ до играч
  private void sendTo(char c, String msg) throws IOException{
    if(c=='X' && this.Xout!=null){
      this.Xout.writeUTF(msg);
    }
    else if(this.Oout!=null){
      this.Oout.writeUTF(msg);
    }
  }

  // Метод за уведомяване на играчите кой е победителя
  private void notifyWin(char c) throws IOException{
      sendTo('X', "SRV: "+c+" wins");
      sendTo('O', "SRV: "+c+" wins");
  }

  // Взима ход от Х
  private void getXMove() throws Exception{
    getMove('X', this.Xin, this.Xout);
  }

  // Взима ход от O
  private void getOMove() throws Exception{
    getMove('O', this.Oin, this.Oout);
  }

  // Да взимане на ход от играч
  // "c" е със стойности X или O и указва за кой играч се взима хода
  private void getMove(char c, DataInputStream in, DataOutputStream out) 
    throws Exception{
    // Try блока ще се опита да прихване IOException
    try{
      // Булева променлива за проверка на валидност на ход
      boolean correctMove = false;
      do{
        // Подканваме играча да изпрати хода си
        sendTo(c, "SRV: Send your row and column for move");

        // Прочитаме неговия ход
        int i = in.readInt();
        int j = in.readInt();

        // Опитваме се да добавим знака на съответната позиция
        // Ако хода е невалинен, add функцията ще върне false
        if(c == 'X') correctMove = this.addX(i,j);
        else correctMove = this.addO(i,j);

        // Ако хода не е коректен, правим проверка за брой грешки
        if(!correctMove){
          // Ако грешките са прекалено много, гоним играча
          if((c=='X' && this.Xfailures == 2) || 
             (c=='O' && this.Ofailures == 2)){
            sendTo(c, "SRV: Sorry, too much wrong moves, you lose");
            if(c == 'X'){
              sendTo('O', "SRV: Your opponent made too many mistakes");
              throw new Exception("O wins");
            }
            else{
              sendTo('X', "SRV: Your opponent made too many mistakes");
              throw new Exception("X wins");
            }
          }
          // Ако не са, караме го да изпрати хода си отново
          else{
            sendTo(c, "SRV: Illegal move");
            if(c=='X') this.Xfailures++;
            else this.Ofailures++;
          }
        }
      }
      while(!correctMove);
    }
    // Нашата предвидена IOException, при която се връща... Exception
    // Както казах по-горе - трябва да се преработи :)
    catch(IOException e){
      if(c=='X'){
        sendTo('O', "SRV: Your opponent is gone");
        throw new Exception("O wins");
      }
      else{
        sendTo('X', "SRV: Your opponent is gone");
        throw new Exception("X wins");
      }        
    }
  }

  // Проверява дали X печели
  public boolean hasXWon(){
    return hasWon('X');
  }

  // Проверява дали O печели
  public boolean hasOWon(){
    return hasWon('O');
  }

  // Методът за проверка дали играч "c" печели
  private Boolean hasWon(char c){
    // cпроверка по редове
    for(int i=0; i<3; i++){
      byte elements = 0;
      for(int j=0; j<3; j++){
        if(this.board[i][j]==c) elements++;
      }
      if(elements==3) return true;
    }

    // проверка по стълбове
    for(int j=0; j<3; j++){
      byte elements = 0;
      for(int i=0; i<3; i++){
        if(this.board[i][j]==c) elements++;
      }
      if(elements==3) return true;
    }

    // проверка на диагоналите
    if(this.board[1][1]==c){
      if((this.board[0][0]==c && this.board[2][2]==c)
           ||
         (this.board[0][2]==c && this.board[2][0]==c)){
        return true;
      }
    }
    // ако се стигне до тук, значи няма печеливша позиция
    return false;
  }

  // за добавяне на X на позиция (i,j)
  public Boolean addX(int i, int j){
    return this.add(i, j, 'X');
  }

  // за добавяне на O на позиция (i,j)
  public Boolean addO(int i, int j){
    return this.add(i, j, 'O');
  }

  // за добавяне на елемент "c" на позиция (i,j)
  private Boolean add(int i, int j, char c){
    // дали не сме извън границите на масива?
    if(i<0 || i>2 || j<0 || j>2){
      return false;
    }
    // или пък позицията вече е заета?
    else if(this.board[i][j] == 'X' || this.board[i][j] == 'O'){
      return false;
    }
    // ако всичко е наред, добавяме елемента
    else{
      board[i][j] = c;
      return true;
    }
  }

  // това тук изпраща текущото състояние на играта към играч c
  // Вместо един такъв голям текстови низ, може да се измисли
  // и много по-елегантен начин, но това само ще усложни нещата :)
  private void sendGameStatusTo(char c) throws IOException{
    StringBuilder strb = new StringBuilder();
    for(int i=0; i<3; i++){
      for(int j=0; j<3; j++){
        strb.append("| "+this.board[i][j]+" | ");
      }
      strb.append("\n");
    }
    sendTo(c, strb.toString());    
  }
}

Клиент:

// Това пък е tic-tac-toe сървъра
// Кодът не само трябва да се поизчисти :)
package XOClient;
import java.util.Scanner;
import java.io.IOException;
import java.net.Socket;
import java.io.DataOutputStream;
import java.io.DataInputStream;

public class XOClient{
  // Къде се свързваме?
  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! Waiting for opponent...");

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

    try{
      // Да видим - с X или с O играя?
      System.out.println("I play with: "+in.readChar());

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

        // Ако то е нещо свързано с край на играта
        if(msgFromServer.equals("SRV: X wins") || 
           msgFromServer.equals("SRV: O wins") ||
           msgFromServer.equals("SRV: Draw game")){
          // приключвам с безкрайния цикъл
          break;
        }
        // Иначе проверявам дали сървъра не ме подканва да дам ход...
        else if(msgFromServer.equals("SRV: Send your row and column for move")){
          // и ако е така - давам ход...
          System.out.print("Enter row: ");
          // четейки ред от клавиатурата
          indexToSend = keybIn.nextInt();
          // и изпращайки го до сървъра
          out.writeInt(indexToSend);
          // после същото за колона
          System.out.print("Enter column: ");
          indexToSend = keybIn.nextInt();
          out.writeInt(indexToSend);
          // Предполагам, че вече другия трябва да играе
          System.out.println("I will wait for my opponent to move");
          // П.П. Ако съм дал грешен ход, може и да съм се излъгал :)
        }
      }
    }
    // Ако по някаква причина изгубим връзка не по наша вина
    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){}
    } 
  }
}

Допълнителни задачи:

  1. Аз лично бих направил клас Users, в който да се запишат сокетите и да се прехвърлят някои от методите на XOGame, а в самия XOGame да се запишат две член променливи от тип Users. Реализирайте го – кодът значително ще се изчисти!;
  2. В сегашният си вариант ако влезе първи клиент, започне да чака, но се отегчи и си тръгне, при последващо влизане на втори клиент ще се получи много неприятна засечка – неговата игра ще започне и ще свърши мигновено. Помислетe как може да се разреши този проблем;
  3. А защо не направите една Swing графична среда за клиента?

 



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

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


*