Skip to content

Instantly share code, notes, and snippets.

@DaveTD
Last active December 22, 2015 10:38
Show Gist options
  • Save DaveTD/6459827 to your computer and use it in GitHub Desktop.
Save DaveTD/6459827 to your computer and use it in GitHub Desktop.
Dashing Paddles gist - for the Dashing Widget Challenge

Test widget

test widget server has expired :(

About

Information is sent by websockets handled through Rubame. Data is sent at a rate of ~50 times a second. Chrome tends to be the best at receiving and rendering the output - Firefox can get a little choppy, although the game is still playable.

The server uses Celluloid to handle threading for multiple games to be played concurrently.

Obviously not everybody needs to be running a server to play the game. If you're acting as a client alone, you can ignore these files:

  1. jobs/dashingpaddles.rb
  2. lib/game.rb
  3. lib/gameserver.rb
  4. lib/gameset.rb
  5. lib/player.rb
  6. lib/timer.rb

And you will need to modify line 9 in widgets/dashingpaddles/dashingpaddles.js

ws = new WebSocket("ws://0.0.0.0:8443");

to point at the server you intend to play on.

The Gemfile included in this gist is just what I used as my Gemfile.

This was mostly an exercise to see what it would take to include full duplex communication in a widget. Although the game may not serve any business purpose, if websockets (or some other similar standard) ever become widespread, it might be worthwhile including in a widget server.

PLEASE NOTE:

Heroku doesn't, and other Paas solutions may not, support websockets.

Because the game can't be hosted easily for free, I'm spending a little money to host it on a VPS. It turns out that this VPS is pretty slow and itself can't handle the amount of data output very well (I suspect it's using an old single core processor, and the server never seems to have more than half a gig of free memory, even before dashing is started). I suppose I should have seen that coming when it was $20.70 for three months of use. Although two players playing is still playable, I can imagine that more games would cause the game, and possibly the server to crash quite easily. Please let me know if that happens (its.dave.dawson@gmail.com) and I'll start it back up ASAP.

Have fun!

Other than that, have fun, and try not to be the guy with the giant "LOSER" message left on his dashboard at the end of the day.

<div class='gridster'>
<ul>
<li data-row="1" data-col="1" data-sizex="2" data-sizey="1">
<div data-id="dashingpaddles" data-view="Dashingpaddles" data-title="Dashingpaddles"></div>
</li>
</ul>
</div>
source 'https://rubygems.org'
gem 'dashing'
# Remove this if you don't need a twitter widget.
gem 'twitter'
gem 'execjs'
gem 'therubyracer', :platforms => :ruby
gem 'rubame'
gem 'celluloid'
SCHEDULER.every'10s', :first_in => 1 do |work|
work.unschedule
y = Gameserver.new
end
require 'celluloid/autostart'
class Game
include Celluloid
attr_accessor :game_set, :finished, :p1paddle, :p2paddle, :gameball
def initialize(game_set)
@game_set = game_set
@finished = nil
@gameSizeX = 500
@gameSizeY = 300
@paddleSizeX = 15
@paddleSizeY = 70
@ballSize = 5
@ballLocation = { :x => 250 , :y => 147.5 }
@p1paddle = { :x => 15 , :y => 125 }
@p2paddle = { :x => 470 , :y => 125 }
@p1Speed = 0
@p2Speed = 0
@ballDirection = { :x => 1 , :y => 0 }
@ballSpeed = 6
@maxBallSpeed = 14
@maxBounceAngle = 5*Math::PI/12
# puts ">> New game started"
broadcast
sleep(0.5)
gameloop
end
def event(event)
# puts ">> Game received event: #{event} in the match between #{game_set.player1.key} and #{game_set.player2.key}"
# Not secure at all. I know.
if event[1] == 'u'
changePlayerSpeed(event[0], -5)
elsif event[1] == 'd'
changePlayerSpeed(event[0], 5)
elsif event[1] == 's'
changePlayerSpeed(event[0], 0)
end
end
def changePlayerSpeed(player, speed)
player == "1" ? @p1Speed = speed : @p2Speed = speed
end
def finish
@timer.terminate
game_set.checkMatch
self.terminate
end
def broadcast
# There's a slight advantage to being player 1. I know.
outputString = String.new
outputString << "s"
outputString << "#{@ballLocation[:x].round}"
outputString << ","
outputString << "#{@ballLocation[:y].round}"
#outputString << ","
#outputString << "#{@p1paddle[:x].round}"
outputString << ","
outputString << "#{@p1paddle[:y].round}"
#outputString << ","
#outputString << "#{@p2paddle[:x].round}"
outputString << ","
outputString << "#{@p2paddle[:y].round}"
outputString << ","
outputString << "#{@game_set.player1.wins}"
outputString << ","
outputString << "#{@game_set.player2.wins}"
# puts outputString
@game_set.player1.client.send("#{outputString}")
@game_set.player2.client.send("#{outputString}")
end
def gameloop
@timer = Timer.new(self)
@timer.async.startTicking
end
def gameTick
handlePlayerCollisions
handleBallCollisions
handleWinning
moveBall
movePlayers
broadcast
end
def moveBall
@ballLocation[:x] = @ballLocation[:x] + (@ballSpeed * @ballDirection[:x])
@ballLocation[:y] = @ballLocation[:y] + (@ballSpeed * @ballDirection[:y])
end
def movePlayers
@p1paddle[:y] = @p1paddle[:y] + @p1Speed
@p2paddle[:y] = @p2paddle[:y] + @p2Speed
end
def handlePlayerCollisions
if @p1paddle[:y] == 0 and @p1Speed < 0
@p1Speed = 0
end
if @p1paddle[:y] == @gameSizeY - @paddleSizeY and @p1Speed > 0
@p1Speed = 0
end
if @p2paddle[:y] == 0 and @p2Speed < 0
@p2Speed = 0
end
if @p2paddle[:y] == @gameSizeY - @paddleSizeY and @p2Speed > 0
@p2Speed = 0
end
end
def handleBallCollisions
# Side collision, reflect angle
if @ballLocation[:y] <= 0
@ballDirection[:y] = @ballDirection[:y] * -1
end
if @ballLocation[:y] + @ballSize >= @gameSizeY
@ballDirection[:y] = @ballDirection[:y] * -1
end
# find all important points...
balltopright = { :x => @ballLocation[:x] + @ballSize, :y => @ballLocation[:y] }
ballbottomleft = { :x => @ballLocation[:x] , :y => @ballLocation[:y] + @ballSize }
ballbottomright = { :x => @ballLocation[:x] + @ballSize, :y => @ballLocation[:y] + @ballSize }
p1topright = { :x => @p1paddle[:x] + @paddleSizeX, :y => @p1paddle[:y] }
p1bottomleft = { :x => @p1paddle[:x] , :y => @p1paddle[:y] + @paddleSizeY }
p1bottomright = { :x => @p1paddle[:x] + @paddleSizeX, :y => @p1paddle[:y] + @paddleSizeY }
p2topright = { :x => @p2paddle[:x] + @paddleSizeX, :y => @p2paddle[:y] }
p2bottomleft = { :x => @p2paddle[:x] , :y => @p2paddle[:y] + @paddleSizeY }
p2bottomright = { :x => @p2paddle[:x] + @paddleSizeX, :y => @p2paddle[:y] + @paddleSizeY }
# determine if any corner of the ball is inside or touching the paddle for p1
if (@ballLocation[:x].between?(p1bottomleft[:x], p1bottomright[:x]) and @ballLocation[:y].between?(p1topright[:y], p1bottomright[:y])) or (balltopright[:x].between?(p1bottomleft[:x], p1bottomright[:x]) and balltopright[:y].between?(@p1paddle[:y], p1bottomleft[:y])) or (ballbottomleft[:x].between?(@p1paddle[:x], p1topright[:x]) and ballbottomleft[:y].between?(p1topright[:y], p1bottomright[:y])) or (ballbottomright[:x].between?(@p1paddle[:x], p1topright[:x]) and ballbottomright[:y].between?(@p1paddle[:y], p1bottomleft[:y]))
@ballLocation[:x] = @p1paddle[:x] + @paddleSizeX + 5
p1ymiddle = @p1paddle[:y] + (@paddleSizeY/2)
ballymiddle = @ballLocation[:y] + (@ballSize/2)
paddleballcollision(1, p1ymiddle, ballymiddle)
end
# determine if any corner of the ball is inside or touching the paddle for p2
if (@ballLocation[:x].between?(p2bottomleft[:x], p2bottomright[:x]) and @ballLocation[:y].between?(p2topright[:y], p2bottomright[:y])) or (balltopright[:x].between?(p2bottomleft[:x], p2bottomright[:x]) and balltopright[:y].between?(@p2paddle[:y], p2bottomleft[:y])) or (ballbottomleft[:x].between?(@p2paddle[:x], p2topright[:x]) and ballbottomleft[:y].between?(p2topright[:y], p2bottomright[:y])) or (ballbottomright[:x].between?(@p2paddle[:x], p2topright[:x]) and ballbottomright[:y].between?(@p2paddle[:y], p2bottomleft[:y]))
@ballLocation[:x] = @p2paddle[:x] - 5 - @ballSize
p2ymiddle = @p2paddle[:y] + (@paddleSizeY/2)
ballymiddle = @ballLocation[:y] + (@ballSize/2)
paddleballcollision(2, p2ymiddle, ballymiddle)
end
end
def paddleballcollision(player, paddlemiddle, ballmiddle)
# Where on the paddle did it hit, compared to the middle of the paddle?
relativeIntersectPoint = paddlemiddle - ballmiddle
# Make that a number between -1 and 1
intersectpossible = @paddleSizeY + (2 * @ballSize)
normalizedRelativeIntersectionY = (relativeIntersectPoint/(intersectpossible/2))
# Get an angle based on normalized value...
bounceAngle = normalizedRelativeIntersectionY * @maxBounceAngle
# Switch direction based on player
direction = player == 1 ? 1 : -1
#calculate new direction...
@ballDirection[:x] = Math.cos(bounceAngle) * direction
@ballDirection[:y] = Math.sin(bounceAngle) * -1
if @ballSpeed < @maxBallSpeed
@ballSpeed = @ballSpeed + 1
end
end
def handleWinning
if @ballLocation[:x] < 0
@game_set.playerwin(2)
end
if @ballLocation[:x] + @ballSize > @gameSizeX
@game_set.playerwin(1)
end
end
end
require 'rubame'
class Gameserver
@@LISTENER = 8443
def initialize
puts ">> Opening game server on port #{@@LISTENER}"
@waitQueue = Array.new
@gameList = Array.new
@idcounter = 0
listen
end
def listen
server = Rubame::Server.new("0.0.0.0", @@LISTENER)
while true
server.run do |client|
client.onopen do
end
client.onmessage do |mess|
handle(mess, client)
end
client.onclose do
@waitQueue.each do |player|
if player.client == client
@waitQueue.delete(player)
puts "Removed closed client from wait queue"
end
end
end
end
end
end
def handle(mess, client)
key = extractAcceptHeader(client.handshake)
if mess.chars.first == 'g'
client.send "p:#{key}"
@waitQueue << Player.new(key, client)
checkForNewGame
elsif mess.chars.first == 'c'
#there has to be a more efficient way of doing this
# puts ">> #{key} says: #{mess}"
@gameList.each do |game|
if game.player1.key == key || game.player2.key == key
#puts "player #{key}: #{mess}"
begin
cmd = mess[2]
if game.player1.key == key
game.active_game.event("1#{cmd}")
else
game.active_game.event("2#{cmd}")
end
rescue
puts ">> Tried sending a user message to a dead actor"
end
break
end
end
elsif mess.chars.first == 'q'
@waitQueue.each do |player|
if player.client == client
@waitQueue.delete(player)
puts "Removed coward from the wait queue"
end
end
else
client.send "Error: Unacceptable message sent."
end
end
def extractAcceptHeader(handshake)
#If I had more time, I'd secure this and use secure sockets, but I don't, so I won't
#Please, please don't use this in production ever
return handshake.headers["sec-websocket-key"]
end
def checkForNewGame
if @waitQueue.size >= 2
#There's going to be a limit to this id counter size
@idcounter = @idcounter + 1
@gameList << GameSet.new(@waitQueue.pop, @waitQueue.pop, self, @idcounter)
end
puts ">> Wait queue: #{@waitQueue.size}"
end
def finishGameSet(finishedGame)
@gameList.delete_if { |game| game.id == finishedGame }
end
end
require 'celluloid/autostart'
class GameSet
include Celluloid
attr_accessor :player1, :player2, :active_game, :id
def initialize(p1, p2, server, id)
@id = id
@server = server
@active_game = nil
@player1 = p1
@player2 = p2
@player1.client.send("g:1")
@player2.client.send("g:2")
@towin = 3
checkMatch
end
def checkMatch
if @player1.wins < @towin and @player2.wins < @towin
beginGame
elsif @player1.wins >= @towin
@player1.client.send("v")
@player2.client.send("d")
@server.finishGameSet(@id)
self.terminate
else
@player2.client.send("v")
@player1.client.send("d")
@server.finishGameSet(@id)
self.terminate
end
end
def beginGame
puts ">> Game commencing between players #{@player1.key} and #{@player2.key}"
@active_game = Game.new(self)
end
def playerwin(player)
player == 1 ? @player1.win : @player2.win
# puts ">> Score: #{@player1.wins} - #{@player2.wins}"
@active_game.finish
end
end
class Player
attr_accessor :key, :client, :wins
def initialize(key, client)
@key = key
@client = client
@wins = 0
puts ">> Player connected: #{key}"
end
def win
@wins = @wins + 1
end
end
require 'celluloid/autostart'
class Timer
include Celluloid
def initialize(target)
@target = target
end
def startTicking
loop do
@target.gameTick
sleep(0.02)
end
end
end
class Dashing.Dashingpaddles extends Dashing.Widget
ready: ->
<link href='http://fonts.googleapis.com/css?family=Press+Start+2P' rel='stylesheet' type='text/css'>
<div id="winner" style="display:none; font-size:100px;"></div>
<h1><a id="paddles1" style="vertical-align: middle;" href="javascript:startGame()">Dashing Paddles</a></h1>
<div><a id="paddles2" href="javascript:startGame()">Click Start</a></div>
<div id="instructions" style="margin-top: 35px; font-size:10px;">
<p>Instructions:</p>
<p>Press 'w' to move paddle up</p>
<p>Press 's' to move paddle down</p>
<p>Win three games to win the match!</p>
</div>
<div id="gameArea" style="margin: 0 auto; position: relative; height: 0px; width:0px; background: grey; display:none;">
<div id="ball" style="position: absolute; height: 10px; width: 10px; background: white;"></div>
<div id="p1paddle" style="position: absolute; height: 70px; width: 15px; background: white;"></div>
<div id="p2paddle" style="position: absolute; height: 70px; width: 15px; background: white;"></div>
<div id="p1score" style="position:absolute; font-size:40px; color:#999999;"></div>
<div id="p2score" style="position:absolute; font-size:40px; color:#999999;"></div>
</div>
var ws = null;
var wins = 0;
var you = 0;
function startGame()
{
if ("WebSocket" in window)
{
ws = new WebSocket("ws://0.0.0.0:8443");
var gameSocket = null;
ws.onopen = function()
{
requestGame(ws);
};
ws.onerror = function(err)
{
console.log('Error: ' + err.data);
}
ws.onmessage = function (evt)
{
var received_msg = evt.data;
if (received_msg.substr(0,1) === "p")
{
ticket = received_msg.substr(2,received_msg.length - 1);
console.log("Ticket: " + ticket);
layoutWaiting(ws);
}
else if (received_msg.substr(0,1) === "g") {
you = received_msg.substr(2,3);
activategamekeys();
layoutPlay();
}
else if (received_msg.substr(0,1) === "v") {
victory();
}
else if (received_msg.substr(0,1) === "d") {
defeat();
}
else if (received_msg.substr(0,1) === "s") {
gamestate = received_msg.substr(1,received_msg.length - 1);
redraw(gamestate);
}
};
ws.onclose = function()
{
};
}
else
{
alert("WebSocket NOT supported by your Browser!");
}
}
function requestGame()
{
ws.send("g");
}
function activategamekeys()
{
var downpressed = false;
var uppressed = false;
document.onkeydown = function(event){
event = event || window.event;
var keycode = event.charCode || event.keyCode;
if(keycode === 83){
if (!downpressed) {
//down();
command('d');
downpressed = true;
}
}
if(keycode === 87){
if (!uppressed){
//up();
command('u');
uppressed = true;
}
}
}
document.onkeyup = function(event) {
event = event || window.event;
var keycode = event.charCode || event.keyCode;
if (downpressed && keycode === 87){ command('d'); uppressed = false; }
else if (uppressed && keycode === 83) { command('u'); downpressed = false; }
else if (keycode === 87) { command('s'); uppressed = false; }
else if (keycode === 83) { command('s'); downpressed = false; }
}
}
function layoutWaiting()
{
//console.log('adjusting widget appearance...');
$("div#winner").hide();
$("a#paddles1").text("Waiting for opponent to join...");
$("a#paddles2").text("Click to accept defeat and cancel");
$("a#paddles1").prop("href", "javascript:surrender()");
$("a#paddles2").prop("href", "javascript:surrender()");
$("div#p1score").text("0");
$("div#p2score").text("0");
$("div#instructions").hide();
}
function surrender()
{
ws.send("q");
defeat();
}
function layoutPlay()
{
$("a#paddles1").text("");
$("a#paddles2").text("");
$("div#gameArea").height(300);
$("div#gameArea").width(500);
$("div#gameArea").show();
$("div#p1score").css( 'top', "2px" );
$("div#p2score").css( 'top', "2px" );
$("div#p1score").css( 'left', "2px" );
$("div#p2score").css( 'right', "2px" );
}
function redraw(gamestate)
{
//$("a#paddles1").text(gamestate);
var stateArray = gamestate.split(',');
$("div#ball").css( 'left', stateArray[0] + "px" );
$("div#ball").css( 'top', stateArray[1] + "px" );
$("div#p1paddle").css( 'left', "15px" );
$("div#p1paddle").css( 'top', stateArray[2] + "px" );
$("div#p2paddle").css( 'left', "470px" );
$("div#p2paddle").css( 'top', stateArray[3] + "px" );
if (stateArray[4] === "0" && stateArray[5] === "0")
{
if (you === "1"){
$("div#p1score").text("YOU");
}
else {
$("div#p2score").text("YOU");
}
}
else {
$("div#p1score").text(stateArray[4]);
$("div#p2score").text(stateArray[5]);
}
}
function command(cmd)
{
ws.send("c:" + cmd);
}
function victory()
{
wins += 1;
$("div#winner").text(wins);
$("div#winner").show();
$("a#paddles1").text(" Consecutive Wins");
$("a#paddles1").prop("href", "javascript:startGame()");
$("a#paddles2").text("Play Again");
$("a#paddles2").prop("href", "javascript:startGame()");
hideGame();
}
function defeat()
{
wins == 0;
$("div#winner").text("LOSER");
$("div#winner").show();
$("a#paddles1").text("Dashing Paddles");
$("a#paddles1").prop("href", "javascript:startGame()");
$("a#paddles2").text("Play Again");
$("a#paddles2").prop("href", "javascript:startGame()");
hideGame();
}
function hideGame()
{
$("div#gameArea").height(0);
$("div#gameArea").width(0);
$("div#gameArea").hide();
}
.widget-dashingpaddles {
font-family: 'Press Start 2P', cursive;
background: #4B4B4B;
p {
padding: 1px;
}
}
@pushmatrix
Copy link

Haha, love it.

@pushmatrix
Copy link

You should include some pictures of what it looks like

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment