AngoLinux

Gioco guardie e ladri con pipe o mailbox con compilazione condizionale

- A cura del Prof. Stefano Salvi -


Facciamo sperimentare anche la compilazione condizionale.

Il testo dell'esercizio è il seguente:

Scrivere un programma in C per giocare a guardie e ladri contro il calcolatore.
Il programma dovrà essere la fusione dei due precedenti con le pipe e con i messaggi, utilizzando una compilazione condizionale per produrre l'una versione o l'altra.
Il ladro sarà rapperesentata da un carattere $, che si sposta casualmente sullo schermo, cercando di allontanarsi dalla guardia, se il metodo di comunicazione è la messagebox.
La guardia sarà rappresentata da un carattere #, che verrà mosso sullo schermo con i tasti freccia dall'utente. Quando la guardia ed il ladro si incontreranno, il gioco terminerà.
Useremo tre processi:
  • Un primo processo (il padre) che gestirà la visualizzazione e controllerà l'incontro della guardia con il ladro, ponendo fine al gioco
  • Un primo figlio che muoverà il ladro a caso, entro un raggio di 1 casella attorno alla posizione precedente, ad un ritmo di 5 spostamenti al secondo, spostandosi però di più nella direzione opposta alla guardia.
  • Un secondo figlio che leggerà la tastiera e sposterà di conseguenza la guardia.
    Questo processo comunicherà di tanto in tanto la sua posizione al ladro (ad esempio ogni 10 spostamenti) se la comunicazione avviene tramite la messagebox.
Useremo una messagebox per far comunicare i figli con il padre e tra loro, oppure una pipe per fare comnunicare i figli con il padre.
Se useremo le messagebox, ogni processo avrà il suo tipo di messaggio. Entrambi i figli scriveranno un record con le coordinate ed il tipo di figlio e lo invieranno con il tipo messaggio del padre.
La guardia invierà di tanto in tanto lo stesso messaggio anche con il tipo di messaggio del ladro, per avvisarlo della sua posizione.
Il padre leggerà dalla pipe e visualizzerà il movimento.
Quando il gioco termina, il padre, che rileva la terminazione, invia un messaggio ad ogni figlio, per farlo terminare. Questo messaggio avrà un tipo di figlio uguale a '0'.
I figli, che attenderanno questo tipo messaggio, termineranno la loro esecuzione all'arrivo del prossimo evento (il prossimo spostamente per il ladro, il prossimo tasto per la guardia).
Se invece useremo le pipe, useremo un'unica pipe per far comunicare i figli con il padre. Entrambi i figli scriveranno un record con le coordinate ed il tipo di figlio e lo invieranno sulla stessa pipe.
Il padre leggerà dalla pipe e visualizzerà il movimento.
Quando il gioco termina, il padre, che rileva la terminazione, termina i figli.

NOTE:

  • Per generare processi in Linux si usa la funzione int fork (). Questa funzione genera un nuovo processo come copia del processo corrente.
    Il nuovo processo avrà una copia esatta delle variabili del processo originale e l'esecuzione di questo nuovo processo comincerà dal punto immediatamente successivo alla funzione fork ().
    Per distinguere il processo che ha richiamato la fork (chiamato in genere padre) da quello prodotto ex novo dalla funzione stessa (chiamato figlio), si deve analizzare il valore di ritorno della funzione stessa:
    Un valore minore di zero (-1)
    Siamo nel processo padre, nessun figlio è stato creato (errore).
    Zero
    Siamo nel figlio.
    Un valore maggiore di zero
    Siamo nel padre. Il numero ritornato è il Process Identifyer (PID) del processo figlio generato.
  • Per fare un ritardo breve in maniera semplice possiamo usare la funzione usleep (int microsecondi);. Come parametro indicheremo il numero di microsecondi di ritardo che vogliamo fare.
  • Per creare una nuova coda di messaggi potremo utilizzare la funzione int msgget(key_t key, int msgflg);. Il parametro key indica la coda cui collegarsi. Se vogliamo una nuova coda, da usare solo nel nostro programma, dovremo utilizzare il valore standard IPC_PRIVATE.
    Nel parametro msgflg potremo mettere il valore IPC_CREAT pre creare la nuova coda.
    Se la creazione non va a buon fine, la funzione ritorna il valore minore do 0. Se invece ritorna un valore maggiore di 0, questo sarà il riferimento alla coda appena creata.
  • Per inserire messaggi nella coda utilizzeremo la funzione int msgsnd(int msqid, struct msgbuf *msgp, size_t msgsz, int msgflg);.
    Il parametro msqid sarà l'identificativo della coda ritornato da msgget. Il parametro msgp punta alla struttura da inserire nella coda. Questa struttura portrà contenere tutto ciò che voglamo spedire, ma il primo campo dovrà essere un long, contenente il tipo di messaggio (un numero positivo). Il parametro msgsz sarà la dimensione dei dati ad eccezione del long che indica il tipo di messaggio, quindi sizeof(msg)-sizeof(long). Il parametro msgflg conterrà una serie di costanti per modificare il comportamento della funzione. Nel caso della msgsnd non useremo nessuna opzione. La funzione ritorna un valore minore di 0 se incontra un'errore.
  • Per recuperare messaggi dalla coda potremo utilizzare la funzione ssize_t msgrcv(int msqid, struct msgbuf *msgp, size_t msgsz, long msg_typ, int msgflg);
    Il parametro msqid sarà l'identificativo della coda ritornato da msgget. Il parametro msgp punta alla struttura nella quale mettere il dato recuperato dalla coda. Il primo campo di questa struttura dovrà essere obbligatoriamente un long, nel quale verrà posto il tipo di messaggio (un numero positivo). Il parametro msgsz sarà la dimensione del buffer a disposizione ad eccezione del long che indica il tipo di messaggio, quindi sizeof(buf)-sizeof(long). Il parametro msg_typ indicherà quali messaggi estrarre dalla coda. Se sarà:
    positivo
    verrà estratto il primo messaggio il cui tipo corrisponde a msg_typ
    zero
    verrà estratto il primo messaggio della coda, qualunque tipo esso abbia.
    negativo
    verrà estratto il primo messaggio il cui tipo minore o uguale al valore assoluto di msg_typ
    Il parametro msgflg conterrà una serie di costanti per modificare il comportamento della funzione. Ad esempio potremo utilizzare IPC_NOWAIT se vogliamo che la funzione non si blocchi in attesa di un messaggio, nel caso la coda sia vuota.
  • Per creare una pipe, si deve usare la funzione int pipe (int fildes [2]);. Questa funzione pone nei interi i descrittori di due file non bufferati:
    fildes [0]
    è il descrittore aperto in ingresso. Potremo usarlo per la funzione read.
    fildes [1]
    è il descrittore aprerto in uscita, lo potremo usare per la funzione write.
  • Per la visualizzazione si useranno le funzioni di ncurses, come spiegate nel relativo tutorial.
  • Per attendere la terminazione di un figlio, si può utilizzare la funzione waitpid (int pid, int *status);. Questa funzione attende la terminazione del processo il cui PID corrisponde al valore del parametro pid. Nella variabile intera status, il cui indirizzo verrà passato alla funzione, verranno insrite una serie di informazioni su come il programma è stato terminato.
  • Per terminare un processo si dovrà inviargli un segnale con la funzione int kill(pid_t pid, int sig);. Dovremo utilizzare il PID che ci ha consegnato la funzione fork, per il figlio. Come segnale potremo utilizzare un qualunque numero (ad esempio 1).
  • Per creare un movimento casuale potremo sommare un numero casuale che vada da -RAGGIO a +RAGGIO ad entrambe le coordinate della posizione corrente del ladro.
    La funzione random(); ritorna un numero intero positivo compreso tra 0 e MAXINT. Per ottenere quindi un numero casuale in un campo minore useremo l'operatore modulo (ad esempio random () % MAX). Dato che noi vogliamo un numero compreso tra -RAGGIO e RAGGIO, avremo RAGGIO numeri negativi, RAGGIO numeri positivi e lo 0, quindi 2*RAGGIO+1 possibili numeri, che otterremo con random()%(RAGGIO*2+1).
    Queste combinazioni sono per&ogravi tutte positive. Per averne RAGGIO negative, dovremo sottrarre al numero ottenuto RAGGIO+1, quindi la formula definitiva sarà random()%(RAGGIO*2+1)-(RAGGIO+1).
    Se poi vogliamo che il movimento casuale porti comunque il ladro in una certa direzione, possiamo caloclare il nonstro spostamento casuale non a partire dalla vecchia posizione, ma da una posizione spostata nella direzione in cui vogliamo muoverci.
    Ammettiamo di avere un secondo punto e volerci allontanare da esso. Se sottraiamo alle nostre coordinate quelle di quel punto, avremo due numeri ognuno dei quali sarà:
    Positivo
    se la relativa coordinata del punto da sfuggire è inferiore alla nostra, quindi per fuggire dovremo aumentare la nostra coordinata
    Uguale a 0
    se la relativa coordinata del punto da sfuggire è uguale alla nostra, quindi per fuggire dovremo lasciare immutata la nostra coordinata e fuggire lungo l'altra direzione
    Negativo
    se la relativa coordinata del punto da sfuggire è superiore alla nostra, quindi per fuggire dovremo calare la nostra coordinata
    Se noi limitiamo questi numeri al campo -1...1 ed aggiungiamo questi numeri alle coordinate correnti del ladro nella formula precedente, esso tenderà a muoversi nella direzione opposta al punto di riferimento.
    Nel caso della pipe, potremo usare la stessa formula, ma lasciare a zero lo spostamento, per non tenere conto della posizione della guardia, che non conosciamo.
  • È necessario controllare che né la guardia né il ladro escano dal campo.
    Nel caso della guardia, occorrerà bloccarla quando raggiunge il bordo.
    Per il ladro, invece, è opportuno invertire lo spostamento se questo portasse fuori campo.
  • È opportuno scrivere funzioni diverse per i figli ed il campo, invece che includere tutto il codice nella funzione main.

Una possibile soluzione è la seguente:
/* guardieladripm.c
 * Stefano Salvi - 19/9/02
 * Gioco 'guardie e ladri'.
 * Un 'ladro' (rappresentato da un '$') fugge a caso sullo schermo.
 * Una 'guardia' (raprresentata da un '#') lo insegue, pilotato dai tasti del giocatore.
 * Il gioco termina quando la guardia raggiunge il ladro.
 * 
 * Per realizzare il gioco utilizzeremo tre processi:
 * 1) processo 'padre'. Gestisce la visualizzazione e verifica se la guardia ha preso il ladro.
 *    Per fare questo deve memorizzarsi le coordinate del ladro.
 *    Quando il gioco e' terminato, 'uccide' i figli e termina (se usa le pipe) oppure invia
 *    un messaggio ai figli, che terminano, ed attende la loro terminazione.
 *
 * 2) processo 'figlio - ladro'. Genera casualmente uno spostamento, che comunica al 'padre'
 *    per la visualizzazione
 *
 * 3) processo 'figlio - guardia'. Legge la tastiera e calcola la nuova posizione, in base 
 *    ai tasti letti. Comunica questa nuova posizione al 'padre'.
 *
 * I tre processi comunicano tramite un'unica pipe oppure con una coda di messaggi. 
 * Ognuno dei due figli scrivera' un'apposita struttura nella pipe. Il Padre leggera' in 
 * continuazione dalla pipe ed agira' di conseguenza.
 * Nel caso della coda di messaggi, periodicamente la guardia inviera' tramite la coda la 
 * sua posizione al ladro.
 * Il padre inviera' un messaggio ad entrambi i figli, per farli terminare.
 *
 *
 * X compilare:
 * gcc guardieladripm.c -o guardieladrip -lncurses (versione pipe)
 * gcc -DMESSAGE -oguardieladrim guardieladripm.c -lncurses (versione messaggi)
 */

#include <stdio.h>
#include <curses.h>
#include <stdlib.h>
#include <unistd.h>

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <sys/stat.h>


#define RAGGIO	 1	/* di quanto si sposta il ladro ad ogni ciclo */

#define	CAMPOTYP 2	/* Messaggi per il campo */
#define	LADROTYP 3	/* Messaggi per il ladro */
#define	GUARDIATYP 4	/* Messaggi per la guardia */

#define	SU	 65	/* Freccia su */
#define GIU	 66	/* Freccia giu */
#define	SINISTRA 68	/* Freccia sinsitra */
#define DESTRA	 67	/* Freccia destra */

#define	MAXX	 80	/* Numero di colonne */
#define MAXY	 24	/* Numero di righe */

// #define	MESSAGE

/* Struttura per la comunicazione tra figli e padre */
struct pos{
#ifdef	MESSAGE
  long mtype;	//tipo di messaggio
#endif
  char c;	//ladro($) o guardia(#)
  int x;	//posizione x
  int y;	//posizione y
};


void ladro (int canale);
void guardia (int canale);
void campo (int canale);

int main()
{
#ifdef	MESSAGE
int msqid;	// riferimento della coda dei messaggi
#else
int p[2];	// Array per la 'pipe' (coppia di descrittori)
#endif
int status;	// stato di ritorno dei processi figli

int pidLadro;	// Pid del figlio 'ladro'
int pidGuardia;	// Pid del figlio 'guardia'
	
  initscr();		// Inizializza curses
  noecho();		// caratteristica della tastiera
  curs_set(0);		// elimina il cursore

#ifdef	MESSAGE
  // Crea la coda dei messaggi
  msqid = msgget(IPC_PRIVATE,IPC_CREAT | S_IREAD | S_IWRITE);
#else
  pipe(p);		// Crea la pipe per comunicare	
#endif

  pidLadro = fork();	// Genera il primo 'figlio'

  if(pidLadro==0)       // pidLadro == 0 -> e' il figlio (ladro)
  {
#ifdef	MESSAGE
    ladro (msqid);	// Esegue il lavoro del ladro
#else
    ladro (p [1]);	// Esegue il lavoro della guardia
#endif
  }		
  else
  {			// Siamo ancora nel padre
    pidGuardia=fork();  // Genera il secondo figlio (guardia)

    if(pidGuardia==0)   // pidGuardia == 0 -> e' il figlio (guardia)
    {
#ifdef	MESSAGE
      guardia (msqid);	// Esegue il lavoro della guardia
#else
      guardia (p [1]);	// Esegue il lavoro della guardia
#endif
    }
    else
    {			// Siamo ancora nel padre
#ifdef	MESSAGE
      campo (msqid);	// Svogliamo il lavoro del padre (gestire il campo)
#else
      campo (p [0]);	// Svogliamo il lavoro del padre (gestire il campo)
#endif
    }
  }
		
#ifdef	MESSAGE
  waitpid(pidLadro,&status,0);
  waitpid(pidGuardia,&status,0);
#else
  kill(pidLadro,1);	// Termina il ladro (con segnale 1 - uno qualunque va' bene);
  kill(pidGuardia,1);	// Termina la guardia (con segnale 1 - uno qualunque va' bene)

  getchar();		// Attendiamo la pressione di un tasto, prima di chiudere
#endif

  endwin();		// Torniamo in modalita' normale

  return 0;		// Termine del programma
}

/* Funzione 'ladro'.
 * Genera un 'movimento casuale' in un raggio RAGGIO rispetto al punto corrente,
 * nel campo di 80X25 caratteri.
 * Il movimento tende comunque a spostare il ladro lontano dalla guardia.
 * La guardia comunica i suoi spostamenti al ladro (se uso la messagebox)
 * Il movimento viene comunicato al padre tramite la messagebox o la pipe.
 */
void ladro (int canale)
{
struct pos pladro;	// Posizione corrente del ladro
#ifdef	MESSAGE
struct pos pinput;	// Posizione della guardia, ricevuta
#endif
int deltax;		// Spostamento orizzontale
int deltay;		// Spostamento verticale
int fugax = 0,fugay = 0;// Spostamento del centro, per allontanarsi

  // Posizione iniziale del ladro: in alto a sinistra.
  pladro.x = 1;
  pladro.y = 1;
  pladro.c='$';		// lo caratterizza come ladro
#ifdef	MESSAGE
  pladro.mtype=CAMPOTYP; //canale di invio della posizione del ladro al padre
  // Posizione iniziale della guardia: il ladro sta' fermo.
  pinput.x = 1;
  pinput.y = 1;
#endif

  while(1)	// Ciclo infinito. Verra' terminato con una 'return'
  {
#ifdef	MESSAGE
    if (msgrcv(canale,&pinput,sizeof(pinput)-sizeof(long),LADROTYP,IPC_NOWAIT) >0 )
    {
      if(pinput.c=='0')
      {
        return;
      }
    }

    fugax =  pladro.x - pinput.x;	// Individua la direzione della guardia
    // normalizza fugax (-1, 0, 1)
    if (fugax < 0)
    {
      fugax = -1; 
    }
    else if (fugax > 0)
    {
      fugax = 1;
    }

    fugay =  pladro.y - pinput.y;	// Individua la direzione della guardia
    // normalizza fugay (-1, 0, 1)
    if (fugay < 0)
    {
      fugay = -1; 
    }
    else if (fugay > 0)
    {
      fugay = 1;
    }
#endif

    /* Calcola uno spostamento casuale da -RAGGIO ... RAGGIO.
     * Abbiamo 2*RAGGIO+1 combinazioni (le RAGGIO negative, le RAGGIO positive e lo 0).
     * 'random() % (RAGGIO*2+1)' ritorna combinazioni da 0 a 2*RAGGIO: quelle che servono
     * ma tutte positive. Per averne RAGGIO negative, dovremo toglier RAGGIO+1
     */
    deltax= ((random() % (RAGGIO*2+1)) - (RAGGIO + 1) + fugax);

    // Se con lo spostamento esce, inverto lo spostamento
    if(pladro.x + deltax < 1 || pladro.x + deltax >= MAXX)
    {
        deltax = -deltax;	// Inverto lo spostamento
    }

    pladro.x += deltax;		// Spostamento orizzontale

    /* Calcola uno spostamento casuale da -RAGGIO ... RAGGIO.
     * per l'algoritmo vedere il deltax
     */
    deltay = ((random()%(RAGGIO*2+1)) - (RAGGIO + 1) + fugay);

    // Se con lo spostamento esce, inverto lo spostamento
    if(pladro.y + deltay < 1 || pladro.y + deltay >= MAXY)
    {
      deltay = -deltay;
    }

    pladro.y += deltay;		// Spostamento verticale

#ifdef	MESSAGE
    msgsnd(canale,&pladro,sizeof(pladro)-sizeof(long),0);
#else
    write(canale,&pladro,sizeof(pladro));  //passo al padre la posizione del ladro
#endif
			
    usleep(200000);		// Pausa per 'dare il ritmo'
  }
}


/* Funzione 'guardia'.
 * Legge i tasti premuti dall'utente. Sposta la guardia nella direzione
 * indicata dalle frecce. Si ferma ai bordi del campo di 80X25 caratteri.
 * Il movimento viene comunicato al padre tramite la messagebox o la pipe.
 * Se uso la messagebox, ogni 10 spostamenti il movimento viene anche comunicato al ladro
 */
void guardia (int canale)
{
struct pos pguardia;	// Posizione corrente della guardia
#ifdef	MESSAGE
struct pos rcampo;	// Risposta dal campo (terminazione)
int i = 0;
#endif

  // Posizione iniziale : in basso a destra
  pguardia.x = MAXX - 1;
  pguardia.y = MAXY - 1;
  pguardia.c='#';		// lo caratterizza come 'guardia'
#ifdef	MESSAGE
  pguardia.mtype=CAMPOTYP;	//canale dell'invio della posizione della guardia al padre

  //passo al padre la posizione iniziale
  msgsnd(canale,&pguardia,sizeof(pguardia)-sizeof(long),0);
#else
  write(canale,&pguardia,sizeof(pguardia));  // passo al padre la posizione iniziale
#endif


  while(1)	// Ciclo infinito. Verra' terminato con una 'return'
  {
  char c;
    c = getch();				// Legge il tasto

    if (c==SU && pguardia.y > 0)		// Freccia SU
    {
      pguardia.y-=1;				// Cala la y
    }

    if(c==GIU  && pguardia.y < MAXY - 1)	// Freccia GIU
    {
      pguardia.y+=1;				// Cresce la y
    }

    if((c==SINISTRA)&&pguardia.x > 0)		// Freccia SINISTRA
    {
      pguardia.x-=1;				// Cala la x
    }

    if(c==DESTRA && pguardia.x < MAXX - 1)	// Freccia DESTRA
    {
      pguardia.x+=1;				// Cresce la x
    }

#ifdef	MESSAGE
    //passo al padre la posizione la posizione corrente
    pguardia.mtype=CAMPOTYP;	//canale dell'invio della posizione della guardia al padre
    msgsnd(canale,&pguardia,sizeof(pguardia)-sizeof(long),0);
					
    if (msgrcv(canale,&rcampo,sizeof(rcampo)-sizeof(long),GUARDIATYP,IPC_NOWAIT) >0 )
    {
      if(rcampo.c=='0')
      {
        return;
      }
    }

    i++;
    if(!(i%10))
    {
      pguardia.mtype=LADROTYP;	//canale dell'invio della posizione della guardia al ladro
      msgsnd(canale,&pguardia,sizeof(pguardia)-sizeof(long),0);
    }				
#else
    write(canale,&pguardia,sizeof(pguardia));  //passo al padre la posizione della guardia
#endif
  }
}

/* Funzione di visualizzazione e controllo (padre).
 * Attende 'eventi' dalla pipe o dalla messagebox (nella quale scrivono entrambi i figli).
 *
 * Se non e' il primo evento (per ogni figlio), prima cancello la posizione
 * vecchia.
 *
 * Se la posizione della guardia corrisponde a quella del ladro, termina il gioco.
 */ 
void campo (int canale)
{
struct pos ladro,guardia, transito;
 
  ladro.x=-1;	// Mai usato
  guardia.x=-1;	// Mai usato

  while(-1)	// Ciclo infinito: terminero' con un return
  {
#ifdef	MESSAGE
    if (msgrcv(canale,&transito,sizeof(transito)-sizeof(long),CAMPOTYP,0) >0 )
    {				
#else
    read(canale,&transito,sizeof(transito));	// Leggo l'evento
#endif
      if(transito.c=='$')			// e' il ladro
      {
        if (ladro.x>=0)				// Se non e' la prima volta
        {
          mvaddch(ladro.y,ladro.x,' ');		// Cancello il vecchio ladro
        }
        ladro=transito;				// Aggiorno la posizione del ladro
      }
      else 					//e' la guardia
      {
        //devo prima cacellare il la pos precedente della guardia
        if (guardia.x>=0)			// Se non e' la prima volta
        {
          mvaddch(guardia.y,guardia.x,' ');	// Cancello la vecchia guardia
        }
        guardia=transito;			// Aggiorno la posizione della guardia
      }

      mvaddch(transito.y,transito.x,transito.c); // Disegno il nuovo 'personaggio'
      curs_set(0);				// Rielimino il cursore
      refresh();				// Aggiorno le modifiche (cancell. e disegno)

      if(guardia.x==ladro.x&&guardia.y==ladro.y)
      {	// Il ladro ha raggiunto la guardia
#ifdef	MESSAGE
	transito.c='0';
        // Invia messaggio di terminazione al ladro
	transito.mtype=LADROTYP; //canale di invio al ladro della fine del programma
   	msgsnd(canale,&transito,sizeof(transito)-sizeof(long),0);
        // Invia messaggio di terminazione alla guardia
   	transito.mtype=GUARDIATYP; //canale di invio alla guardia della fine del programma
   	msgsnd(canale,&transito,sizeof(transito)-sizeof(long),0);
#endif
        return;	
      }
#ifdef	MESSAGE
    }
#endif
  }
}

Per provare il programma, scaricare il sorgente, compilarlo con il comando cc guardieladripm.c -o guardieladrip -lncurses per ottenere la versione con la pipe, da eseguire con il comando ./guardieladrip.
Per la versione con la messegebox compilarlo con il comando cc guardieladripm.c -DMESSAGE -o guardieladrim -lncurses oppure togliere il commento alla istruzione #define MESSAGE ed eseguire con il comando ./guardieladrim.


[Home Page dell'ITIS "Fermi"] [Indice Quarta] [Precedente] [Successivo]

© Ing. Stefano Salvi - Released under GPL licence