Nu kan du spille kortspillet Krig – online!

https://wallnot.dk/krig/ har jeg netop offentliggjort årets julespil nummer 1: Krig!

Ej, jeg havde allerede prøvet at simulere Krig, men synes det kunne være spændende at få logikkerne til at hænge sammen med interaktivitet (dog højst begrænset) og logikker for rent faktisk at vise spillet.

Nu har jeg gjort et forsøg og jeg har kommenteret en masse i koden, så den forhåbentlig er nem at følge med i.

Her er views.py fra Django:

from django.shortcuts import render
import random			# Used to shuffle decks
import base64			# Used for obfuscation and deobfuscation functions
from math import ceil 	# Used to round up

# Create decks function - not a view
def new_deck(context):
	# Create card values and list of cards in each colour
	card_values = range(2,15)
	spades = [str(i) + "S" for i in card_values]
	clubs = [str(i) + "C" for i in card_values]
	diamonds = [str(i) + "D" for i in card_values]
	hearts = [str(i) + "H" for i in card_values]
	# Combine colours to deck
	deck = spades + clubs + diamonds + hearts
	# Shuffle deck		
	random.shuffle(deck)
	# Divide deck between two players and convert to commaseparated string
	player_a_deck = ",".join(deck[0:26])
	player_b_deck = ",".join(deck[26:52])
	# Obfuscate decks to make cheating marginally harder using the obfuscate function
	# production variable toggles this behavior because it's very time consuming to debug
	# if obfuscation is on
	production = True
	if production == True:
		player_a_deck = obfuscate(player_a_deck)
		player_b_deck = obfuscate(player_b_deck)
	# Add the two decks to context
	context['player_a_deck_form'] = player_a_deck
	context['player_b_deck_form'] = player_b_deck
	# Set index to 0 to only turn one card for first round of game
	context['index'] = 0
	return context

# Obfuscate by converting to base64 encoding - not a view
def obfuscate(deck):
	return base64.b64encode(deck.encode()).decode()

# Deobfuscate by converting from base64 encoding to string - not a view
def deobfuscate(deck):
	return base64.b64decode(deck.encode()).decode()

# Logic to create a list of which cards should be hidden or shown to player - not a view
def show_hide_cards(cards_on_table, index):
	counter = 0
	cards_on_table_show_hide = []
	for card in cards_on_table:
		# First card should always be shown
		if counter == 0:
			cards_on_table_show_hide.append([card, True])
		# If the card number is divisible by 4 it is the turn card in a war
		elif counter % 4 == 0:
			cards_on_table_show_hide.append([card, True])
		# If the card number equals the index value, one or both players does not
		# have enough cards for a full war so the last card should be turned
		elif counter == index:
			cards_on_table_show_hide.append([card, True])
		else:
			cards_on_table_show_hide.append([card, False])
		counter += 1
	return cards_on_table_show_hide

# Page view
def index(request):
	# Empty context variable to add to
	context = {}
	# Production variable to toggle obfuscation
	production = True
	# First visit, game has not been started
	if not request.method == 'POST':
		# Create a deck using the new_deck function
		new_deck(context)
	# Game has started
	else:
		### GAME PREPARATION AND CARD DISPLAY LOGIC ###
		# Current game status is used in template to know whether game has been
		# started or not, or has ended
		game_status = "Going on"
		
		# Get submitted decks from user submitted POST request
		player_a_deck = request.POST.get('player_a_deck')
		player_b_deck = request.POST.get('player_b_deck')
		
		# Deobfuscate submitted decks using the deobfuscate function
		if production == True:
			player_a_deck = deobfuscate(player_a_deck)
			player_b_deck = deobfuscate(player_b_deck)
		
		# Convert decks to lists
		player_a_deck = player_a_deck.split(",")
		player_b_deck = player_b_deck.split(",")

		# Get submitted index value in order to know which cards to compare
		# The index is used in case of war to determine which cards to compare
		# and what cards to show to player
		index = int(request.POST.get('index'))
		context['current_index'] = index
		
		# In order to display cards in correct order in case of war for player_b
		# a number of slices are prepared and added to context as strings in a list.
		# number_of_slices is rounded up in case index is not divisible by 4 (endgame logic)
		number_of_slices = ceil(index/4)	
		slices = []
		# Only needed if number of slices is above 0
		if number_of_slices:
			start = 1
			end = 5
			for slice in range(number_of_slices):
				slices.append(str(start)+":"+str(end))
				start +=4
				end += 4
		context['slices'] = slices
		
		# In order to display cards to player using a loop, the deck is sliced
		# by the index value plus 1. # If index is 0, 1 card should be shown.
		# If index is 4 because of war, 5 cards should be shown... and so on.
		a_cards_on_table = player_a_deck[:index+1]
		b_cards_on_table = player_b_deck[:index+1]
		
		# Cards on table is run through function to decide which cards to show face up/face down
		# to player and added to context.
		context['a_cards_on_table'] = show_hide_cards(a_cards_on_table, index)
		context['b_cards_on_table'] = show_hide_cards(b_cards_on_table, index)
		
		# Length of cards "on the table" is calculated in order to calculate remaining cards in player decks.
		# The value for player a is shown to the players and is also used for template card display logic.
		a_cards_on_table_length = len(a_cards_on_table)
		b_cards_on_table_length = len(b_cards_on_table)
				
		# Calculate number of cards in decks
		a_number_of_cards = len(player_a_deck)
		b_number_of_cards = len(player_b_deck)

		# Add remaining cards in deck to context to show to players
		a_remaining_in_deck = a_number_of_cards - a_cards_on_table_length
		b_remaining_in_deck = b_number_of_cards - b_cards_on_table_length
		context['a_remaining_in_deck'] = a_remaining_in_deck
		context['b_remaining_in_deck'] = b_remaining_in_deck
		
		### GAME LOGIC ###
		# Check if both players have decks large enough to compare
		if a_number_of_cards > index and b_number_of_cards > index:
			# Convert first card in decks to integer value in order to compare
			player_a_card = int(player_a_deck[index][:len(player_a_deck[index])-1])
			player_b_card = int(player_b_deck[index][:len(player_b_deck[index])-1])

			# Player a has the largest card
			if player_a_card > player_b_card:
				# Add cards in play to end of player a deck and delete them from beginning
				# of player a and player b decks
				player_a_deck.extend(player_a_deck[:index+1])
				player_a_deck.extend(player_b_deck[:index+1])	
				del player_a_deck[:index+1]
				del player_b_deck[:index+1]
				# If a play is decided, index is set to 0
				index = 0
				context['message'] = "Du vandt runden!"
			# Player b has the largest card
			elif player_a_card < player_b_card:
				# Cards are added to deck in different order from player a to deck in order
				# to avoid game risk of going on forever
				player_b_deck.extend(player_b_deck[:index+1])	
				player_b_deck.extend(player_a_deck[:index+1])
				del player_a_deck[:index+1]
				del player_b_deck[:index+1]
				# If a play is decided, index is set to 0
				index = 0
				context['message'] = "Du tabte runden!"
			# Cards must be equal and war is on
			else:
				# In case of war normally four cards are added to the index, but
				# In order to accomodate a case of end-game war, there are special cases
				# if either player doesn't quite have enough cards for a full 4-card-turn war
				if a_number_of_cards >= index + 4 <= b_number_of_cards:
					index += 4
				# Since the if statement two levels up already checks that number of cards is larger
				# than the index value, an else with no criteria is enough to decide how many cards
				# each player has left to turn and add the smallest number to the index
				else:
					# Calculate the difference between number of cards and index for each player.
					# The smallest of the two differences is added to index to decide how many cards to use for war.
					# One is subtracted for the card already on the table
					a_difference = a_number_of_cards - index
					b_difference = b_number_of_cards - index
					index += min(a_difference, b_difference) - 1
					# Edge case: If war on last remaining card for either player, 1 is added to index to end the game
					# by getting the index above the number of cards in the deck of the player(s) with no cards left
					if a_remaining_in_deck == 0 or b_remaining_in_deck == 0:
						index += 1
				# Messages are different for single, double, trippel wars and anything above.
				# Since the index can be upped by less than four, less than or equal is used to
				# decide which kind of war is on.
				if index <= 4:
					context['message'] = "Krig!"
				elif index <= 8:
					context['message'] = "Dobbeltkrig!"
				elif index <= 12:
					context['message'] = "Trippelkrig!"
				else:
					context['message'] = "Multikrig!"
		
		### AFTER GAME LOGIC AND DECIDE GAME LOGIC ###
		# Calculate length of decks after game logic has run
		player_a_deck_length = len(player_a_deck)
		player_b_deck_length = len(player_b_deck)
		
		# Compare lengths of decks to decide if someone has won. The number of cards on table for
		# next turn of cards is always at least one more than the index (index 0, 1 card, index 4,
		# 5 cards). There are three possible outcomes:
		# 1) Equal game: Both players are unable to turn and have equal sized decks (very, very rare!)
		# 2) Player a is unable to play and has a smaller deck than b (if both players are unable to turn, largest deck wins)
		# 3) Same as 2) for player b
		if player_a_deck_length <= index and player_b_deck_length <= index and player_a_deck_length == player_b_deck_length:
			context['message'] = "Spillet blev uafgjort. Hvor tit sker det lige?"
			game_status = "Over"
		elif player_a_deck_length <= index and player_a_deck_length < player_b_deck_length:
			context['message'] = "Du tabte spillet!"
			game_status = "Over"			
		elif player_b_deck_length <= index and player_b_deck_length < player_a_deck_length:
			context['message'] = "Du vandt spillet!"	
			game_status = "Over"			

		# Add size of decks after play to context to decide whether to show decks to player
		context['after_deck_a'] = player_a_deck
		context['after_deck_b'] = player_b_deck
		
		# Add game status to context
		context['game_status'] = game_status
		
		# Convert decks back to strings
		player_a_deck = ",".join(player_a_deck)
		player_b_deck = ",".join(player_b_deck)
		
		# Obfuscate decks using obfuscate function
		if production == True:		
			player_a_deck = obfuscate(player_a_deck)
			player_b_deck = obfuscate(player_b_deck)
		
		# Context for form
		context['player_a_deck_form'] = player_a_deck
		context['player_b_deck_form'] = player_b_deck
		context['index'] = index
		
		# If game is over, create a new deck to add to form for new game
		if game_status == "Over":
			new_deck(context)
	return render(request, 'krig/index.html', context)

Og her er skabelonen index.html:

{% load static %}
{% spaceless %}
<!doctype html>
<html lang="da">
	<head>
		<title>Krig!</title>
		<meta name="description" content="Spil det populære, vanedannende kortspil krig mod computeren - online!">
		<meta charset="utf-8">
		<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
		<link rel="stylesheet" href="{% static "krig/style.css" %}">
		<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
		<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png">
		<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
		<link rel="manifest" href="/site.webmanifest">
		<link rel="mask-icon" href="/safari-pinned-tab.svg" color="#5bbad5">
		<meta name="msapplication-TileColor" content="#ffc40d">
		<meta name="theme-color" content="#ffffff">
		{% comment %}Most of stylesheet is loaded externally, but logic to size images in case of war is kept in template{% endcomment %}
		{% if current_index > 0 %}
		<style>
			img {
				width: 22%;
				display: inline;
			}
		</style>
		{% endif %}
	</head>
	<body>
		<h1>Krig</h1>
		
		{% comment %}Status message of current round or game is displayed{% endcomment %}
		<p class="status">
			{{ message }}
		</p>
		
		{% comment %}Page is divided in two-column grid. Each column is aligned towards vertical center of page{% endcomment %}
		<div class="grid">
			{% comment %}Player a ("You") column{% endcomment %}
			<div class="item text-right">
				<p>Dig</p>

				{% comment %}If any cards are left to turn, show number, if no cards are left, write no cards left{% endcomment %}
				<p class="cardsleft">
					{% if a_remaining_in_deck > 0 %}
						{{ a_remaining_in_deck }} kort tilbage i bunken
					{% elif a_remaining_in_deck == 0 %}
						Ingen kort tilbage!
					{% endif %}
				</p>

				{% comment %}Back of card (deck) is shown if cards are left in deck or game has not begun{% endcomment %}
				{% if a_remaining_in_deck > 0 or not game_status %}
					<img src="{% static 'krig/back_r.svg' %}">
				{% endif %}

				{% comment %}Loop to show player's turned cards.{% endcomment %}
				{% for card in a_cards_on_table %}
					{% if card.1 == True %}
						<img src="{% static 'krig/'|add:card.0|add:'.svg' %}"><br>
					{% else %}
						<img src="{% static 'krig/back_r.svg' %}">
					{% endif %}
				{% endfor %}
			</div>

			{% comment %}Player b ("Computer") column{% endcomment %}
			<div class="item text-left">
				<p>Computeren</p>
				{% comment %}If any cards are left to turn, show number, if no cards are left, write no cards left{% endcomment %}
				<p class="cardsleft">
					{% if b_remaining_in_deck > 0 %}
						{{ b_remaining_in_deck }} kort tilbage i bunken
					{% elif b_remaining_in_deck == 0 %}
						Ingen kort tilbage!
					{% endif %}
				</p>

				{% comment %}
					The order of the deck and the first turned card is different for player b who plays on the right side.
					Therefore if there is a first card in player b's cards on table that card is shown.
				{% endcomment %}
				{% if b_cards_on_table.0 %}
					<img src="{% static 'krig/'|add:b_cards_on_table.0.0|add:'.svg' %}">
				{% endif %}

				{% comment %}If b has cards left in deck or game has not started, show back of deck{% endcomment %}
				{% if b_remaining_in_deck > 0 or not game_status %}
						<img src="{% static 'krig/back_r.svg' %}">
				{% endif %}
				<br>
				
				{% comment %}
					Due to the order of player b's shown cards being different than for player a, this loop to show cards
					in case of war is a little different from player a's.
					The slices variable contains pairs of values saved as strings that the Django template filter |slice can
					understand, e.g. "1:5". These are looped through so that only parts of b_cards_on_table corresponding to
					the slice is looped through for each single, double, etc. war. The loop through b_cards_on_table is reversed
					because the card being turned is shown left of the hidden cards in the war.
				{% endcomment %}
				{% for slice_cut in slices %}
					{% for card in b_cards_on_table|slice:slice_cut reversed %}
						{% if card.1 == True %}
							<img src="{% static 'krig/'|add:card.0|add:'.svg' %}">
						{% else %}
							<img src="{% static 'krig/back_r.svg' %}">
						{% endif %}
					{% endfor %}<br>
				{% endfor %}
			</div>
		</div>

		{% comment %}
			This form is used for user input with the text in the button depending on whether user is on:
			1) Starting page: User can start a game
			2) In an ongoing game: User can turn next card
			3) In a game that has ended: User can start a new game
		{% endcomment %}
		<form class="next" action="{% url 'krig_index' %}" method="post">
			{% csrf_token %}
			<input name="player_a_deck" type="hidden" value="{{ player_a_deck_form }}">
			<input name="player_b_deck" type="hidden" value="{{ player_b_deck_form }}">
			<input name="index" type="hidden" value="{{ index }}">
			<button type="submit">{% if not game_status %}Start spillet{% elif game_status == "Going on" %}Vend næste kort{% elif game_status == "Over" %}Start nyt spil{% endif %}</button>
		</form>
	</body>
</html>
{% endspaceless %}

God fornøjelse!