Add JavaScript client
This commit is contained in:
parent
a9e03b481c
commit
cfea7f287e
@ -18,7 +18,10 @@ public class WebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer {
|
||||
|
||||
@Override
|
||||
public void registerStompEndpoints(StompEndpointRegistry registry) {
|
||||
// For JavaFX client
|
||||
registry.addEndpoint("/stomp");
|
||||
// For JavaScript client
|
||||
registry.addEndpoint("/sockjs").withSockJS();
|
||||
}
|
||||
|
||||
}
|
||||
|
204
server/src/main/resources/static/client.css
Normal file
204
server/src/main/resources/static/client.css
Normal file
@ -0,0 +1,204 @@
|
||||
canvas {
|
||||
display: block;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font: 24pt sans-serif;
|
||||
font-variant: small-caps;
|
||||
text-align: center
|
||||
}
|
||||
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Below based on http://tobiasahlin.com/spinkit */
|
||||
|
||||
.sk-cube-grid {
|
||||
width: 200px;
|
||||
height: 200px;
|
||||
margin: 35px auto;
|
||||
}
|
||||
|
||||
.sk-cube-grid .sk-cube {
|
||||
width: 20%;
|
||||
height: 20%;
|
||||
background-color: orange;
|
||||
float: left;
|
||||
-webkit-animation: sk-cubeGridScaleDelay 1.5s infinite ease-in-out;
|
||||
animation: sk-cubeGridScaleDelay 1.5s infinite ease-in-out;
|
||||
}
|
||||
|
||||
/*
|
||||
0.0s - 26
|
||||
0.1s - 27, 21
|
||||
0.2s - 28, 22, 16
|
||||
0.3s - 29, 23, 17, 11
|
||||
0.4s - 30, 24, 18, 12, 06
|
||||
0.5s - 25, 19, 13, 07, 01
|
||||
0.6s - 20, 14, 08, 02
|
||||
0.7s - 15, 09, 03
|
||||
0.8s - 10, 04
|
||||
0.9s - 05
|
||||
*/
|
||||
|
||||
/* 0 second delay */
|
||||
.sk-cube-grid .sk-cube26 {
|
||||
-webkit-animation-delay: 0s;
|
||||
animation-delay: 0s;
|
||||
}
|
||||
|
||||
/* 0.1 second delay */
|
||||
.sk-cube-grid .sk-cube27 {
|
||||
-webkit-animation-delay: 0.1s;
|
||||
animation-delay: 0.1s;
|
||||
}
|
||||
.sk-cube-grid .sk-cube21 {
|
||||
-webkit-animation-delay: 0.1s;
|
||||
animation-delay: 0.1s;
|
||||
}
|
||||
|
||||
/* 0.2 second delay */
|
||||
.sk-cube-grid .sk-cube28 {
|
||||
-webkit-animation-delay: 0.2s;
|
||||
animation-delay: 0.2s;
|
||||
}
|
||||
.sk-cube-grid .sk-cube22 {
|
||||
-webkit-animation-delay: 0.2s;
|
||||
animation-delay: 0.2s;
|
||||
}
|
||||
.sk-cube-grid .sk-cube16 {
|
||||
-webkit-animation-delay: 0.2s;
|
||||
animation-delay: 0.2s;
|
||||
}
|
||||
|
||||
/* 0.3 second delay */
|
||||
.sk-cube-grid .sk-cube29 {
|
||||
-webkit-animation-delay: 0.3s;
|
||||
animation-delay: 0.3s;
|
||||
}
|
||||
.sk-cube-grid .sk-cube23 {
|
||||
-webkit-animation-delay: 0.3s;
|
||||
animation-delay: 0.3s;
|
||||
}
|
||||
.sk-cube-grid .sk-cube17 {
|
||||
-webkit-animation-delay: 0.3s;
|
||||
animation-delay: 0.3s;
|
||||
}
|
||||
.sk-cube-grid .sk-cube11 {
|
||||
-webkit-animation-delay: 0.3s;
|
||||
animation-delay: 0.3s;
|
||||
}
|
||||
|
||||
/* 0.4 second delay */
|
||||
.sk-cube-grid .sk-cube30 {
|
||||
-webkit-animation-delay: 0.4s;
|
||||
animation-delay: 0.4s;
|
||||
}
|
||||
.sk-cube-grid .sk-cube24 {
|
||||
-webkit-animation-delay: 0.4s;
|
||||
animation-delay: 0.4s;
|
||||
}
|
||||
.sk-cube-grid .sk-cube18 {
|
||||
-webkit-animation-delay: 0.4s;
|
||||
animation-delay: 0.4s;
|
||||
}
|
||||
.sk-cube-grid .sk-cube12 {
|
||||
-webkit-animation-delay: 0.4s;
|
||||
animation-delay: 0.4s;
|
||||
}
|
||||
.sk-cube-grid .sk-cube6 {
|
||||
-webkit-animation-delay: 0.4s;
|
||||
animation-delay: 0.4s;
|
||||
}
|
||||
|
||||
/* 0.5 second delay */
|
||||
.sk-cube-grid .sk-cube25 {
|
||||
-webkit-animation-delay: 0.5s;
|
||||
animation-delay: 0.5s;
|
||||
}
|
||||
.sk-cube-grid .sk-cube19 {
|
||||
-webkit-animation-delay: 0.5s;
|
||||
animation-delay: 0.5s;
|
||||
}
|
||||
.sk-cube-grid .sk-cube13 {
|
||||
-webkit-animation-delay: 0.5s;
|
||||
animation-delay: 0.5s;
|
||||
}
|
||||
.sk-cube-grid .sk-cube7 {
|
||||
-webkit-animation-delay: 0.5s;
|
||||
animation-delay: 0.5s;
|
||||
}
|
||||
.sk-cube-grid .sk-cube1 {
|
||||
-webkit-animation-delay: 0.5s;
|
||||
animation-delay: 0.5s;
|
||||
}
|
||||
|
||||
/* 0.6 second delay */
|
||||
.sk-cube-grid .sk-cube20 {
|
||||
-webkit-animation-delay: 0.6s;
|
||||
animation-delay: 0.6s;
|
||||
}
|
||||
.sk-cube-grid .sk-cube14 {
|
||||
-webkit-animation-delay: 0.6s;
|
||||
animation-delay: 0.6s;
|
||||
}
|
||||
.sk-cube-grid .sk-cube8 {
|
||||
-webkit-animation-delay: 0.6s;
|
||||
animation-delay: 0.6s;
|
||||
}
|
||||
.sk-cube-grid .sk-cube2 {
|
||||
-webkit-animation-delay: 0.6s;
|
||||
animation-delay: 0.6s;
|
||||
}
|
||||
|
||||
/* 0.7 second delay */
|
||||
.sk-cube-grid .sk-cube15 {
|
||||
-webkit-animation-delay: 0.7s;
|
||||
animation-delay: 0.7s;
|
||||
}
|
||||
.sk-cube-grid .sk-cube9 {
|
||||
-webkit-animation-delay: 0.7s;
|
||||
animation-delay: 0.7s;
|
||||
}
|
||||
.sk-cube-grid .sk-cube3 {
|
||||
-webkit-animation-delay: 0.7s;
|
||||
animation-delay: 0.7s;
|
||||
}
|
||||
|
||||
/* 0.8 second delay */
|
||||
.sk-cube-grid .sk-cube10 {
|
||||
-webkit-animation-delay: 0.8s;
|
||||
animation-delay: 0.8s;
|
||||
}
|
||||
.sk-cube-grid .sk-cube4 {
|
||||
-webkit-animation-delay: 0.8s;
|
||||
animation-delay: 0.8s;
|
||||
}
|
||||
|
||||
/* 0.9 second delay */
|
||||
.sk-cube-grid .sk-cube5 {
|
||||
-webkit-animation-delay: 0.9s;
|
||||
animation-delay: 0.9s;
|
||||
}
|
||||
|
||||
@-webkit-keyframes sk-cubeGridScaleDelay {
|
||||
0%, 70%, 100% {
|
||||
-webkit-transform: scale3D(1, 1, 1);
|
||||
transform: scale3D(1, 1, 1);
|
||||
} 35% {
|
||||
-webkit-transform: scale3D(0, 0, 1);
|
||||
transform: scale3D(0, 0, 1);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes sk-cubeGridScaleDelay {
|
||||
0%, 70%, 100% {
|
||||
-webkit-transform: scale3D(1, 1, 1);
|
||||
transform: scale3D(1, 1, 1);
|
||||
} 35% {
|
||||
-webkit-transform: scale3D(0, 0, 1);
|
||||
transform: scale3D(0, 0, 1);
|
||||
}
|
||||
}
|
53
server/src/main/resources/static/client.html
Normal file
53
server/src/main/resources/static/client.html
Normal file
@ -0,0 +1,53 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Lingo</title>
|
||||
<link rel="stylesheet" href="client.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="gameDiv" class="hidden">
|
||||
<canvas id="canvas" width="600" height="425"></canvas>
|
||||
</div>
|
||||
<div id="waitingDiv">
|
||||
<h1>Waiting for Opponent</h1>
|
||||
<!-- Based on http://tobiasahlin.com/spinkit -->
|
||||
<div class="sk-cube-grid">
|
||||
<div class="sk-cube sk-cube1"></div>
|
||||
<div class="sk-cube sk-cube2"></div>
|
||||
<div class="sk-cube sk-cube3"></div>
|
||||
<div class="sk-cube sk-cube4"></div>
|
||||
<div class="sk-cube sk-cube5"></div>
|
||||
<div class="sk-cube sk-cube6"></div>
|
||||
<div class="sk-cube sk-cube7"></div>
|
||||
<div class="sk-cube sk-cube8"></div>
|
||||
<div class="sk-cube sk-cube9"></div>
|
||||
<div class="sk-cube sk-cube10"></div>
|
||||
<div class="sk-cube sk-cube11"></div>
|
||||
<div class="sk-cube sk-cube12"></div>
|
||||
<div class="sk-cube sk-cube13"></div>
|
||||
<div class="sk-cube sk-cube14"></div>
|
||||
<div class="sk-cube sk-cube15"></div>
|
||||
<div class="sk-cube sk-cube16"></div>
|
||||
<div class="sk-cube sk-cube17"></div>
|
||||
<div class="sk-cube sk-cube18"></div>
|
||||
<div class="sk-cube sk-cube19"></div>
|
||||
<div class="sk-cube sk-cube20"></div>
|
||||
<div class="sk-cube sk-cube21"></div>
|
||||
<div class="sk-cube sk-cube22"></div>
|
||||
<div class="sk-cube sk-cube23"></div>
|
||||
<div class="sk-cube sk-cube24"></div>
|
||||
<div class="sk-cube sk-cube25"></div>
|
||||
<div class="sk-cube sk-cube26"></div>
|
||||
<div class="sk-cube sk-cube27"></div>
|
||||
<div class="sk-cube sk-cube28"></div>
|
||||
<div class="sk-cube sk-cube29"></div>
|
||||
<div class="sk-cube sk-cube30"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="//cdnjs.cloudflare.com/ajax/libs/sockjs-client/1.1.1/sockjs.min.js"></script>
|
||||
<script src="//cdnjs.cloudflare.com/ajax/libs/stomp.js/2.3.3/stomp.min.js"></script>
|
||||
<script src="client.js"></script>
|
||||
</body>
|
||||
</html>
|
306
server/src/main/resources/static/client.js
Normal file
306
server/src/main/resources/static/client.js
Normal file
@ -0,0 +1,306 @@
|
||||
var HEIGHT = 300;
|
||||
var WIDTH = 250;
|
||||
var SIDE = 50;
|
||||
var MARGIN_TOP = 50;
|
||||
var MARGIN_BOTTOM = 75;
|
||||
|
||||
var myScore = 0;
|
||||
var myGuess;
|
||||
var myGuesses;
|
||||
var myProgress;
|
||||
var myResults;
|
||||
var opponentScore = 0;
|
||||
var opponentResults;
|
||||
var lastWord;
|
||||
|
||||
var canvasDiv = document.getElementById('canvasDiv');
|
||||
var waitingDiv = document.getElementById('waitingDiv');
|
||||
var canvas = document.getElementById('canvas');
|
||||
var ctx = canvas.getContext('2d');
|
||||
|
||||
var client;
|
||||
|
||||
function main() {
|
||||
ctx.font = '25px Monospace';
|
||||
ctx.textBaseline = 'middle';
|
||||
ctx.textAlign = 'center';
|
||||
|
||||
addKeydownListener();
|
||||
addKeypressListener();
|
||||
|
||||
reset();
|
||||
repaint();
|
||||
|
||||
client = Stomp.over(new SockJS('/sockjs'));
|
||||
|
||||
client.connect({}, function(frame) {
|
||||
console.log('Connected: ' + frame);
|
||||
subscribeToOpponentJoined();
|
||||
subscribeToOpponentLeft();
|
||||
subscribeToOpponentReports();
|
||||
subscribeToPlayerReports();
|
||||
client.send('/app/lingo/join');
|
||||
});
|
||||
}
|
||||
|
||||
// special keys
|
||||
function addKeydownListener() {
|
||||
document.addEventListener('keydown', function(e) {
|
||||
// backspace
|
||||
if (e.which === 8) {
|
||||
myGuess = myGuess.substr(0, myGuess.length - 1);
|
||||
repaint();
|
||||
e.preventDefault();
|
||||
}
|
||||
// return
|
||||
else if (e.which === 13) {
|
||||
if (myGuess.length === 5) {
|
||||
client.send("/app/lingo/guess", {}, myGuess);
|
||||
myGuess = '';
|
||||
repaint();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// characters
|
||||
function addKeypressListener() {
|
||||
document.addEventListener('keypress', function(e) {
|
||||
var charCode = e.charCode;
|
||||
if (isCharacter(charCode)) {
|
||||
if (isCharacterLowercase(charCode)) {
|
||||
charCode = charCode - 32;
|
||||
}
|
||||
var char = String.fromCharCode(charCode);
|
||||
if (myGuess.length < 5) {
|
||||
myGuess += char;
|
||||
repaint();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function drawMyBoard() {
|
||||
var x = 25, y = MARGIN_TOP;
|
||||
drawScore(x, y, myScore);
|
||||
drawInput(x, y, myGuess);
|
||||
var yStart = drawGuesses(x, y, myGuesses, myResults);
|
||||
drawHint(x, yStart, myProgress);
|
||||
drawGrid(x, y);
|
||||
}
|
||||
|
||||
function drawOpponentBoard() {
|
||||
var x = 325, y = MARGIN_TOP;
|
||||
drawScore(x, y, opponentScore);
|
||||
drawResults(x, y, opponentResults);
|
||||
drawGrid(x, y);
|
||||
}
|
||||
|
||||
function drawLastWord() {
|
||||
if (lastWord) {
|
||||
var x = canvas.width / 2;
|
||||
var y = canvas.height - MARGIN_BOTTOM / 2;
|
||||
ctx.fillStyle = 'black';
|
||||
ctx.fillText('Previous word: ' + lastWord.toUpperCase(), x, y);
|
||||
}
|
||||
}
|
||||
|
||||
function drawScore(x, y, score) {
|
||||
var scoreX = x + WIDTH / 2;
|
||||
var scoreY = y / 2;
|
||||
ctx.fillStyle = 'black';
|
||||
ctx.fillText(score, scoreX, scoreY);
|
||||
}
|
||||
|
||||
function drawGrid(xOrigin, yOrigin) {
|
||||
ctx.beginPath();
|
||||
for (var x = 0; x <= WIDTH; x += SIDE) {
|
||||
ctx.moveTo(xOrigin + x, yOrigin);
|
||||
ctx.lineTo(xOrigin + x, yOrigin + HEIGHT);
|
||||
}
|
||||
for (var y = 0; y <= HEIGHT; y += SIDE) {
|
||||
ctx.moveTo(xOrigin, yOrigin + y);
|
||||
ctx.lineTo(xOrigin + WIDTH, yOrigin + y);
|
||||
}
|
||||
ctx.strokeStyle = 'black';
|
||||
ctx.stroke();
|
||||
}
|
||||
|
||||
function drawInput(xOrigin, yOrigin, input) {
|
||||
ctx.fillStyle = 'green';
|
||||
var x = xOrigin + SIDE * 0.5;
|
||||
var y = yOrigin + SIDE * 0.5;
|
||||
for (var i = 0; i < myGuess.length; i++) {
|
||||
ctx.fillText(myGuess[i], x, y);
|
||||
x += SIDE;
|
||||
}
|
||||
}
|
||||
|
||||
function drawGuesses(xOrigin, yOrigin, guesses, results) {
|
||||
var y = yOrigin + SIDE * 1.5;
|
||||
var numGuesses = Math.min(4, guesses.length);
|
||||
for (var i = 0; i < numGuesses; i++) {
|
||||
var x = xOrigin + SIDE * 0.5;
|
||||
var guess = guesses[guesses.length - numGuesses + i];
|
||||
var result = results[results.length - numGuesses + i];
|
||||
for (var j = 0; j < 5; j++) {
|
||||
if (result[j] === 1) {
|
||||
ctx.fillStyle = 'yellow';
|
||||
ctx.fillRect(x - SIDE * 0.5, y - SIDE * 0.5, SIDE, SIDE);
|
||||
} else if (result[j] === 2) {
|
||||
ctx.fillStyle = 'orange';
|
||||
ctx.fillRect(x - SIDE * 0.5, y - SIDE * 0.5, SIDE, SIDE);
|
||||
}
|
||||
ctx.fillStyle = 'green';
|
||||
ctx.fillText(guess[j], x, y);
|
||||
x += SIDE;
|
||||
}
|
||||
y += SIDE;
|
||||
}
|
||||
return y;
|
||||
}
|
||||
|
||||
function drawResults(xOrigin, yOrigin, results) {
|
||||
var y = yOrigin + SIDE * 1.5;
|
||||
var numResults = Math.min(4, results.length);
|
||||
for (var i = 0; i < numResults; i++) {
|
||||
var x = xOrigin + SIDE * 0.5;
|
||||
var result = results[results.length - numResults + i];
|
||||
for (var j = 0; j < 5; j++) {
|
||||
if (result[j] === 1) {
|
||||
ctx.fillStyle = 'yellow';
|
||||
ctx.fillRect(x - SIDE * 0.5, y - SIDE * 0.5, SIDE, SIDE);
|
||||
} else if (result[j] === 2) {
|
||||
ctx.fillStyle = 'orange';
|
||||
ctx.fillRect(x - SIDE * 0.5, y - SIDE * 0.5, SIDE, SIDE);
|
||||
}
|
||||
x += SIDE;
|
||||
}
|
||||
y += SIDE;
|
||||
}
|
||||
return y;
|
||||
}
|
||||
|
||||
function drawHint(xOrigin, yOrigin, progress) {
|
||||
var x = xOrigin + SIDE * 0.5;
|
||||
for (var i = 0; i < 5; i++) {
|
||||
ctx.fillText(progress[i], x, yOrigin);
|
||||
x += SIDE;
|
||||
}
|
||||
}
|
||||
|
||||
function isCharacter(charCode) {
|
||||
return isCharacterLowercase(charCode) || isCharacterUppercase(charCode);
|
||||
}
|
||||
|
||||
function isCharacterLowercase(charCode) {
|
||||
return charCode >= 97 && charCode <= 122;
|
||||
}
|
||||
|
||||
function isCharacterUppercase(charCode) {
|
||||
return charCode >= 65 && charCode <= 90;
|
||||
}
|
||||
|
||||
function isValidResult(result) {
|
||||
for (var i = 0; i < 5; i++) {
|
||||
if (result[i] !== 0 && result[i] !== 1 && result[i] !== 2) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function repaint() {
|
||||
// clear the canvas
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
|
||||
// draw the components
|
||||
drawMyBoard();
|
||||
drawOpponentBoard();
|
||||
drawLastWord();
|
||||
}
|
||||
|
||||
function reset(firstLetter) {
|
||||
if (!firstLetter) {
|
||||
firstLetter = '';
|
||||
}
|
||||
myGuess = '';
|
||||
myGuesses = [];
|
||||
myProgress = [firstLetter, '', '', '', ''];
|
||||
myResults = [];
|
||||
opponentResults = [];
|
||||
}
|
||||
|
||||
function subscribeToOpponentJoined() {
|
||||
client.subscribe('/user/topic/lingo/opponentJoined', function(message) {
|
||||
var firstLetter = message.body;
|
||||
reset(firstLetter);
|
||||
gameDiv.classList.remove('hidden');
|
||||
waitingDiv.classList.add('hidden');
|
||||
repaint();
|
||||
});
|
||||
}
|
||||
|
||||
function subscribeToOpponentLeft() {
|
||||
client.subscribe('/user/topic/lingo/opponentLeft', function(message) {
|
||||
lastWord = null;
|
||||
gameDiv.classList.add('hidden');
|
||||
waitingDiv.classList.remove('hidden');
|
||||
repaint();
|
||||
});
|
||||
}
|
||||
|
||||
function subscribeToOpponentReports() {
|
||||
client.subscribe('/user/topic/lingo/opponentReports', function(message) {
|
||||
var report = JSON.parse(message.body);
|
||||
if (report.correct === true) {
|
||||
var guess = report.guess;
|
||||
var firstLetter = report.firstLetter;
|
||||
console.log('Opponent guessed correctly! ' + guess);
|
||||
opponentScore = opponentScore + 100;
|
||||
lastWord = guess;
|
||||
reset(firstLetter);
|
||||
repaint();
|
||||
} else {
|
||||
var result = report.result;
|
||||
console.log('Opponent result: ' + result);
|
||||
opponentResults.push(result);
|
||||
repaint();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function subscribeToPlayerReports() {
|
||||
client.subscribe('/user/topic/lingo/playerReports', function(message) {
|
||||
var report = JSON.parse(message.body);
|
||||
console.log('My report: ' + report);
|
||||
if (report.correct === true) {
|
||||
var guess = report.guess;
|
||||
var firstLetter = report.firstLetter;
|
||||
console.log('I guessed correctly!');
|
||||
myScore = myScore + 100;
|
||||
lastWord = guess;
|
||||
reset(firstLetter);
|
||||
repaint();
|
||||
} else {
|
||||
var guess = report.guess;
|
||||
var result = report.result;
|
||||
console.log('My result: ' + result);
|
||||
// TODO: use isValidResult function
|
||||
if (result[0] === 9) {
|
||||
myGuesses.push('-----');
|
||||
} else {
|
||||
for (var i = 0; i < 5; i++) {
|
||||
if (result[i] === 2) {
|
||||
myProgress[i] = guess[i];
|
||||
}
|
||||
}
|
||||
myGuesses.push(guess);
|
||||
}
|
||||
myResults.push(result);
|
||||
repaint();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
main();
|
BIN
server/src/main/resources/static/favicon.ico
Normal file
BIN
server/src/main/resources/static/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 318 B |
24
server/src/main/resources/static/index.html
Normal file
24
server/src/main/resources/static/index.html
Normal file
@ -0,0 +1,24 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Lingo</title>
|
||||
<link rel="stylesheet" href="//unpkg.com/bootstrap/dist/css/bootstrap.min.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>Lingo</h1>
|
||||
<h2>JavaFX Client</h2>
|
||||
<p>For now you need to
|
||||
<a href="https://github.com/charlesrgould/lingo-websocket/archive/master.zip">download</a>
|
||||
the
|
||||
<a href="https://github.com/charlesrgould/lingo-websocket">source code</a>
|
||||
and run a command to launch the client.
|
||||
</p>
|
||||
<p>Single player and multiplayer.</p>
|
||||
<h2>JavaScript Client (easier)</h2>
|
||||
<p>Play the game <a href="client.html">here</a>.</p>
|
||||
<p>Multiplayer only.</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
Loading…
x
Reference in New Issue
Block a user