Simon Lang:
Web Programmer

Code Samples

Back to Top Javascript (With jQuery and Data Templates)

/**
 * offlineCards
 *
 * This object provides a namespace for all "offline game" functionality.
 *  The code below duplicates the game rules originally implemented in PHP
 *  in the playActions controller
 *
 * @author		 Simon Lang
 */
var offlineCards = {

    deckSize: 52,
		handSize: 26, // This number is updated dynamically anyway...
		numberOfPlayers: 4,
		currentPlayerIndex: 0,
		currentRound: 0,
		selectedStatisticIndex: null,
		teamNames: ['Player 1', 'Player 2', 'Player 3', 'Player 4'],
		hands: [],
		scores: [],

		restart: function() {
			// Update round
			this.currentRound = 0;
			$('.round').html(1);
		
			// Set number of teams
			this.numberOfPlayers = parseInt($('#numberOfOpponents').val()) + 1;	// From common settings form.

			// Name Teams
			var fields = $('#offlineSettingsForm form').find('input[name*=team]').serializeArray()
			for (i in fields) {
				var teamName = fields[i].value;
				if (!teamName) {
					var playerNumber = parseInt(i) + 1;
					teamName = 'Player '+playerNumber;
				}
				this.teamNames[i] = teamName;
			}

			// Hide a bunch of panes
			$('.showHands,.highScorePane,.currentCardPane').html('');
			$('.gameMessage,#blackOverlay,#offlineSettingsForm,.gameOverPane').fadeOut();

			// Shuffle, Deal, Update Scoreboard, Display first card
			if ($('.previousOfflineCard').length == 0) {
				// This is here so the "view cards" screen is in order. Messy code. Remove when done testing.
				this.shuffle();
			}
			this.deal().updateScoreboard().popCard()
			
			$('.gameplayArea').fadeIn();
		},

		getCurrentHand: function() {
			return this.hands[this.currentPlayerIndex];
		},

		shuffle: function() {
			cards = array_shuffle(cards);
			return this;
		},
		
		deal: function() {
			for (i = 0; i < this.numberOfPlayers; i++) {
				this.hands[i] = [];
				this.scores[i] = 0;
			}
			this.handSize = Math.floor(this.deckSize / this.numberOfPlayers);
			var cardIndex = 0;
			for (i = 0; i < this.handSize; i++) {
				for (j = 0; j < this.numberOfPlayers; j++) {
					var card = cards[cardIndex];
					card.cardIndex = cardIndex;
					this.hands[j].push(card);
					cardIndex++;
				}
			}
			$('.totalRounds').html(this.handSize);
			return this;
		},

    popCard: function() {
			var hand = this.getCurrentHand();
			var card = hand[this.currentRound];
			$('.currentCard').html($('#cardTemplate').render(card)).show();
			$('#tooltip').remove();	// bugfix. Sometimes gets stuck because of no mouseout event presumably
		},

		loadCard: function(card_name, allowStatSelect) {
			var card = cards[card_name];
			card.allowStatSelect = allowStatSelect;
			$(".currentCard").html($("#cardTemplate").render(card)).show();
			if (this.selectedStatisticIndex && !allowStatSelect) {
				$('.playStats tr[rel='+this.selectedStatisticIndex+']').addClass('selectedStatistic');
			}
			//$('#tooltip').remove();	// needed here?
		},

		nextRound: function(selectedStatisticIndex) {
			this.selectedStatisticIndex = selectedStatisticIndex;

			var playedCards = [];

			var winningPlayerIndex = 0;
			var winningCard = this.hands[winningPlayerIndex][this.currentRound];
			
			// The first card is usually the winningCard. Except when it's an anti_trump
			while (winningCard.is_anti_trump && winningPlayerIndex < this.numberOfPlayers) {
				// They lose money
				//var billionsLost = (winningCard.name == 'Resources') ? 100 : 50;		// messes up syntax highlighter
				this.scores[winningPlayerIndex] -= ONE_BILLION * billionsLost;
				
				// Try the next card
				winningPlayerIndex++;
				winningCard = this.hands[winningPlayerIndex][this.currentRound];
			}

			// Check for Equal Draw
			var statValues = [];
			var trumpInHand = false;
			var isDraw = false;
			var highWins;
			for (i = 0; i < this.numberOfPlayers; i++) {
				var card = this.hands[i][this.currentRound];
				if (card.is_trump) {
					trumpInHand = true;
				}
				if (typeof(card.PlayStats[this.selectedStatisticIndex]) != "undefined") {
					highWins = card.PlayStats[this.selectedStatisticIndex].high_wins;
					statValues.push(card.PlayStats[this.selectedStatisticIndex].value);
				}
			}
			statValues.sort(function(a,b){return a - b});
			if (!highWins) {
				statValues.reverse();
			}
			if (!trumpInHand) {
				if (statValues[statValues.length - 1] == statValues[statValues.length - 2]) {
					isDraw = true;
				}
			}

			// Check for best card
			for (i = 0; i < this.numberOfPlayers; i++) {
				var card = this.hands[i][this.currentRound];

				// For the .showHands area
				var playedCard = {
					teamName: this.teamNames[i],
					cardName: card.name,
					image: card.image,
					statValue: "",
					cardIndex: card.cardIndex
				};
				
				// Trumps win by default
				if (card.is_trump) {
					winningCard = card;
					winningPlayerIndex = i;
					playedCards.push(playedCard);
					continue;
				}

				// Anti_Trumps lose by default
				if (card.is_anti_trump) {
					// They lose money
					//var billionsLost = (card.name == 'Resources') ? 100 : 50;		// messes up syntax highlighter
					this.scores[i] -= ONE_BILLION * billionsLost;
					
					playedCards.push(playedCard);
					continue;
				}

				// Ensure we are looking at a valid Statistic
				var cardStat = card.PlayStats[this.selectedStatisticIndex];
				var winningCardStat = winningCard.PlayStats[this.selectedStatisticIndex];
				if (typeof(cardStat) !== "undefined") {
					playedCard.statValue = cardStat.short_display_value;

					// Ensure we are looking at a valid Statistic
					if (typeof(winningCardStat) !== "undefined") {
						// High Wins
						if (cardStat.high_wins && parseFloat(cardStat.value) > parseFloat(winningCardStat.value)) {
							winningCard = card;
							winningPlayerIndex = i;
						}
						// Low Wins
						if (!cardStat.high_wins && parseFloat(cardStat.value) < parseFloat(winningCardStat.value)) {
							winningCard = card;
							winningPlayerIndex = i;
						}
					}
				}
				
				playedCards.push(playedCard);
			}

			if (isDraw) {
				// Announce draw. No points awarded.
				$(".gameMessage").html("Draw!").show();
			}
			else {
				// Award points and announce winner
				var billionsWon = this.numberOfPlayers;
				if (winningCard.is_trump) {
					//billionsWon = (winningCard.name == 'World Peace') ? 300 : 200;	// messes up syntax highlighter
				}
				this.scores[winningPlayerIndex] += ONE_BILLION * billionsWon;
				this.updateScoreboard();
				$(".gameMessage").html(this.teamNames[winningPlayerIndex]+" wins with card: "+winningCard.name).show();
			}

			this.currentPlayerIndex = winningPlayerIndex;
	
			// Display played cards
			$('.showHands').html($('#showHandsTemplate').render(playedCards));
			
			// Advance round
			this.currentRound++;
			$(".round").html(this.currentRound + 1);

			// Check for Game Over
			if (this.currentRound >= this.handSize) {
				this.gameOver();
				return;
			}
			
			// "It's your turn" prompt area
			var winningTeamName = this.teamNames[winningPlayerIndex];
			var nextCard = this.hands[winningPlayerIndex][this.currentRound];
			$('.currentCardPane').html($('#currentCardPaneTemplate').render({ 
				teamName: winningTeamName,
				cardIndex: nextCard.cardIndex,
				cardName: nextCard.name,
				cardImage: nextCard.image
			}));

			// Display next card for the winning player
			this.popCard();

			$('.gameplayArea').show();
		},
	
		gameOver: function() {
			var topPlayerIndex = 0;
			for (i = 0; i < this.numberOfPlayers; i++) {
				if (this.scores[i] > this.scores[topPlayerIndex]) {
					topPlayerIndex = i;
				}
			}
			$('.playerThatWon').html(this.teamNames[topPlayerIndex]);
			$('.gameplayArea,.currentCard').fadeOut(function() {
				$('.gameOverPane').fadeIn();
			});

			// Submit HighScores
			var request = {};
			for (i = 0; i < this.numberOfPlayers; i++) {
				request[this.teamNames[i]] = this.scores[i];
			}
			$.post('/offline/enterHighScore', request, function(d) {
				$('.highScorePane').load('/play/highScores');
			});
		},
		
		updateScoreboard: function() {
			var scores = [];
			for (i = 0; i < this.numberOfPlayers; i++) {
				var teamName = this.teamNames[i];
				var score = '$'+number_format(this.scores[i]);
				scores.push({
					teamName: teamName,
					score: score
				});
			}
			$('#offlineScoreboard').html($('#scoreboardTemplate').render(scores));
			return this;
		},

		// Debugging
		logHands: function() {
			console.log(this.hands);
		},

		// Debugging: Produces a clickable list of all cards
		createDebugCardList: function() {
			for (i in cards) {
				$('.output').append('<a href="" class="offlineCardLink" rel="'+i+'">'+cards[i].name+'</a>');
			}
    }

};

$(function() {

	// Start game
	offlineCards.restart();

	$('.offlineSelectStatistic').live('click', function(e) {
		e.preventDefault();
		offlineCards.nextRound($(this).attr('rel'));
	});

	$('.offlineCardLink').live('click', function(e) {
		e.preventDefault();
		offlineCards.loadCard($(this).attr('rel'), $(this).hasClass('allowStatSelect'));
	});

	$('.backToCurrentCardLink').live('click', function(e) {
		e.preventDefault();
		$('.currentOfflineCardLink').click();
	});

	$('.showOfflineSettings').live('click', function(e) {
		e.preventDefault();
		$('#blackOverlay,#offlineSettingsForm').fadeIn();
	});

	// Restart Game
	$('#offlineSettingsForm form').submit(function() {
		offlineCards.restart();
		return false;
	});

});


Back to Top PHP (Symfony)

<?php

/**
 * multiplayer actions.
 *
 * @package    wickets
 * @subpackage multiplayer
 * @author     Simon Lang
 */
class multiplayerActions extends wicketsActions
{
	public function preExecute()
	{
		$this->multiplayerSession = new MultiplayerSession();
		$this->user = $this->getUser()->getDatabaseRow();
	}

  public function executeIndex(sfWebRequest $request)
  {
		$this->cleanup();
		$this->modes = Doctrine::getTable('Mode')->getPlayable();
  }

	public function executeCreateGame(sfWebRequest $request)
	{
		$game = Doctrine_Core::getTable('Game')->create();
		$game->name = $request->getParameter('name');
		if (!$request->getParameter('name')) {
			$game->name = $this->getUser()->getDatabaseRow()->getFirstName()."'s Game";
		}
		$game->mode_id = $request->getParameter('mode_id');
		$game->collection_id = $request->getParameter('collection_id');
		$game->share_deck = $request->getParameter('share_deck');
		$game->last_request = time();
		$game->save();
		$this->redirect('multiplayer/joinGame?id='.$game->getId());
	}

	public function executeJoinGame(sfWebRequest $request)
	{
		$this->getGame($request->getParameter('id'), $request);

		// Get player if they already exist
		$this->player = Doctrine_Core::getTable('Player')->getByUserIdAndGameId($this->user->getId(), $this->game->getId());

		// Render "Choose Team" form
		if (!$this->game->getShareDeck() && !$request->getParameter('actual_collection_id') && !is_object($this->player)) {
			$userCollection = Doctrine_Core::getTable('Collection')->getByUserId($this->user->getId())->getFirst();
			$this->userCollectionId = null;
			if (is_object($userCollection)) {
				$this->userCollectionId = $userCollection->getId();
			}

			$this->canUseOwnDeck = $this->getUser()->getDatabaseRow()->canUseOwnDeck($this->game->getModeId());
			return;
		}

		// If the game has already started, delete this player and kick them out
		if ($this->game->getStartedAt()) {
			if (is_object($this->player)) {
				$this->redirect($this->generateUrl('multiplayer', $this->game));
			}
			else {
				$this->getUser()->setFlash('flashMessageIcon', 'error');
				$this->getUser()->setFlash('flashMessage', 'Sorry, that game is already in progress.');
				$this->redirect('multiplayer/index');
			}
		}

		// If the player does not exist yet, create them.
		if (!is_object($this->player)) {
			$this->player = Doctrine_Core::getTable('Player')->create();
			$this->player->game_id = $this->game->getId();
			$this->player->name = 'Player';
			$this->player->session_id = session_id();
			$this->player->last_request = time();
			$this->player->user_id = $this->getUser()->getDatabaseRow()->getId();
			$this->player->name = $this->getUser()->getDatabaseRow()->getFirstName();
			$this->player->save();
		}

		// Player selects their deck
		if (!$this->game->getShareDeck() && $request->getParameter('actual_collection_id')) {
			$this->player->collection_id = $request->getParameter('actual_collection_id');
		}
	
		$this->player->is_ready = true;	// TODO: remove this field. it is now redundant
		$this->player->save();

		$this->redirect($this->generateUrl('multiplayer', $this->game));
	}

	public function executeStartGame(sfWebRequest $request)
	{
		$this->getGame($request->getParameter('id'), $request);
		$this->players = $this->game->getPlayers();

		$randomPlayerIndex = rand(0, count($this->players) - 1);

		$this->game->round = 1;
		$this->game->started_at = time();
		$this->game->current_player_id = $this->players->get($randomPlayerIndex)->getId();
		$this->game->message = 'Game Started!';
		$this->game->save();

		$this->game->getCurrentPlayer()->updateLastRequest();

		$this->shuffleAndDeal();

		$this->redirect($this->generateUrl('multiplayer', $this->game));
	}

	public function executeViewGame(sfWebRequest $request)
	{
		$this->getGame($request->getParameter('id'), $request);

		$_SESSION['game_id'] = $this->game->getId();

		$this->players = $this->game->getPlayers();
		$this->currentPlayer = $this->game->getCurrentPlayer();
		
		$this->hostPlayer = $this->players->get(0);
		$this->player = $this->game->getPlayerByUserId($this->user->getId());
	
		// Check this user actually has a player in this game.
		if (!is_object($this->player)) {
			$this->getUser()->setFlash('flashMessageIcon', 'error');
			$this->getUser()->setFlash('flashMessage', 'Sorry, that game is already in progress.');
			$this->redirect('multiplayer/index');
		}
	}

	/*

		... SNIP ...

		This is just a sample. It does not include the entire file.

	*/

}