Gioco del Tris in rete

Questo progetto è stato tratto dall'articolo pubblicato al seguente: link. Il gioco è realizzato scrivendo un'applicazione server (Tic Tac Toe Server) e un'applicazione client (Tic Tac Toe Client). Le due applicazioni Java sono state modificate (in alcuni punti in peggio) per poterle scrivere con NetBeans. In particolare, sarebbe preferibile creare un array di label, ma l'analisi critica completa e la ricerca di soluzioni più efficienti a questo e ad altri problemi viene lasciata come esercizio all'abile programmatore.


L'applicazione Server

Avviare NetBeans. Creare un nuovo progetto di Tipo Java Application. Premere il pulsante Next.

Assegnare il nome all'applicazione (ad esempio: TrisServer) e togliere la marca di spunta alla riga "Create Main Class". Premere il pulsante Finish.

Clic destro sul nome del progetto e, dal menu contestuale, scegliere: New → JFrame Form …. Nella casella Class Name scrivere: giocoTris. Inserire il frame in un package denominato ad esempio: my.tris. Premere il pulsante Finish.

L'avanzamento del gioco è regolato da un protocollo tra i client ed il server:

I tipi di messaggi che il client può inviare al server sono due:

I tipi di messaggi che il server può inviare al client sono:

  • CELLA n dove: 0 ≤ n ≤ 8

  • ESCI

  • Tu sei: c, dove c è uno dei caratteri 'X' o 'O'.

  • MOSSA VALIDA

  • MOSSA AVVERSARIO n dove n è il numero, compreso tra 0 e 8, della cella.

  • VITTORIA

  • SCONFITTA

  • PAREGGIO

  • AVVISO messaggio

Il protocollo consiste nel far precedere ciascun messaggio da un testo. In questo modo il funzionamento del server può essere provato con Telnet.

Proprietà della classe.

Le seguenti due proprietà della classe Frame sono istanze della classe Giocatore che verrà incorporata nella classe Frame.

L'array griglia riproduce in memoria dello stato del gioco:

private Giocatore[] griglia = {

    null, null, null,

    null, null, null,

    null, null, null

};

La variabile turno indica il giocatore corrente:

Giocatore turno;

Metodi della classe.

La funzione haVinto() viene richiamata dopo che uno dei giocatori ha effettuato la propria mossa. Stabilisce se il giocatore corrente ha allineato tre pedine.

public boolean haVinto() {

  return

        (griglia[0] != null && griglia[0] == griglia[1] && griglia[0] == griglia[2])

      ||(griglia[3] != null && griglia[3] == griglia[4] && griglia[3] == griglia[5])

      ||(griglia[6] != null && griglia[6] == griglia[7] && griglia[6] == griglia[8])

      ||(griglia[0] != null && griglia[0] == griglia[3] && griglia[0] == griglia[6])

      ||(griglia[1] != null && griglia[1] == griglia[4] && griglia[1] == griglia[7])

      ||(griglia[2] != null && griglia[2] == griglia[5] && griglia[2] == griglia[8])

      ||(griglia[0] != null && griglia[0] == griglia[4] && griglia[0] == griglia[8])

      ||(griglia[2] != null && griglia[2] == griglia[4] && griglia[2] == griglia[6]);

}

La funzione grigliaPiena() restituisce il valore vero se non ci sono celle vuote:

public boolean grigliaPiena() {

  for (int i = 0; i < griglia.length; i++) {

    if (griglia[i] == null) {

      return false;

    }

  }

  return true;

}

La funzione mossaIn() riceve due parametri: il numero della cella e il giocatore che intende posizionare la pedina. Viene richiamata dal thread Giocatore per controllare se la mossa è consentita, cioè se è il turno del giocatore e se la cella è libera. Se le condizioni sono soddisfatte, la cella viene occupata con la pedina del giocatore corrente e il turno passa all'altro giocatore.

public synchronized boolean mossaIn(int cella, Giocatore pedina) {

  if (pedina == turno && griglia[cella] == null) {

    griglia[cella] = turno;

    turno = turno.avversario;

    turno.mossaAvversario(cella);

    return true;

  }

  return false;

}

La classe Giocatore

All'interno della classe del Frame incoporare la seguente classe Giocatore che eredita proprietà e metodi dalla classe Thread. Un giocatore è identificato dal segno 'X' o 'O'. Inoltre ogni giocatore usa un proprio canale di comunicazione.

class Giocatore extends Thread {

  char segno;

  Giocatore avversario;

  BufferedReader input;

  PrintWriter output;

  Socket socket;

il Costruttore.

Associa un segno di identificazione al giocatore e apre due canali sul socket per comunicare con il client. Comunica al client il segno con cui giocherà

  public Giocatore(Socket socket, char segno) {

    this.socket = socket;

    this.segno = segno;

    try {

      input = new BufferedReader(

            new InputStreamReader(socket.getInputStream()));

      output = new PrintWriter(socket.getOutputStream(), true);

      output.println("Tu sei: " + segno);

      output.println("AVVISO in attesa che si connetta l'avversario");

    } catch (IOException e) {

      System.out.println("Il giocatore si è disconnesso: " + e);

    }

  }

Il metodo fissaAvversario() assegna il valore alla proprietà avversario.

  public void fissaAvversario(Giocatore avversario) {

    this.avversario = avversario;

  }

Il metodo mossaAvversario() invia i messaggi relativi alla casella scelta e all'eventuale vittoria.

  public void mossaAvversario(int cella) {

    output.println("MOSSA AVVERSARIO " + cella);

    output.println(haVinto() ? "SCONFITTA" : grigliaPiena() ? "PAREGGIO" : "");

  }

Il metodo run()

  public void run() {

    try {

      output.println("AVVISO Tutti i giocatori connessi");

      if (segno == 'X') {

        output.println("AVVISO é il tuo turno");

      }

      while (true) {

        String comando = input.readLine();

        if (comando.startsWith("CELLA")) {

          int cella = Integer.parseInt(comando.substring(6));

          if (mossaIn(cella, this)) {

            output.println("MOSSA VALIDA");

            output.println(haVinto() ? "VITTORIA" : grigliaPiena() ? "PAREGGIO": "");

          } else {

            output.println("AVVISO ?");

          }

        } else if (comando.startsWith("ESCI")) {

          return;

        }

      }

    } catch (IOException e) {

      System.out.println("Giocatore disconnesso: " + e);

    } finally {

      try {socket.close();} catch (IOException e) {}

    }

  }

} // fine classe Giocatore

Il gestore dell'evento "ascolta()"

La pressione del pulsante collocato sul frame, mette il server in ascolto di richieste di connessione:

try {

  ascolta();

} catch(IOException e) {

  jLabel1.setText("errore");

}

La funzione ascolta

Dopo aver ricevuto una richiesta di connessione, si crea un'istanza di Giocatore, associandogli il socket e il segno di identificazione., Poi si assegna il valore alla proprietà avversario, si stabilisce il turno del primo giocatore e si aviano i thread.

public void ascolta() throws IOException {

  ServerSocket ascoltatore = new ServerSocket(8901);

  try {

    Giocatore giocatoreX = new Giocatore(ascoltatore.accept(), 'X');

    Giocatore giocatoreO = new Giocatore(ascoltatore.accept(), 'O');

    giocatoreX.fissaAvversario(giocatoreO);

    giocatoreO.fissaAvversario(giocatoreX);

    turno = giocatoreX;

    giocatoreX.start();

    giocatoreO.start();

  } finally {

    ascoltatore.close();

  }

}

Provare il funzionamento del protocollo utilizzando telnet.


L'applicazione Client

Avviare NetBeans e creare un nuovo progetto: trisClient. Togliere la marca di spunta alla casella: Create Main Class e premere il pulsante Finish.

Fare clic destro sul nome del progetto e, dal menu contestuale, scegliere New → JFrame Form …. Nella casella Class Name scrivere: frmClientTris. Inserire il frame in un package denominato, ad esempio: my.clienttris. Premere il pulsante Finish.

Dalla tavolozza dei componenti prelevare un Panel e collocarlo sul frame. Ridimensionarlo per coprire l'intero frame.

Preparare le tre immagini di dimensioni 35x40:

b.jpg O.jpg ics.jpg

Progetto dell'interfaccia.

Nella figura sono stati indicati i nomi delle label, allo scopo di usare i corretti riferimenti nel programma proposto. La proprietà Text delle label dovrebbe essere cancellata, ma in questa versione si consiglia di impostarla al valore "-".

La finestra dovrà mostrare le immagini delle caselle disposte in una griglia 3x3.
Le immagini verranno assegnate alla proprietà icon dei componenti Label.
Posizionare una label sul pannello.

Preparare il package per le immagini.
Nella finestra del progetto fare clic destro sul package my.clienttris e, dal menu contestuale, scegliere la voce New Java package. Denominarlo my.clienttris.my.img e premere il pulsante Finish. Il package deve essere contenuto nella cartella Source Package.

Selezionare la label, facendo clic sul componente aggiunto al pannello oppure facendo clic sul nome del componente nella finestra Navigator. Fare clic sul pulsante con i puntini (…) nella finestra delle proprietà, sulla riga icon.

Premere il pulsante Import to Project …. Sfogliare le cartelle del computer per raggiungere le immagini. Selezionare l'immagine, premere il pulsante Next e selezionare la cartella destinazione (corrispondente al nome assegnato al package). Ripetere l'operazione per ciascuna immagine da importare. Alla fine, nella cartella my/img ci devono stare le immagini importate.

Dopo aver importato le immagini, la proprietà icon deve essere impostata a "b.jpg". Cancellare la proprietà text del componente Label.

Con il clic destro sulla label scegliere la voce Duplicate, dal menu contestuale, ed allineare la nuova label. Ripetere l'operazione, fino ad ottenere la griglia di 3x3 label. Attenzione: disporre le label nell'ordine seguente:

jLabel1 jLabel2 jLabel3
jLabel4 jLabel5 jLabel6
jLabel7 jLabel8 jLabel9

Aggiungere un componente TextField ed assegnare il valore alla proprietà Text: 127.0.0.1. Aggiungere un pulsante sul pannello e assegnare il valore alla proprietà Text: Connetti.

Aggiungere un componente TextArea.

Aggiungere 4 label allineandole al di sotto del componente TextArea (vedere figura):

Passare alla scheda Source.

Aggiungere le seguenti proprietà alla classe:

private static int PORT = 8901;

private Socket socket;

private BufferedReader in;

private PrintWriter out;

private char segnoMio, segnoAvversario;

private int casellaCorrente;

private ImageIcon icon, avversario;

Per eliminare gli errori segnalati, clic destro su un punto qualsiasi del foglio con il codice sorgente e, dal menu contestuale, scegliere "Fix Imports".

Tornare alla scheda Design. Selezionare il pulsante. Nella finestra delle proprietà clic sul pulsante Events, ed aggiungere il gestore di evento al pulsante Connetti. Si apre la scheda Source, con il cursore posizionato sul gestore aggiunto. Al suo interno scrivere le righe seguenti:

String indirizzo = jTextField1.getText();

try {

  socket = new Socket(indirizzo, PORT);

  in = new BufferedReader(new InputStreamReader(socket.getInputStream()));

  out = new PrintWriter(socket.getOutputStream(), true);

} catch(IOException e) {}

try {

  play();

} catch (Exception ex) { }

La seguente funzione riceve come parametri il numero della cella e il segno, quindi cambia la label corrispondente.

private void casella(int cella, char segnoXO){

icon = new javax.swing.ImageIcon(getClass().getResource(segnoXO=='X' ? "/my/clienttris/img/ics.jpg" : "/my/clienttris/img/O.jpg"));

  switch(cella) {

    case 0:

      jLabel1.setIcon(icon);

      break;

    case 1:

      jLabel2.setIcon(icon);

      break;

    case 2:

      jLabel3.setIcon(icon);

      break;

    case 3:

      jLabel4.setIcon(icon);

      break;

    case 4:

      jLabel5.setIcon(icon);

      break;

    case 5:

      jLabel6.setIcon(icon);

      break;

    case 6:

      jLabel7.setIcon(icon);

      break;

    case 7:

      jLabel8.setIcon(icon);

      break;

    case 8:

      jLabel9.setIcon(icon);

      break;

  }

}

La classe Frame contiene una classe incorporata che eredita dalla classe Thread. Il metodo run del thread legge i messaggi ricevuti dal server e indica lo stato del gioco:

class gioca extends Thread {

  public void run() {

    String msgDalServer;

    try {

      while (true) {

      msgDalServer = in.readLine();

      if (msgDalServer.startsWith("MOSSA VALIDA")) {

        jLabel12.setText("Mossa valida, aspetta");

        casella(casellaCorrente, segnoMio);

      } else if (msgDalServer.startsWith("MOSSA AVVERSARIO")) {

        int loc = Integer.parseInt(msgDalServer.substring(17));

        casella(loc, segnoAvversario);

        jLabel12.setText("L'avversario ha giocato, è il tuo turno");

      } else if (msgDalServer.startsWith("VITTORIA")) {

        jLabel12.setText("Hai vinto");

        break;

      } else if (msgDalServer.startsWith("SCONFITTA")) {

        jLabel12.setText("Hai perso");

        break;

      } else if (msgDalServer.startsWith("PAREGGIO")) {

        jLabel12.setText("Hai pareggiato");

        break;

      } else if (msgDalServer.startsWith("AVVISO")) {

        jLabel12.setText(msgDalServer.substring(7));

      }

    }

    out.println("ESCI");

    } catch(IOException e) {}

  }

}

Il metodo seguente viene richiamato quando si preme il pulsante "Connetti". Resta in attesa del messaggio inviato dal server e, dopo averlo ricevuto, lancia il thread.

ATTENZIONE: verificare il corretto percorso delle immagini.

public void play() throws Exception {

  String msgDalServer;

  try {

  msgDalServer = in.readLine();

    if (msgDalServer.startsWith("Tu sei:")) {

      char segno = msgDalServer.charAt(8);

      icon = new javax.swing.ImageIcon(getClass().getResource(segno=='X' ? "/my/clienttris/img/ics.jpg" : "/my/clienttris/img/O.jpg"));

      avversario = new javax.swing.ImageIcon(getClass().getResource(segno=='X' ? "/my/clienttris/img/O.jpg" : "/my/clienttris/img/ics.jpg"));

      jLabel10.setIcon(icon);

      jLabel11.setIcon(avversario);

      segnoMio = (segno == 'X' ? 'X' : 'O');

      segnoAvversario = (segno == 'X' ? 'O' : 'X');

      jLabel10.setText("Tu sei: " + segnoMio);

      jLabel11.setText("Avversario: " + segnoAvversario);

    }

    gioca g = new gioca();

    g.start();

  }

  finally {

  }

}

I gestori degli eventi sulle label:

Attenzione: non incollare tutto il codice. Il gestore di evento onMousePressed, per ogni label, deve essere creato attraverso la scheda Events di netBeans, bisogna solo completare, ciascun gestore, con le due istruzioni:

casellaCorrente=n;
out.println("CELLA n");

private void jLabel1MousePressed(java.awt.event.MouseEvent evt) {

  casellaCorrente=0;

  out.println("CELLA 0");

}

private void jLabel2MousePressed(java.awt.event.MouseEvent evt) {

  casellaCorrente = 1;

  out.println("CELLA 1");

}

private void jLabel3MousePressed(java.awt.event.MouseEvent evt) {

  casellaCorrente = 2;

  out.println("CELLA 2");

}

private void jLabel4MousePressed(java.awt.event.MouseEvent evt) {

  casellaCorrente = 3;

  out.println("CELLA 3");

}

private void jLabel5MousePressed(java.awt.event.MouseEvent evt) {

  casellaCorrente=4;

  out.println("CELLA 4");

}

private void jLabel6MousePressed(java.awt.event.MouseEvent evt) {

  casellaCorrente = 5;

  out.println("CELLA 5");

}

private void jLabel7MousePressed(java.awt.event.MouseEvent evt) {

  casellaCorrente = 6;

  out.println("CELLA 6");

}

private void jLabel8MousePressed(java.awt.event.MouseEvent evt) {

  casellaCorrente = 7;

  out.println("CELLA 7");

}

private void jLabel9MousePressed(java.awt.event.MouseEvent evt) {

  casellaCorrente = 8;

  out.println("CELLA 8");

}

Problema: riprodurre un suono, di breve durata, in corrispondenza delle azioni dei giocatori.

Suggerimento: incollare il seguente codice all'inizio della funzione casella()

    try {
            AudioInputStream audio = AudioSystem.getAudioInputStream(new File("Ding.wav"));
            Clip clip = AudioSystem.getClip();
            clip.open(audio);
            clip.start();
        }
        catch(Exception e) {     }

Importare le librerie necessarie.

Inserire i file audio nella cartella dist, in cui è presente il file di tipo jar. Completare aggiungendo gli opportuni suoni, ad esempio, in corrispondenza di una mossa non valida o in funzione del risultato della partita.