Batalla naval en PHP

De Cacauet Wiki
Dreceres ràpides: navegació, cerca

Anem a fer un joc de batalla naval en PHP.

Requeriments[modifica]

  1. El servidor tindrà una partida emmagatzemada (fixa o aleatòria)
  2. L'usuari anirà destapant els quadres. El servidor, depenent de l'entrada de l'usuari, hi pintarà:
    • si es troba una vaixell hi pintarà una "X"
    • si no n'hi ha, pintarà aigua "~"
  3. S'han d'acumular els quadres destapats.
  4. Cal de desactivar els quadres ja destapats perquè l'usuari no els torni a enviar.
  5. Cal comprovar el final de partida (tots els vaixells enfonsats) amb la funció final_de_partida()
  6. Donarem la puntuació a l'usuari (nombre d'intents totals realitzats).


Disseny[modifica]

Batalla amb sessions PHP

Contemplem dues estratègies:

  • Amb sessions: és la única manera de poder fer una partida amb vaixells col·locats aleatòriament i amb botons.
  • Sense sessions: només podrem fer una partida fixa, i per arrossegar els valors de la partida caldrà un formulari amb checkboxes (podria també ser un select o desplegable).

Batalla naval2.png

La batalla sense sessions ens obliga a utilitzar un formulari per poder arrossegar el valor de les caselles dins de $_POST.


Implementació sense sessions i formulari gegant[modifica]

Si no utilitzem sessions, no podrem emmagatzemar dades, pel que ens serà impossible tenir una partida aleatòria persistent d'una forma senzilla. Podríem utilitzar un arxiu per solucionar-ho. A l'exemple mostrat el què fem es marcar uns vaixells fixes predefinits (hardcoded) a mode d'exercici (no ens seria suficient per un joc real, però és una primera aproximació). Els definirem en un array en format de nombres de dos xifres que indicaran fila i columna (per exemple, un vaixell amb 2 posicions com el què es veu a la foto de més amunt):

$vaixells = array( "12", "13" );

Farem un formulari "gegant" amb checkboxes. Tots tenen el mateix "name=shootme" però amb un "value" diferent. Per tant, a la variable $_POST["shootme"] ens arribarà un array amb les "values" de cada checkbox que estigui marcat. Per exemple:

<input type='checkbox' name='shootme[]' value='34' />

El value='34' (fila, columna) ens indica que aquest checkbox correspon a la casella de la fila 3, columna 4.

Quan haguem marcat aquest checkbox, el servidor rebrà:

$_POST["shootme"] = Array( [0] => "34" )

I per poder "acumular" les tirades, caldrà que quan tornem a pintar el input, ho haguem de fer amb els atributs "checked" i "readonly", així l'usuari el veurà activat i alhora no el podrà desactivar.

<input type='checkbox' name='shootme[]' value='$i$j' checked readonly />


Arxiu principal[modifica]

<html>
<head>
<style>
	td {
		width: 2em;
		height: 2em;
		text-align: center;
	}
	.aigua {
		background-color: blue;
	}
	.tocat {
		background-color: red;
	}
</style>
</head>
 
<body>
<h1>Batalla naval sense sessions</h1>
<?php	// un "xivato" per depurar
	//if(isset($_POST["shootme"])) print_r($_POST["shootme"]);
?>
<form method="post">
<table>
<?php
	require('batalla_funcions.php');
 
	// vaixells predefinits o "hardcoded"
	// el nº sempre de 2 xifres s'interpreta com a <fila><columna>, per ex. "25" = fila 1, columna 5
	$vaixells = array( "12", "13",		// 1 vaixell de 2 posicions
			   "35", "45", "55"	// 1 vaixell de 3 posicions
			 	);
	// pintem tauler i ens retorna el nº de vaixells tocats
	$tocats = pinta_tauler($vaixells);
?>
</table>
<input type="submit" />
</form>
<?php
	// Calculem si hem arribat al "final de partida"
	if( $tocats==count($vaixells) ) {
		echo "<p>FELICITATS! Has destruit tota la flota enemiga!</p>\n";
		echo "<p>Puntuació: ".puntuacio()."</p>\n";
	}
?>
</body>
</html>

Arxiu batalla_funcions.php[modifica]

Fixa't en què:

  • L'estratègia serà construir un <td class="tocat"> o <td class="aigua"> allà on haguem disparat.
  • Enlloc de fer un <td><input...</td> a cada IF, hem optat per modificar unes variables i només imprimir el <td...</td> al final de tot utilitzant aquestes variables. Fa tot plegat una mica més senzill.
  • Les variables a modificar son:
    • $estilcss
    • $contingut
    • $checked
    • $readonly

Finalment "imprimirem" (o "pintarem") la cel·la amb:

$contingut = "<input type='checkbox' name='shootme[]' value='$i$j' $checked $readonly />";
...
echo "<td class='$estilcss'>$contingut</td>\n";
<?php
 
function pinta_tauler( $vaixells ) {
    $tocats = 0;
    // doble bucle per fer files (i) i columnes (j)
    for( $i=0; $i<=8; $i++ ) { 
        echo "<tr>\n";
        for( $j=0; $j<=8; $j++ ) {
            // cal definir i inicialitzar aquí les variables
            $estilcss = "";
            $contingut = "";
            $checked = "";
            $readonly = "";
            if( $i==0 )
                // pintem nº de columna
                $contingut = "$j";
            elseif( $j==0 )
                // pintem nº de fila
                $contingut = "$i";
            else {
                // cel·la "normal" (amb checkbox)
                if( isset($_POST["shootme"]) ) {
                    // revisem si la cel·la actual $i$j esta a l'array de vaixells
                    if( in_array("$i$j",$vaixells) && in_array("$i$j",$_POST["shootme"]) ) {
                        // tocat!!
                        $estilcss = "tocat";
                        $checked = "checked";
                        // cal posar readonly (i no disabled) per bloquejar la casella
                        $readonly = "readonly";
                        $tocats++;
                    }
                    elseif( in_array("$i$j",$_POST["shootme"]) ) {
                        // aigua!!
                        $estilcss = "aigua";
                        $checked = "checked";
                        $readonly = "readonly";
                    }
                    // si no està a $_POST es que no hem disparat en aquesta cel·la
                }
                // pintem formulari
                $contingut = "<input type='checkbox' name='shootme[]' value='$i$j' $checked $readonly />";
            }
            // pintem la cel·la
            echo "<td class='$estilcss'>$contingut</td>\n";
        }
        echo "</tr>\n";
    }    
    return $tocats;
}
 
function puntuacio() {
    if( !isset($_POST["shootme"]) )
        return 0;
    return count($_POST["shootme"]);
}
 
?>


Implementació amb sessions[modifica]

L'avantatge de les sessions és que:

  • Podem col·locar els vaixells aleatòriament. Si no tenim sessions no es podria guardar enlloc la situació, i cada cop que recarreguéssim estarien a un lloc diferent.
  • Podem fer botons que cridin la pàgina amb el valor de la cel·la a destapar mitjançant GET, per exemple:
    <button onclick='location.href="?boto=35"'>boto35</button>
    Això simplifica l'enviament de dades: no cal reenviar tot el formulari sencer per POST.
  • Podem enregistrar el rècord de les partides que es van fent.

Aprofitarem per apuntar a una sèrie de bones pràctiques a realitzar quan programem amb PHP.

Arxiu principal[modifica]

A l'arxiu principal gestionarem l'inici de sessió (session_start) i el reset de la partida.

Fixa't en què:

  • Aquest arxiu té poca cosa: la feina es fa a les funcions
    • reinicia_partida()
    • processa_entrada()
    • pinta_taula()
    • puntuacio()
  • A $_SESSION["record"] guardarem el rècord de la partida, que serà la puntuació més baixa (intents fallits). Per això l'inicialitzem més endavant al valor 100 (no arribarem mai perquè només hi ha 64 cel·les.
<?php
    session_start();
 
    require("batallanaval_funcions.php");
 
    // reset: reiniciem sessió i partida
    if( isset($_GET["reset"]) ) {
        // guardem record en variable temporal
        $record = $_SESSION["record"];
        // destruim $_SESSION
        session_destroy();
        session_start();
        // creem partida nova (aleatòria)
        reinicia_partida();
        // retornem el record a la variable $_SESSION
        if( $record )
            $_SESSION["record"] = $record;
    } else if( isset($_GET["boto"]) ){
        // si s'ha pres algun botó, processem quin ha estat aquest boto
        // S'afegirà la posició disparada a un array dins de sessió
        processa_entrada();
    }
?>
<html>
<head>
<style>
    td {
        border: 1px solid black;
        text-align: center;
        width: 1.3em;
        height: 1.3em;
    }
</style>
<meta charset="utf-8" />
</head>
 
<body>
<h1>Batalla naval</h1>
 
<p>Record: <?=$_SESSION["record"] ?> punts.</p>
 
<table>
<?php
    pinta_taula();
?>
</table>
 
<?php
    if( final_de_partida() ) {
        $puntuacio = puntuacio();
        echo "<p>FINAL DE PARTIDA! ENHORABONA!</p>\n";
        echo "<p>La teva puntuació és <b>$puntuacio punts</b>.<p>\n";
        if( $puntuacio<$_SESSION["record"] ) {
            $_SESSION["record"] = $puntuacio;
            echo "<p>FELICITATS! has fet rècord !!!";
        }
    }
?>
 
<br>
X = tocat<br>
~ = aigua<br>
 
<a href="?reset=1">Reinicia partida</a><br>
 
</body>
 
</html>
?>

Arxiu de funcions (batallanaval_funcions.php)[modifica]

Guardarem la partida a $_SESSION["partida"], tot i que aplicarem una bona pràctica i utilitzarem un define d'aquesta manera

define("PARTIDA","partida");
$_SESSION[PARTIDA] = array();

Té l'avantatge que redueix els errors ja que si no poses correctament PARTIDA ens donarà un error de PHP, mentre que si fem $_SESSION["partidaxyz"] per error potser simplement falla el programa però sense donar cap senyal d'error.

De la mateixa manera, i per fer el codi més llegible definirem les constants següents:

define("VAIXELL","x");
define("TOCAT","X");
define("AIGUA","~");
<?php
    // bona pràctica: les constants les fem amb un DEFINE
    // evita errors al utilitzar els arrays associatius
    // enlloc de $_SESSION["partida"] farem $_SESSION[PARTIDA]
    define("PARTIDA","partida");
    // bones pràctiques: utilitzem constants enlloc de símbols com "x" o "~"
    // fa el codi més llegible i entenedor
    define("VAIXELL","x");
    define("TOCAT","X");
    define("AIGUA","~");
 
    function reinicia_partida() {
        // inicialitzem variable $partida (després la guardarem a $_SESSION)
        // serà un array associatiu on posar els vaixells
        $partida = array();
        // 3 vaixells de 2 posicions
        $n2 = 0;
        while( $n2 < 3 ) {
            $fila = rand(1,7);
            $col = rand(1,7);
            $vert = rand(0,1);
            $fila2 = $fila;
            $col2 = $col;
            if( $vert )
                $fila2++;
            else
                $col2++;
            // comprovem si no hem creat una cel·la que ja està ocupada
            if( !isset($partida["$fila$col"]) && !isset($partida["$fila2$col2"]) ) {
                // afegim la cel·la a l'array associatiu
                $partida["$fila$col"] = VAIXELL;
                $partida["$fila2$col2"] = VAIXELL;
                $n2++;
            }
        }
        // guardem partida a la sessió
        $_SESSION[PARTIDA] = $partida;
        // reiniciem rècord (valor màxim)
        if( !isset($_SESSION["record"]) )
            $_SESSION["record"] = 100;
    }
 
    function pinta_taula() {
        $partida = $_SESSION[PARTIDA];
        for($i=1;$i<=8;$i++) {
            echo "    <tr>\n";
            for($j=1;$j<=8;$j++) {
                echo "        <td>";
                if( isset($partida[$i.$j]) && $partida[$i.$j]==TOCAT ) {
                    echo TOCAT;
                } elseif (isset($partida[$i.$j]) && $partida[$i.$j]==AIGUA) {
                    echo AIGUA;
                }
                else {
                    echo "<button onclick='location.href=\"?boto=$i$j\"' >&nbsp;</button>";
                }
                echo "</td>\n";
            }
            echo "    </tr>\n";
        }
    }
 
    function processa_entrada() {
        // carreguem variable de sessió
        $partida = $_SESSION[PARTIDA];
        // capturem per GET valor del botó premut (per ex. index.php?boto=31)
        $val = $_GET["boto"];
        if( isset($partida[$val]) && $partida[$val]==VAIXELL )
            // SÍ! el marquem com a tocat
            $partida[$val] = TOCAT;
        else
            // no... el marquem com a aigua
            $partida[$val] = AIGUA;
        // guardem el valor de la partida
        $_SESSION[PARTIDA] = $partida;
    }
 
    function final_de_partida() {
        // si queda algun vaixell sense enfonsar ("x" minúscula) es que no hem acabat
        if( in_array( VAIXELL, $_SESSION[PARTIDA]) ) {
            return false;
        }
        // si arriem aquí es que no hi ha cap "x"
        return true;
    }
 
    function puntuacio() {
        // el nº de punts seran el nº de cel·les fallides (AIGUA)
        $punts = 0;
        foreach ($_SESSION[PARTIDA] as $key => $value) {
            if( $value == AIGUA )
                $punts++;
        }
        return $punts;
    }
?>