Tennis

Un torneo di tennis virtuale viene giocato attraverso una piattaforma web che permette agli utenti di registrarsi e giocare partite contro avversari scelti casualmente dal sistema o tra utenti che si accordano per giocare una partita. Le partite possono essere giocate in una delle seguenti due modalità:

  1. al meglio dei 3 set, dove vince il giocatore che si aggiudica per primo 2 set su 3, oppure
  2. al meglio dei 5 set, dove vince chi si aggiudica 3 set su 5.

I risultati delle partite vengono registrati in un file di testo avente il seguente formato:

gioc1 gioc2 nset x1-y1 x2-y2 ...

Il significato dei campi di ciascuna riga è il seguente:

  • gioc1 e gioc2 sono i nomi del primo e del secondo giocatore, rispettivamente; sono stringhe di massimo 50 caratteri senza spazi;
  • nset è il numero massimo di set da giocare (3 o 5), espresso come numero intero;
  • x1-y1, il punteggio del primo set;
  • x2-y2, il punteggio del secondo set;
  • seguono i punteggi degli altri set, se presenti.

Esempi di riga:

Nole Roger 5 7-6 6-7 6-4 6-3
Rafa Nole 5 6-7 5-7 7-6 6-1 6-0

Si scriva un programma in linguaggio C che legga il file dei risultati e calcoli le quantità specificate nelle varie richieste. Il nome del file da leggere deve essere fornito al programma come unico argomento sulla linea di comando.

NOTA: per chi non conoscesse sufficientemente le regole del tennis, basti sapere che un set è composto da giochi, o game; il set viene vinto dal giocatore che si aggiudica per primo 6 giochi, oppure 7 giochi se si giunge al punteggio di 5 pari o 6 pari. Per esempio, un set che termina 7-5 significa che il primo giocatore ha vinto 7 giochi, mentre il secondo ne ha vinti 5. Il set è vinto dal primo giocatore.

Suggerimenti per la lettura

Per memorizzare i dati di una partita è possibile usare una struttura dati come la seguente:

struct partita {
    char g1[51], g2[51];  // nomi dei giocatori
    int maxset;           // numero massimo di set da giocare
    int n_set;            // numero di set giocati
    int score[5][2];      // game giocati nei vari set
};

Il significato dei campi è nei commenti. In particolare score[i][j] memorizza il punteggio della partica nel set i (con i compreso in [0,4]) per il giocatore j (con j compreso in {0,1}).

La funzione leggi_file dichiarata come segue può essere usata per la lettura del file:

struct partita *leggi_file(FILE *f, int *count);

Essa restituirà l'indirizzo di un vettore di strutture struct partita, allocato al suo interno tramite malloc e realloc. Il file da leggere è f, già aperto in lettura con fopen prima di chiamare la leggi_file. Il parametro count passato per riferimento conterrà il numero di partite lette, ovvero il numero di elementi nel vettore restituito.

1) Partita con più giochi

Determinare la partita nella quale sono stati giocati più giochi in totale (max_giochi).

Stampare i dati nel seguente formato:

[MAX-GIOCHI]
gioc1 gioc2 max_giochi

Se più partite hanno totalizzato un numero di giochi pari a max_giochi, stampare la prima che compare nel file.

Suggerimenti

È utile definire una funzione come la seguente:

int giochi_in_partita(struct partita *partita);

la quale riceve in ingresso una struttura che memorizza i dati di una partita, il cui indirizzo è partita, e restituisce il numero di games giocati nella partita. La funzione conterrà un ciclo for che itera da 0 a partita->n_set (il numero di set nella struttura puntata da partita) e somma i punteggi dei due giocatori per ciascun set.

La funzione giochi_in_partita potrà essere usata all'interno di un'altra funzione, dichiarata come segue:

int indice_partita_max_giochi(struct partita *elenco, int n, int *max);

la quale riceve come parametro il vettore elenco di tutte le partite lette da file, e il loro numero n, e restituisce l'indice della prima partita che contiene il numero massimo di giochi. Il numero di giochi per partita viene calcolato usando la giochi_in_partita suggerita precedentemente. La funzione restituisce il valore intero max, che le viene passato per riferimento, contenente il numero massimo di giochi tra tutte le partite.

La funzione indice_partita_max_giochi può essere richiamata a cua volta nel main:

indice = indice_partita_max_giochi(partite, n, &max_giochi);

dove partite è il vettore restituito dalla leggi_file. indice può essere usato per stampare il nome dei giocatori della partita corrispondente.

2) Giochi totali

Determinare il numero totale di game (tot_giochi) giocati tra tutte le partite memorizzate nel file.

Si stampi a video tale valore nel seguente formato:

[TOT-GIOCHI]
tot_giochi

Suggerimenti

Si può creare una funzione come la seguente:

int tot_giochi(struct partita *elenco, int n);

la quale, dato il vettore elenco di partite lette da file, e il suo numero n, restituisce il numero totale di games giocati. Per realizzarla basta scrivere un ciclo for che itera su tutte le n partite (ricorda che n è il secondo parametro) e, al suo interno, un ciclo for che itera su tutti i set di ciascuna partita (campo n_set nella partita), e aggiunge il punteggio di tutti i set in tutte le partite ad una variabile locale alla funzione, che conterrà il conteggio totale.

ATTENZIONE: ricorda di inizializzare a zero il valore della variabile che viene usata per il conteggio.

3) Media set

Determinare il numero medio di set giocati in tutte le partite giocate al meglio dei 3 set (media3). Effettuare lo stesso calcolo per le partite giocate al meglio dei 5 set (media5).

Si stampi a video i valori calcolati nel seguente formato:

[MEDIA]
media3
media5

Se non è possibile calcolare la media per una tipologia di partite, stampare il testo NULL.

Suggerimenti

Si può creare una funzione con il seguente prototipo:

double media(struct partita *elenco, int n, int n_set);

Essa riceve l'elenco elenco delle n partite da considerare (n è il secondo parametro). Inoltre, n_set indica il numero di set delle partite da considerare nel calcolo della media. La funzione conterrà un ciclo for che itera sulle n partite, e sommerà il numero di set giocati (campo n_set nella struttura struct partita) solo se tale numero è uguale al valore del parametro n_set.

Oltre al conteggio della somma, bisogna contare il numero di set considerati nella sommatoria, in modo da calcolare la media.

Il calcolo della media può non essere possibile se il numero di set di interesse è pari a zero, ovvero sarebbe zero il denominatore nel calcolo della media. In tal caso la funzione può restituire un valore minore di zero, per "comunicare" al codice chiamante che il valore della media non è da considerarsi valido.

La funzione media può essere richiamata dal main due volte, per calcolare il numero medio di set in partite com massimo 3 e 5 set come segue:

media3 = media(partite, n, 3);
media5 = media(partite, n, 5);

4) Numero di tie break

Determinare il numero totale di set che è terminato al tie-break (n_tiebreak). Un set termina al tie break se viene vinto col punteggio di 7-6 (oppure 6-7).

Si stampi a video il valore nel seguente formato:

[TIE]
n_tiebreak

Suggerimenti

Si può creare una funzione con il seguente prototipo:

int tie(struct partita *elenco, int n);

Essa riceve l'elenco elenco delle n partite da considerare (n è il secondo parametro). La funzione conterrà un ciclo for che itera sulle n partite, e conteggerà il numero di partite il cui numero totale di games giocati è pari a 13 (7+6).

5) Numero utenti unici

Calcolare il numero n_utenti di utenti unici che hanno giocato almeno una partita elencata nel file. Il conteggio degli utenti unici prevede che se un utente ha giocato più di una partita, esso deve essere conteggiato una volta sola.

Si stampi il risultato col seguente formato:

[UTENTI]
n_utenti

Suggerimenti

Per risolvere questo punto e il successivo conviene dichiarare una nuova struttura dati come segue:

struct utente {
    char nome[51];
    int punteggio;
};

Essa servirà per creare un vettore di queste strutture, ciascuna delle quali si riferisce ad un utente, per il quale si conserverà anche il punteggio utile per rispondere al punto (6).

La funzione seguente servirà per restituire l'elenco degli utenti unici:

struct utente *elenco_utenti_unici(struct partita *elenco, int n, int *count);

Essa riceve come parametri il vettore elenco di n partite lette da file (n è il secondo parametro), e restituisce un vettore di strutture struct utente, allocato tramite malloc e realloc all'interno della funzione stessa. Il parametro count, passato per riferimento, serve a restituire il numero di utenti trovati.

Il valore puntato dal parametro count, al termine dell'esecuzione della funzione, conterrà il risultato da stampare per risolvere questo punto.

NOTA: al suo interno, la funzione elenco_utenti_unici funziona più o meno come la leggi_file, solo che i dati da "leggere" non sono in un file ma all'interno del vettore elenco. Tra l'altro, la realloc per aumentare la dimensione del vettore degli utenti non serve, in quanto si sa già quale è il numero massimo di utenti: esso sarà pari al numero di partite, ovvero n, moltiplicato per due!

La funzione elenco_utenti_unici può essere resa elegante usando una funzione che restituisce l'indice di un utente già eventualmente presente nel vettore, oppure -1 se l'utente non è presente, nel qual caso può essere aggiunto al vettore. Questa funzione potrà essere dichiarata come segue:

int indice_utente(struct utente *elenco, int n, char *nome);

e prende come parametri il vettore elenco degli n utenti attualmente presenti nel vettore, mentre nome è il nome dell'utente che deve essere cercato nel vettore.

La funzione elenco_utenti_unici farà un ciclo su tutte le partite, e per ciascuna partita potrà controllare se l'utente è già presente con delle istruzioni simili alle seguenti:

    struct utente *utenti;
    ...
    presente = indice_utente(utenti, *count, (elenco + i)->g1);
    if (presente < 0) strcpy(utenti[(*count)++].nome, (elenco + i)->g1);

Le due istruzioni saranno all'interno del ciclo che itera su tutte le partite. Come si vede, le due istruzioni servono per gestire la verifica della presenza del primo giocatore, e vanno ripetute anche per il secondo giocatore. Il valore di *count viene incrementato, e il nome del giocatore viene copiato nel vettore, solo se il valore restituito da indice_utente è negativo, il che significa che il nome non è già presente nel vettore.

6) Classifica

Si determini la classifica calcolata sulla base dei risultati presenti nel file. Un giocatore guadagna un punto per ogni set vinto (attenzione, non per ogni game!), indipendentemente dalla vittoria finale della partita.

Stampare i primi 10 giocatori classificati nel seguente formato:

[CLASSIFICA]
punteggio nome_giocatore
...
punteggio nome_giocatore

Se più giocatori hanno totalizzato il medesimo punteggio finale, li si ordini in ordine alfabetico crescente.

Se il numero totale di giocatori è minore di 10, stampare solo i giocatori presenti.

Suggerimenti

Per risolvere questo punto va utilizzato il vettore degli utenti unici creato come spiegato nel punto (5).

Inoltre, si può dichiarare una funzione come la seguente:

void n_set_vinti(struct partita *partita, int *v1, int *v2);

Essa calcola il numero di set vinti dal primo e dal secondo giocatore nella partita puntata da partita. I due valori sono memorizzati rispettivamente all'indirizzo puntato da v1 e v2.

La funzione n_set_vinti può essere usata all'interno di una funzione che valuta, per tutte le partite, quanti set sono stati vinti dai due giocatori che giocano la partita. La funzione può essere dichiarata come segue:

void calcola_punteggi(struct partita *elenco, int n, struct utente *utenti, int n_utenti);

Essa riceve come parametri il vettore elenco delle n partite giocate, e il vettore utenti degli n_utenti identificati al punto (5).

La funzione calcola_punteggi effettuerà un ciclo per tutte le partite, e per ciascuna chiamerà la funzione n_set_vinti per ottenere il numero di set vinti dai due giocatori. Dopodichè, cercherà i due nomi dei giocatori nel vettore utenti e incrementerà il punteggio dei due giocatori in base al numero di set vinti da ciascuno.

Per determinare la classifica, basta ordinare il vettore utenti al termine dell'esecuzione di calcola_punteggi e stampare i dati delle prime 10 strutture presenti nel vettore ordinato.

Per l'ordinamento del vettore si può usare la qsort, facendo attenzione a ordinare per punteggio, e in caso di parità, ordinando alfabeticamente per nome. Si può usare una funzione di confronto come la seguente:

int cmp_utenti(const void *p1, const void *p2)
{
    const struct utente *u1 = p1;
    const struct utente *u2 = p2;
    if (u1->punteggio < u2->punteggio)
        return 1;
    else if (u1->punteggio > u2->punteggio)
        return -1;
    else {   // stessi punti: ordino per nome
        return strcmp(u1->nome, u2->nome);
    }
}

Si ricordi che la funzione di confronto deve restituire:

  • un risultato positivo se il primo argomento è maggiore del secondo
  • un risultato negativo se il primo argomento è minore del secondo
  • un risultato pari a zero se sono uguali

Due argomenti sono uguali solo se hanno lo stesso punteggio e lo stesso nome.

Verifica automatica

Si utilizzi il tool pvcheck di verifica automatica per testare il corretto funzionamento del programma (maggiori informazioni circa l'uso di pvcheck sono disponibili qui).

Il file contenente i test è tennis.test. Per eseguire i test è necessario scaricare anche i seguenti file di dati che sono da salvare nella medesima directory del file di test:

Il comando da eseguire per il test è il seguente:

./pvcheck -f tennis.test ./a.out

Il programma può anche essere verificato manualmente, per esempio utilizzando il primo dei due file di dati, eseguendo:

./a.out test_2gioc_10000par.txt

Nella prossima pagina potrai esaminare un esempio di soluzione dell'esercizio.