The Statistics of War (the card game)

Photo by Jack Hamilton on Unsplash.

During Thanksgiving Break I played the card game War with my girlfriend and remembered how wonderfully mindless it is. Because there's no strategy or decision-making, it was feasible to write a program to play the game. Once I could simulate one game, I could simulate several and collect some statistics.

Evidently the rules of War have some gaps, including what to do with the cards you win. Do you place them on the bottom of your deck, in order, or do you shuffle and reuse them once your deck is empty? Why not program both methods and compare? Here is a plot of both approaches after 100,000 trials of each. The x-axis is the number of turns required to complete the game, and the y-axis is the frequency of that outcome across 100,000 trials.

The data proved more interesting than I expected.

  • Shuffling the winnings led to much faster games — a mean of 262, a mode of 84, and a max of 2,702 turns.
  • When the winnings aren't shuffled, the numbers are less clear. Many games were seemingly infinite so I added a cap of 5,000 turns per game. 11,837 (11.8%) of the games hit this limit (and do not appear on the graph). But plenty of games ended after 4,000+ turns, so we could expect many of these long games to be finite as well. If we ignore the capped games, the mode is 104 turns.
  • Both plots show a binary distribution of the results, as you can see in the higher and lower groupings of dots. It turns out that games are more likely to finish after an even number of turns. Each player starts with 26 cards, so if there are no wars and one player wins every hand, the game will last 26 turns. For every turn that player loses, they must win an additional hand to cancel the loss, so the game still requires an even number of turns. Only a war will break this trend because 4 cards change possession instead of 1. A game with an odd number of wars will take an odd number of turns to complete (ignoring wars with fewer than 3 cards, when one player's deck is low).

As for the program, here it is in JavaScript. The highlighted lines show the changes made between the Shuffle and No-Shuffle versions.

(function() {
	var NUM_TRIALS = 100000, // # games to simulate
		MAX_TURNS = 5000; // maximum # of turns to avoid infinite loops

	// From http://stackoverflow.com/a/2450976
	function shuffle(array) {
		var currentIndex = array.length,
			temporaryValue,
			randomIndex;

		// While there remain elements to shuffle...
		while (0 !== currentIndex) {
			// Pick a remaining element...
			randomIndex = Math.floor(Math.random() * currentIndex);
			currentIndex -= 1;
			// And swap it with the current element.
			temporaryValue = array[currentIndex];
			array[currentIndex] = array[randomIndex];
			array[randomIndex] = temporaryValue;
		}
		return array;
	}

	function Card(o) {
		var self = this;
		self.suit = o.suit;
		self.value = o.value;

		// Generate a string representation of the card
		self.toString = function() {
			var displayValue = self.value;
			switch (self.value) {
				case 11:
					displayValue = 'J';
					break;
				case 12:
					displayValue = 'Q';
					break;
				case 13:
					displayValue = 'K';
					break;
				case 14:
					displayValue = 'A';
					break;
			}
			return self.suit + displayValue;
		};

		// Return a negative number if compare is higher than this card.
		// Return a positive number if this card is higher than compare.
		// Return zero if the cards are equal.
		self.compare = function(/*Card*/ compare) {
			return self.value - compare.value;
		};

		return self;
	}

	function Player(o) {
		var self = this,
			hand = [],
			discard = [];

		self.name = o.name || 'Player';

		// Draw one card from the player's hand
		self.draw = function() {
			if (hand.length === 0) {
				// Version 1: The discard pile is shuffled before using				//hand = shuffle(discard).concat(hand);				// Version 2: The discard pile is used in the order that it was created				hand = discard.concat(hand);				discard = [];
			}
			return hand.pop();
		};

		// Add one or more cards to the player's discard pile
		self.discard = function(cards) {
			if (Array.isArray(cards)) {
				discard = cards.concat(discard);
			} else {
				discard.push(cards);
			}
		};

		// Count the total number of cards in this player's possession
		self.numCards = function() {
			return hand.length + discard.length;
		};

		// Does the user have any cards remaining?
		self.hasCards = function() {
			return self.numCards() > 0;
		};

		// Generate a string representation of the player
		self.toString = function() {
			return self.name + ' (' + self.numCards() + ' cards)';
		};

		return self;
	}

	function Game(o) {
		var self = this,
			deck = shuffle(o.deck);

		// The number of turns that have elapsed in this game
		self.numTurns = 0;

		// Simulate a single game
		self.play = function() {
			var player1 = new Player({ name: 'John' }),
				player2 = new Player({ name: 'Jane' });

			// Split the deck between the two players
			var iDeck;
			for (iDeck = 0; iDeck < deck.length; iDeck++) {
				if (iDeck % 2) {
					player1.discard(deck[iDeck]);
				} else {
					player2.discard(deck[iDeck]);
				}
			}

			var player1Card,
				player2Card,
				cardCompare,
				winnings,
				isWar,
				numWarCards,
				iWarCards;

			while (player1.hasCards() && player2.hasCards()) {
				self.numTurns++;

				// End this game if it's taking too long, to avoid potentially infinite loops
				if (self.numTurns > MAX_TURNS) {
					return;
				}

				player1Card = player1.draw();
				player2Card = player2.draw();
				winnings = [player1Card, player2Card];

				cardCompare = player1Card.compare(player2Card);

				if (cardCompare > 0) {
					// Player 1 wins this turn
					player1.discard(winnings);
				} else if (cardCompare < 0) {
					// Player 2 wins this turn
					player2.discard(winnings);
				} else {
					// War!
					do {
						// If a player is completely out of cards now, they lose the game
						if (!player1.hasCards()) {
							// Player 2 wins this game
							isWar = false;
							player2.discard(winnings);
						} else if (!player2.hasCards()) {
							// Player 1 wins this game
							isWar = false;
							player1.discard(winnings);
						} else {
							isWar = true;

							// If either player doesn't have enough cards for war, bet what they have
							numWarCards = Math.min(
								player1.numCards() - 1,
								player2.numCards() - 1,
								3,
							);

							for (iWarCards = 0; iWarCards < numWarCards; iWarCards++) {
								winnings.push(player1.draw());
								winnings.push(player2.draw());
							}

							player1Card = player1.draw();
							player2Card = player2.draw();
							winnings.push(player1Card);
							winnings.push(player2Card);

							cardCompare = player1Card.compare(player2Card);

							if (cardCompare > 0) {
								// Player 1 wins the war and this turn
								isWar = false;
								player1.discard(winnings);
							} else if (cardCompare < 0) {
								// Player 2 wins the war and this turn
								isWar = false;
								player2.discard(winnings);
							} else {
								// The card values are equal again, continue the war
							}
						}
					} while (isWar);
				}
			}

			// Note: this is erroneous if MAX_TURNS was reached
			self.winner = player1.hasCards() ? player1 : player2;
		};

		return self;
	}

	// Generate the master deck of all 52 cards.
	// It will be reused and shuffled in each Game instance.
	var suits = ['&spadesuit;', '&clubsuit;', '&heartsuit;', '&diamondsuit;'],
		deck = [];

	var iSuit, iValue;
	for (iSuit = 0; iSuit < suits.length; iSuit++) {
		// J = 11, Q = 12, K = 13, A = 14
		for (iValue = 2; iValue <= 14; iValue++) {
			deck.push(new Card({ suit: suits[iSuit], value: iValue }));
		}
	}

	// Run the desired number of trials.
	// Populate wins with the number of turns and frequency of that outcome.
	var iTrials,
		thisGame,
		wins = [];
	for (iTrials = 0; iTrials < NUM_TRIALS; iTrials++) {
		thisGame = new Game({ deck: deck });
		thisGame.play();

		if (!wins[thisGame.numTurns]) {
			wins[thisGame.numTurns] = 1;
		} else {
			wins[thisGame.numTurns]++;
		}
	}
})();

To close, enjoy the amazing uniqueness of a well-shuffled deck.

Published under  on .

Drew

Drew

Hi! I'm Drew, the Wimpy Programmer. I'm a software developer and formerly a Windows server administrator. I use this blog to share my mistakes and ideas.

Comments on "The Statistics of War (the card game)"

Avatar for BenjaminBenjamin

I know this is an older post but I just wanted to say thank you! I was assigned the task of creating a program that could simulate a game of war and I designed it to not shuffle. Seeing the statistics laid out like this made it much easier to determine if my game lengths were falling in line with what you may expect from a typical game. The frequency of an endless game being played threw me off and I honestly thought there was something wrong with my code until I took a look at your data. Is the act of shuffling a common rule? Where I live everyone I have ever played with typically adds their winnings to the bottom of their pile as we go along. Thanks again!

Avatar for DrewDrew

Thanks Benjamin! I'm glad this old post could help you.

When I learned to play War, we shuffled. I think this adds excitement to the game. My fiancée learned to play without shuffling, and so perhaps it's no coincidence that she considers War a waste of time. The Wikipedia article seems to confirm that the rules are hazy on this point.

In any event, this simulation was eye-opening for me and I'd definitely recommend shuffling to anyone who wants a quicker game. But then again, I remember as a kid playing War with my brothers during neverending car trips. Stretching out every game by not shuffling might be a better way to pass the time.

Avatar for Don HerlofskyDon Herlofsky

My 8 year old grandson beat me in two hands. We did shuffle after the first round. And also left the jokers in the deck. Can't imagine the odds of anyone doing that! Unbelievable!