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!

Avatar for Apt6NApt6N

Drew - thank you for this post. My roommate and I were having a heated discussion on the game of war. So do you think a game can go on forever without a winner?

Avatar for BladeBlade

My son and I had a game today that went only one round. My son did not win a single time. we had 2 "double wars". We play that on a match card you each lay 3 cards down and flip up on the forth. What are the odds of a single round game???? Never seen that happen before.

Avatar for RayRay

Glad to see other people arriving at the same question. Did you run the program with one side shuffling and the other not?

Avatar for SamSam

This is great! I also made a war simulation without knowing this existed! github.com/sambecker/rustwar I consistently averaged 268 turns with shuffling and 580 turns without shuffling (excluding 6% or so of games that never finish). Would love to compare our approaches, see where differences/edge cases might exist!

Avatar for ClintClint

My wife, son, and I were playing 3-way War last night. We got a 3 way war (very rare), laid down our three face down cards, and then all matched again for a DOUBLE 3-Way War.

I have NEVER seen this in probably thousands of games over my nearly 40 years of playing the game. Is there any way to calculate the odds of such an event? We use a double deck, but the odds still have to be in the thousands, right?

Avatar for MirandaMiranda

I was looking for the odds of having a triple war. My daughter and I just had one and I can never remember ever having a triple war. Miranda

Avatar for WhenYouNeedHelpWhenYouNeedHelp

For the sake of continuing knowledge, I wanted to post about my results with this problem.

Funnily enough, I was also playing a round of war with my girlfriend when I had the idea to simulate the game and find the average number of turns.

I managed to simulate 1 Million games (with random shuffling) and got the following results:

Mode: 86
Average: 260
Minimum: 5
Maximum: 3548

I'm guessing these differences in numbers (Mode and Average) boil down to our handling of some special situations, since anything passed 10,000 simulations tended to have the same average / mode.

Avatar for MarkMark

Stumbled upon this as I just had the same curiosity about the game of war. My intuition tells me this implementation must have a subtle bug somewhere if there are "infinite" turn games. I did run this a few times, and it happens too frequently. After all - how many real games of WAR have you played that never ended? Sure, some shuffle permutations will lead to long games, but infinite?

JS is not my favored language, but I'll take a look around and see if anything pops out as a problem.

FWIW, my own implementation that I wrote prior to finding this post ran ~24M simulations last night and the longest game was 1531 turns w/ 232 wars.

Avatar for MarkMark

Follow-up. I spent some time on this, and it appears that the discard pile can enter an infinite oscillation between two different states, which means the game state itself must be (as you mention) in an infinite loop. This does not appear to be normal game play. Example (Player.discard was modified to print the state of the discard pile if hand.length===0), which shows the discard pile for Jane enter the oscillation:

discard[Jane] 2K9J7A7A2106107A3Q7J28310688Q495K6J4Q6K39294J
discard[Jane] 4J59694K3Q7J2K396Q78710286J7Q5A4103102A9A5J5K
discard[Jane] 2K6J7A895A4102103A6Q7J78510684Q397K6J3Q2K49498J
discard[Jane] 2J49694K3Q7J6K797Q58510482J3Q2A6107108A392A5J5K
discard[Jane] 2K6J7A895A4102103A6Q7J78510684Q397K4J8Q2K49693J
discard[Jane] 2J49694K3Q7J6K797Q58510482J3Q2A6107108A392A5J5K
discard[Jane] 2K6J7A895A4102103A6Q7J78510684Q397K6J3Q2K49498J
discard[Jane] 2J49694K3Q7J6K797Q58510482J3Q2A6107108A392A5J5K
discard[Jane] 2K6J7A895A4102103A6Q7J78510684Q397K4J8Q2K49693J
discard[Jane] 2J49694K3Q7J6K797Q58510482J3Q2A6107108A392A5J5K
discard[Jane] 2K6J7A895A4102103A6Q7J78510684Q397K6J3Q2K49498J
....

I modified Player.discard() to remove the discard pile from the game state, using only hand to track a player's card state:

self.discard = function(cards) {
    if (Array.isArray(cards)) {
        hand = hand.concat(cards);
    } else {
        hand = [cards].concat(hand);
    }
}

With this modification the oscillation is gone, and game play proceeds without infinite loops, and is actually pretty close to what I observe with my own implementation.

One other note, closely inspect the order in which cards are added and removed from the hand and discard piles. Think about holding a deck of cards, for example, and the order that cards would be added to piles in a game. In the original code, when a deck is being delt, Player.discard places the new card at the bottom of the deck via discard.push(), but when a player receives cards in game play, the function adds them to the top of the deck. While these don't impact the running of a simulation, is doesn't model how cards are typically handled. Also, Player.draw() takes a card from the bottom of the deck via hand.pop() rather than from the traditional top via hand.shift().

It's great to see others having interest in the same mental exercise.

I'll leave it to a future reader to isolate where the discard pile oscillation is originating, and will bookmark the page to occasionally check in.

Take care all.

-Mark

Avatar for RadomirRadomir

I just played war with my 6y old son, and I was surprised by how long it took. It didn't seem so long when I played it as a kid. And it made me think about how long it usually takes actually properly to finish a game of war. So your results are really helpful :)

About the shuffle / no shuffle - we shuffle, mainly because it's pretty hard to keep track of the order in case of actual war when you have some cards facing down, and some facing up. And we do not add the won cards to the bottom of the deck right away. We put them in separate piles and use them when a hand is empty. It does not really make much of a difference unless you decide to shuffle the whole pile before making it your new hand.

Anyways - it's fun to play with children. And once again - thanks for your research :)

Avatar for LosingAtWarLosingAtWar

My daughter is kicking my ass right now