Un petit jeu de cartes web

Message 1, par Elzen

§ Posté le 14/05/2014 à 19h 00m 55

Comme vous le savez, je ne suis pas fan de tout faire dans le navigateur Web, et je considère que pas mal d'applications seraient mieux dans leur client dédié, à plus forte raison celles qui n'ont pas besoin de transferts réseaux.

Néanmoins, les technologies Web habituelles (XHTML, SVG, CSS et JavaScript) offrent beaucoup de possibilités, et il est parfois assez simple (du moins, de mon point de vue) de parvenir à des résultats assez sympathiques en les utilisants.


Voici quelques jours, nesthib, co-administrateur du site TdCT.org qui regroupe quelques services web utiles, m'a contacté pour me demander de le conseiller au sujet d'un jeu web qu'il envisageait de mettre en place.

Je ne rentrerai pas dans les détails de son jeu en particulier, sur lequel je ne me suis en fait pas encore trop attardé ; mais il s'agit d'un jeu de cartes, et comme les conseils en question sont susceptibles de s'appliquer à n'importe quelle autre sorte de jeu de cartes, et donc possiblement d'intéresser d'autres personnes, je me suis dit qu'il pourrait être judicieux que je les reporte ici également.


Ce petit tutoriel sans prétention (et adressé aux personnes ayant déjà quelques notions de langages web, désolées pour celles qui débutent) vous expliquera donc comment on peut, dans une page web, afficher une « main » de cartes et la manipuler.


Commençons par le HTML, puisque c'est lui qui contient la structure de base que les autres parties ne feront que styliser et manipuler :

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
	<meta http-equiv="content-type" content="text/html; charset=utf-8" />
	<title>Jeu de cartes – Tutoriel</title>
	<link rel="stylesheet" href="demo.css" />
	<script type="text/javascript" src="demo.js"></script>
</head>
<body>
	<div id="hand">
		<object class="card" type="image/svg+xml" data="demo.svg">🂠</object>
		<object class="card" type="image/svg+xml" data="demo.svg">🂠</object>
		<object class="card" type="image/svg+xml" data="demo.svg">🂠</object>
		<object class="card" type="image/svg+xml" data="demo.svg">🂠</object>
		<object class="card" type="image/svg+xml" data="demo.svg">🂠</object>
	</div>
	
	<div id="deal">
		<object class="card" type="image/svg+xml" data="demo.svg">🂠</object>
	</div>
</body>
</html>

Je ne fournis ici que les éléments principaux, ceux qui vont réagir aux actions de l'utilisateur ; à vous de remplir le reste du fichier comme vous l'entendez. Ce code fonctionnera d'ailleurs aussi bien que vous utilisez la version la plus récente du langage, ou bien l'une des normes plus anciennes – la balise object est reconnue depuis un bon moment.

Pourquoi cette balise object, d'ailleurs ? Des images matricielles auraient certes fait l'affaire, mais cela aurait, d'une part, pesé un peu plus lourd en terme de transferts ; et d'autre part, été un peu moins souple, comme nous le verrons par la suite.

Le (très simple) fichier SVG utilisé dans cet exemple se trouve ici, si vous voulez tester au fur et à mesure (les autres fichiers utilisés seront liés en fin d'article). Quant aux caractères unicodes représentant les cartes, qui sont fournis comme texte alternatif, référez-vous, par exemple, à cette page.

Dans mon exemple, la première div, « hand », représente la main du joueur, c'est-à-dire les cartes dont dispose le joueur affichant la page. L'autre, « deal », représente le tas de cartes, sur lequel nous viendrons cliquer, tout à l'heure, pour distribuer.


Ceci étant posé, intéressons-nous maintenant au fichier CSS. Nous allons commencer par décorer un peu ces deux zones, et par les positionner. Réservant l'espace en haut de la fenêtre pour les affichages (par exemple, les cartes jouées par les autres joueurs), nous placerons la main en bas de l'écran, vers la gauche, et le tas de cartes vers la droite, comme ceci :

#hand {
	text-align: center;
	background-color: #008800;
	position: absolute;
	padding-top: 20px;
	padding-bottom: 20px;
	border-radius: 5px;
	bottom: 1em;
	width: 600px;
	height: 200px;
}

#deal {
	text-align: center;
	background-color: #008800;
	border-radius: 5px;
	position: absolute;
	padding-top: 20px;
	padding-bottom: 20px;
	bottom: 1em;
	width: 190px;
	height: 200px;
	right: 1em;
}

.card {
	display: inline-block;
	border: 1px solid #000000;
	border-radius: 5px;
	width: 150px;
	height: 200px;
}

Bon, comme vous l'aurez remarqué, ainsi, les cartes occupent trop de place, et débordent de l'espace réservé, ce qui n'est pas génial. Mais qui tient ses cartes ainsi ? Comme « en vrai », nous allons faire en sorte qu'elles se superposent.

Pour cela, il suffit de faire en sorte de décaler vers la gauche toutes les cartes situées après la première dans la main. Décaler vers la gauche, c'est utiliser une marge négative. Pour sélectionner toutes les cartes de la main, sauf la première, nous allons utiliser le sélecteur CSS « + », qui permet de désigner un élément immédiatement précédé d'un autre, comme ceci :

#hand .card + .card {
	margin-left: -75px;
}

Pour compléter l'effet, nous pouvons utiliser la propriété CSS 3 de transformation pour incliner légèrement les premières et dernières cartes :

#hand .card:first-child {
	transform: rotate(-5deg);
}
#hand .card:nth-child(2) {
	transform: rotate(-2deg);
}
#hand .card:nth-child(n+3) {
	transform: rotate(0);
}
#hand .card:nth-last-child(2) {
	transform: rotate(2deg);
}
#hand .card:last-child {
	transform: rotate(5deg);
}

Le rotate(0), au milieu, étant destiné à tout mettre au même niveau (sans lui, les deux premières cartes passeraient au dessus de la suivante).

Bien sûr, si vous préférez en rester au CSS classique, ne récupérez pas cette partie ; et dans ce cas, il vous faudra aussi retirer les « border-radius » un peu plus haut (mais tout le reste sera compatible).


Pour un petit effet visuel sympathique, nous pouvons également faire en sorte que chaque carte se soulève, lorsque l'on passe la souris dessus. Pour cela, il sera nécessaire de rendre les cartes flottantes, afin que l'effet puisse avoir lieu sans perturber la zone :

#hand .card {
	float: left;
	margin-left: 30px;
}
#hand .card:hover {
	margin-top: -50px;
}


Nous en avons maintenant terminé pour la partie purement CSS, et la suite va nécessiter l'usage de JavaScript. L'inconvénient de JavaScript est habituellement que ce n'est pas nécessairement accessible, ni utilisable partout.

Mais, en l'occurrence, le reste de la page ne l'est pas forcément non plus, et les navigateurs susceptibles d'afficher un SVG sont également ceux sur lesquels il est possible d'activer ce langage.

Néanmoins, l'accessibilité étant essentielle, je ne peux que vous encourager, si vous décidez de mettre ce tutoriel en application, de coder également une version « de secours », jouable par exemple dans un navigateur en mode texte.

Mais ça, ça n'entre pas dans le cadre dont nous parlons ici, puisque ça dépend essentiellement de votre moteur de jeu côté serveur.


Nous allons commencer par retourner les cartes que nous voyions jusqu'ici de dos, histoire de voir un peu ce qu'elles nous cachent. Bien sûr, cela va se faire par un tirage aléatoire.

Et c'est là que le fait d'utiliser du SVG prend tout son intérêt : comme il s'agit d'un langage XML placé dans une balise object, nous pouvons accéder à son propre arbre DOM, et donc modifier son style ou son contenu sans avoir à faire télécharger plusieurs dizaines d'images matricielles différentes :

function showValue(doc, val, col) {
	var text = doc.getElementsByTagName("text")[0];
	text.removeChild(text.firstChild);
	text.appendChild(doc.createTextNode(val));
	text.style.fill = col;
	var path = doc.getElementsByTagName("path")[0];
	path.style.fill = "#FFEBCD";
	path.style.stroke = col;
	var rect = doc.getElementsByTagName("rect")[0];
	rect.style.fill = "#20B2AA";
}

window.onload = function() {
	var values = new Array("🂡", "🂢", "🂣", "🂤", "🂥",
		"🂦", "🂧", "🂨", "🂩", "🂪", "🂫", "🂭", "🂮",
		"🃑", "🃒", "🃓", "🃔", "🃕", "🃖", "🃗", "🃘",
		"🃙", "🃚", "🃛", "🃝", "🃞", "🂱", "🂲", "🂳",
		"🂴", "🂵", "🂶", "🂷", "🂸", "🂹", "🂺", "🂻",
		"🂽", "🂾", "🃁", "🃂", "🃃", "🃄", "🃅", "🃆",
		"🃇", "🃈", "🃉", "🃊", "🃋", "🃍", "🃎");
	
	var hand = document.getElementById("hand");
	var cards = hand.getElementsByClassName("card");
	for (var i=0; i<cards.length; i++) {
		var rand = parseInt(Math.random()*values.length);
		showValue(cards[i].contentDocument, values[rand],
			(rand>values.length/2)?"#FF0000":"#OOOOOO");
		cards[i].removeChild(cards[i].firstChild);
		cards[i].appendChild(document.createTextNode(values[rand]));
	}
};

Nous utilisons ici une fonction sur l'événement « onload » de la page, car celui-ci se déclenche lorsque l'ensemble du contenu de la page a été chargé, y compris le contenu de nos différentes balises object.

Vous pouvez en tout cas constater que le style du document SVG est complètement modifiable, ce qui permet beaucoup plus de libertés qu'avec une image matricielle ; même si un SVG plus complexe comme ceux que génèrent les logiciels spécialisés pourrait être moins évident à régler correctement (Utiliser des identifiants pour les éléments à changer peut aider).

Et bien sûr, pour une véritable application, il faudrait une méthode aléatoire un peu mieux soignée, mais je vous laisse déterminer ce dont vous avez besoin en fonction de votre jeu.


Nous allons maintenant faire en sorte que cliquer sur la pile de cartes, à droite, ajoute une nouvelle carte dans notre main. Puisqu'il s'agit d'une balise <object>, nous ne pouvons pas utiliser directement son événement « onclick », car le clic de souris est accaparé par le document qu'il contient. C'est donc à cet élément qu'il va falloir s'adresser :

window.onload = function() {
	/* Laissez ici le code précédent et ajoutez ça à la suite… */
	
	var deal = document.getElementById("deal");
	var card = deal.getElementsByClassName("card")[0];
	card.contentDocument.documentElement.onclick = function() {
		var hand = document.getElementById("hand");
		var cards = hand.getElementsByClassName("card");
		if (cards.length >= 6) return; // C'est assez, non ?
		var pos = parseInt(Math.random()*cards.length);
		var card = document.createElement("object");
		card.className = "card";
		var rand = parseInt(Math.random()*values.length);
		card.appendChild(document.createTextNode(values[rand]));
		card.type = "image/svg+xml";
		card.onload = function() {
			showValue(card.contentDocument, values[rand],
				(rand>values.length/2)?"#FF0000":"#OOOOOO");
		};
		card.data = "demo.svg";
		hand.insertBefore(card, cards[pos]);
	};
};

À vous de faire en sorte, bien sûr, que le tirage aléatoire se fasse sans remise, pour simuler un véritable jeu de carte ; et plutôt que d'insérer une nouvelle carte à une position aléatoire, n'hésitez pas à les trier correctement.


Cependant, puisque notre carte est ajoutée à une position indéterminée, il peut être intéressant de la mettre un peu en valeur au moment où elle apparaît, par exemple en la surélevant comme nous le faisons pour la carte sur laquelle passe la souris.

Pour cela, nous allons revenir un instant au fichier CSS, afin de faire en sorte que l'effet de passage de souris puisse également correspondre à une classe particulière, comme ceci :

#hand .card:hover, .card.incoming {
	margin-top: -50px;
}

Puis, nous allons modifier le fichier JavaScript, pour faire en sorte que l'élément reçoive la classe CSS à sa création, et la perde un instant plus tard :

		card.className = "card incoming";
		setTimeout(function() { card.className = "card"; }, 1000);

(Rappelons que le délai d'un appel à setTimeout s'exprime en millisecondes).


Enfin, pour terminer, nous allons faire en sorte que cliquer sur une carte la joue, c'est-à-dire la retire de la main. Et puisque nous tâchons de faire des choses à peu près esthétique, nous allons faire en sorte qu'elle se déplace vers la zone de jeu avant de disparaître.

Pour cela, il nous faut déterminer quelle est la carte sur laquelle l'utilisateur à cliqué. Ce qui n'est pas aussi évident que d'habitude, car, puisque nous utilisons une balise <object> plutôt qu'une image classique, le clic n'a pas lieu dans le document habituel. C'est le revers de la médaille du SVG. Mais il existe, bien sûr, un moyen de contourner ce problème :

function playCard(ev) {
	var hand = document.getElementById("hand");
	var cards = hand.getElementsByClassName("card");
	for (var i=0; i<cards.length; i++) {
		if (cards[i].contentDocument != ev.target.ownerDocument)
			continue; // Ce n'est pas cette carte-là.
		// Insérez ici la requête AJAX requise.
		cards[i].style.position = "fixed";
		cards[i].style.marginLeft = 0;
		cards[i].style.bottom = "20px";
		cards[i].style.left = "200px";
		cards[i].style.zIndex = "99";
		movePlayedCard(cards[i]);
	}
}

function movePlayedCard(card) {
	if (card.offsetTop <= 0) {
		card.parentNode.removeChild(card);
		return;
	}
	card.style.left = (card.offsetLeft+1)+"px";
	card.style.top = (card.offsetTop-1)+"px";
	setTimeout(movePlayedCard, 1, card);
}

Pour faire fonctionner ce code, restera encore à ajouter cette ligne quelque part dans la fonction showValue (pour qu'elle s'applique à chaque carte dévoilée) :

	doc.documentElement.onclick = playCard;


Voilà pour ce petit tutoriel sans prétention. Ce qu'il y a ci-dessus est fait en vitesse, et il resterait bien sûr pas mal de choses à améliorer (toute contribution sera la bienvenue), mais cela peut commencer à être utilisable. À vous de coder le véritable jeu de cartes pour lequel ceci pourrait servir d'interface.

Au cas où, voici, en plus du SVG fourni ci-dessus, les fichiers HTML, CSS et JavaScript dûment complétés ; et pour le jeu vraiment codé par nesthib, vous pouvez aller voir par là 😉

Message 2, par NashKaR

§ Posté le 28/05/2015 à 0h 16m 08

Bonsoir,


Je suis étudiant en 2ème année d'école informatique. En cherchant un moyen de travailler avec des images de cartes au format.svg, j'ai eu la chance de tomber sur votre site. Je vous remercie pour ce tutoriel. Je me permets de vous écrire car en suivant le tuto, j'ai remarqué une petite faute au niveau de la fonction playCard(ev) avec l'instruction card.style.bottom = ""; qui ne se trouve pas dans le fichier JavaScript fourni à la fin.


Merci à vous et belle soirée.

Message 3, par Elzen

§ Posté le 03/06/2015 à 16h 25m 13

Oui, en effet, c'est une erreur de recopie ci-dessus : cette instruction n'a rien à fiche-là, puisque, d'une part, la variable « card » n'est pas définie dans la fonction ; d'autre part, la position verticale de la carte considérée est déjà modifiée par l'instruction « cards[i].style.bottom = "20px"; » un peu plus haut.

C'est dûment corrigé 😊

(Suite au décès inopiné de mon précédent serveur, je profite de mettre en place une nouvelle machine pour essayer de refaire un outil de blog digne de ce nom. J'en profiterai d'ailleurs aussi pour repasser un peu sur certains articles, qui commencent à être particulièrement datés. En attendant, le système de commentaires de ce blog n'est plus fonctionnel, et a donc été désactivé. Désolé ! Vous pouvez néanmoins me contacter si besoin par mail (« mon login at ma machine, comme les gens normaux »), ou d'ailleurs par n'importe quel autre moyen. En espérant remettre les choses en place assez vite, tout plein de datalove sur vous !)