Vue - It's the Royal Game of Ur
up vote
12
down vote
favorite
Background
After learning Kotlin for Advent of Code in December, I started looking into cross-compiling Kotlin for both the JVM and to JavaScript. Then I wrote a game server in Kotlin and also a simple game implementation of a game known as The Royal Game of Ur. Game logic by itself doesn't do much good though without a beautiful client to play it with (few people likes sending data manually). So I decided to make one in what have become my favorite JavaScript framework (everyone must have one, right?).
The repository containing both the client and the server can be found here: https://github.com/Zomis/Server2
Play the game
You can now play The Royal Game of UR with a server (a simple AI just making a random move is also available to play against) or without a server. (If you can't get the server to work, play the version without a server).
Please note that these will be updated continuously and may not reflect the code in this question.
Rules of The Royal Game of Ur
Or rather, my rules.
Two players are fighting to be the first player who races all their 7 pieces to the exit.
The pieces walk like this:
v<<1 E<< 1 = First tile
>>>>>>>| E = Exit
^<<2 E<<
- Only player 1 can use the top row, only player 2 can use the bottom row. Both players share the middle row.
- The first tile for Player 1 is the '1' in the top row. Player 2's first tile is the '2' in the bottom row.
- Players take turns in rolling the four boolean dice. Then you move a piece a number of steps that equals the sum of these four booleans.
- Five tiles are marked with flowers. When a piece lands on a flower the player get to roll again.
- As long as a tile is on a flower another piece may not knock it out (only relevant for the middle flower).
Main Questions
- Do I have too many / too few components? I am aiming to make several other games in Vue so I like to make things re-useable.
- How are my Vue skills?
- Can anything be done better with regards to how I am using Vue?
- I am nowhere near a UX-designer, but how is the user experience?
- Any other feedback also welcome.
Code
Some code that is not included below:
require("../../../games-js/web/games-js")
: This is the Kotlin code for the game model. This is code that has been transpiled to JavaScript from Kotlin.
import Socket from "../socket"
: This is an utility class for handling the potential WebSocket connection. The code below is checking if the Socket is connected and can handle both scenarios.
RoyalGameOfUR.vue
<template>
<div>
<h1>{{ game }} : {{ gameId }}</h1>
<div>
<div>{{ gameOverMessage }}</div>
</div>
<div class="board-parent">
<UrPlayerView v-bind:game="ur" v-bind:playerIndex="0"
:gamePieces="gamePieces"
:onPlaceNewHighlight="onPlaceNewHighlight"
:mouseleave="mouseleave"
:onPlaceNew="placeNew" />
<div class="ur-board">
<div class="ur-pieces-bg">
<div v-for="idx in 20" class="piece piece-bg">
</div>
<div class="piece-black" style="grid-area: 1 / 5 / 2 / 7"></div>
<div class="piece-black" style="grid-area: 3 / 5 / 4 / 7"></div>
</div>
<div class="ur-pieces-flowers">
<UrFlower :x="0" :y="0" />
<UrFlower :x="3" :y="1" />
<UrFlower :x="0" :y="2" />
<UrFlower :x="6" :y="0" />
<UrFlower :x="6" :y="2" />
</div>
<div class="ur-pieces-player">
<transition name="fade">
<UrPiece v-if="destination !== null" :piece="destination" class="piece highlighted"
:mouseover="doNothing" :mouseleave="doNothing"
:class="{['piece-' + destination.player]: true}">
</UrPiece>
</transition>
<UrPiece v-for="piece in playerPieces"
:key="piece.key"
class="piece"
:mouseover="mouseover" :mouseleave="mouseleave"
:class="{['piece-' + piece.player]: true, 'moveable':
ur.isMoveTime && piece.player == ur.currentPlayer &&
ur.canMove_qt1dr2$(ur.currentPlayer, piece.position, ur.roll)}"
:piece="piece"
:onclick="onClick">
</UrPiece>
</div>
</div>
<UrPlayerView v-bind:game="ur" v-bind:playerIndex="1"
:gamePieces="gamePieces"
:onPlaceNewHighlight="onPlaceNewHighlight"
:mouseleave="mouseleave"
:onPlaceNew="placeNew" />
<UrRoll :roll="lastRoll" :usable="ur.roll < 0 && canControlCurrentPlayer" :onDoRoll="onDoRoll" />
</div>
</div>
</template>
<script>
import Socket from "../socket";
import UrPlayerView from "./ur/UrPlayerView";
import UrPiece from "./ur/UrPiece";
import UrRoll from "./ur/UrRoll";
import UrFlower from "./ur/UrFlower";
var games = require("../../../games-js/web/games-js");
if (typeof games["games-js"] !== "undefined") {
// This is needed when doing a production build, but is not used for `npm run dev` locally.
games = games["games-js"];
}
let urgame = new games.net.zomis.games.ur.RoyalGameOfUr_init();
console.log(urgame.toString());
function piecesToObjects(array, playerIndex) {
var playerPieces = array[playerIndex].filter(i => i > 0 && i < 15);
var arrayCopy = ; // Convert Int32Array to Object array
playerPieces.forEach(it => arrayCopy.push(it));
function mapping(position) {
var y = playerIndex == 0 ? 0 : 2;
if (position > 4 && position < 13) {
y = 1;
}
var x =
y == 1
? position - 5
: position <= 4 ? 4 - position : 4 + 8 + 8 - position;
return {
x: x,
y: y,
player: playerIndex,
key: playerIndex + "_" + position,
position: position
};
}
for (var i = 0; i < arrayCopy.length; i++) {
arrayCopy[i] = mapping(arrayCopy[i]);
}
return arrayCopy;
}
export default {
name: "RoyalGameOfUR",
props: ["yourIndex", "game", "gameId"],
data() {
return {
highlighted: null,
lastRoll: 0,
gamePieces: ,
playerPieces: ,
lastMove: 0,
ur: urgame,
gameOverMessage: null
};
},
created() {
if (this.yourIndex < 0) {
Socket.send(
`v1:{ "type": "observer", "game": "${this.game}", "gameId": "${
this.gameId
}", "observer": "start" }`
);
}
Socket.$on("type:PlayerEliminated", this.messageEliminated);
Socket.$on("type:GameMove", this.messageMove);
Socket.$on("type:GameState", this.messageState);
Socket.$on("type:IllegalMove", this.messageIllegal);
this.playerPieces = this.calcPlayerPieces();
},
beforeDestroy() {
Socket.$off("type:PlayerEliminated", this.messageEliminated);
Socket.$off("type:GameMove", this.messageMove);
Socket.$off("type:GameState", this.messageState);
Socket.$off("type:IllegalMove", this.messageIllegal);
},
components: {
UrPlayerView,
UrRoll,
UrFlower,
UrPiece
},
methods: {
doNothing: function() {},
action: function(name, data) {
if (Socket.isConnected()) {
let json = `v1:{ "game": "UR", "gameId": "${
this.gameId
}", "type": "move", "moveType": "${name}", "move": ${data} }`;
Socket.send(json);
} else {
console.log(
"Before Action: " + name + ":" + data + " - " + this.ur.toString()
);
if (name === "roll") {
let rollResult = this.ur.doRoll();
this.rollUpdate(rollResult);
} else {
console.log(
"move: " + name + " = " + data + " curr " + this.ur.currentPlayer
);
var moveResult = this.ur.move_qt1dr2$(
this.ur.currentPlayer,
data,
this.ur.roll
);
console.log("result: " + moveResult);
this.playerPieces = this.calcPlayerPieces();
}
console.log(this.ur.toString());
}
},
placeNew: function(playerIndex) {
if (this.canPlaceNew) {
this.action("move", 0);
}
},
onClick: function(piece) {
if (piece.player !== this.ur.currentPlayer) {
return;
}
if (!this.ur.isMoveTime) {
return;
}
console.log("OnClick in URView: " + piece.x + ", " + piece.y);
this.action("move", piece.position);
},
messageEliminated(e) {
console.log(`Recieved eliminated: ${JSON.stringify(e)}`);
this.gameOverMessage = e;
},
messageMove(e) {
console.log(`Recieved move: ${e.moveType}: ${e.move}`);
if (e.moveType == "move") {
this.ur.move_qt1dr2$(this.ur.currentPlayer, e.move, this.ur.roll);
}
this.playerPieces = this.calcPlayerPieces();
// A move has been done - check if it is my turn.
console.log("After Move: " + this.ur.toString());
},
messageState(e) {
console.log(`MessageState: ${e.roll}`);
if (typeof e.roll !== "undefined") {
this.ur.doRoll_za3lpa$(e.roll);
this.rollUpdate(e.roll);
}
console.log("AfterState: " + this.ur.toString());
},
messageIllegal(e) {
console.log("IllegalMove: " + JSON.stringify(e));
},
rollUpdate(rollValue) {
this.lastRoll = rollValue;
},
onDoRoll() {
this.action("roll", -1);
},
onPlaceNewHighlight(playerIndex) {
if (playerIndex !== this.ur.currentPlayer) {
return;
}
this.highlighted = { player: playerIndex, position: 0 };
},
mouseover(piece) {
if (piece.player !== this.ur.currentPlayer) {
return;
}
this.highlighted = piece;
},
mouseleave() {
this.highlighted = null;
},
calcPlayerPieces() {
let pieces = this.ur.piecesCopy;
this.gamePieces = this.ur.piecesCopy;
let obj0 = piecesToObjects(pieces, 0);
let obj1 = piecesToObjects(pieces, 1);
let result = ;
for (var i = 0; i < obj0.length; i++) {
result.push(obj0[i]);
}
for (var i = 0; i < obj1.length; i++) {
result.push(obj1[i]);
}
console.log(result);
return result;
}
},
computed: {
canControlCurrentPlayer: function() {
return this.ur.currentPlayer == this.yourIndex || !Socket.isConnected();
},
destination: function() {
if (this.highlighted === null) {
return null;
}
if (!this.ur.isMoveTime) {
return null;
}
if (
!this.ur.canMove_qt1dr2$(
this.ur.currentPlayer,
this.highlighted.position,
this.ur.roll
)
) {
return null;
}
let resultPosition = this.highlighted.position + this.ur.roll;
let result = piecesToObjects(
[[resultPosition], [resultPosition]],
this.highlighted.player
);
return result[0];
},
canPlaceNew: function() {
return (
this.canControlCurrentPlayer &&
this.ur.canMove_qt1dr2$(this.ur.currentPlayer, 0, this.ur.roll)
);
}
}
};
</script>
<style>
.piece-0 {
background-color: blue;
}
.ur-pieces-player .piece {
margin: auto;
width: 48px;
height: 48px;
}
.piece-1 {
background-color: red;
}
.piece-flower {
opacity: 0.5;
background-image: url('../assets/ur/flower.svg');
margin: auto;
}
.board-parent {
position: relative;
}
.piece-bg {
background-color: white;
border: 1px solid black;
}
.ur-board {
position: relative;
width: 512px;
height: 192px;
min-width: 512px;
min-height: 192px;
overflow: hidden;
border: 12px solid #6D5720;
border-radius: 12px;
margin: auto;
}
.ur-pieces-flowers {
z-index: 60;
}
.ur-pieces-flowers, .ur-pieces-player,
.ur-pieces-bg {
display: grid;
grid-template-columns: repeat(8, 1fr);
grid-template-rows: repeat(3, 1fr);
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
.ur-pieces-player .piece {
z-index: 70;
}
.piece {
background-size: cover;
z-index: 40;
width: 100%;
height: 100%;
}
.piece-black {
background-color: #7f7f7f;
}
.player-view {
width: 512px;
height: 50px;
margin: auto;
display: flex;
flex-flow: row;
justify-content: space-between;
align-items: center;
}
.side {
display: flex;
flex-flow: row;
}
.piece.highlighted {
opacity: 0.5;
box-shadow: 0 0 10px 8px black;
}
.side-out {
flex-flow: row-reverse;
}
.moveable {
cursor: pointer;
animation: glow 1s infinite alternate;
}
@keyframes glow {
from {
box-shadow: 0 0 10px -10px #aef4af;
}
to {
box-shadow: 0 0 10px 10px #aef4af;
}
}
.fade-enter-active, .fade-leave-active {
transition: opacity .5s;
}
.fade-enter, .fade-leave-to {
opacity: 0;
}
</style>
UrFlower.vue
<template>
<div class="piece piece-flower"
v-bind:style="{ 'grid-area': (y+1) + '/' + (x+1) }">
</div>
</template>
<script>
export default {
name: "UrFlower",
props: ["x", "y"]
};
</script>
UrPiece.vue
<template>
<transition name="fade">
<div class="piece"
v-on:click="click(piece)"
:class="piece.id"
@mouseover="mouseover(piece)" @mouseleave="mouseleave()"
v-bind:style="{ gridArea: (piece.y+1) + '/' + (piece.x+1) }">
</div>
</transition>
</template>
<script>
export default {
name: "UrPiece",
props: ["piece", "onclick", "mouseover", "mouseleave"],
methods: {
click: function(piece) {
console.log(piece);
this.onclick(piece);
}
}
};
</script>
UrPlayerView.vue
<template>
<div class="player-view">
<div class="side side-remaining">
<div class="number">{{ remaining }}</div>
<div class="pieces-container">
<div v-for="n in remaining" class="piece-small pointer"
:class="{ ['piece-' + playerIndex]: true, moveable: canPlaceNew && n == remaining }"
@mouseover="onPlaceNewHighlight(playerIndex)" @mouseleave="mouseleave()"
style="position: absolute; top: 6px;"
:style="{ left: (n-1)*12 + 'px' }" v-on:click="placeNew()">
</div>
</div>
</div>
<transition name="fade">
<div class="player-active-indicator" v-if="game.currentPlayer == playerIndex"></div>
</transition>
<div class="side side-out">
<div class="number">{{ out }}</div>
<div class="pieces-container">
<div v-for="n in out" class="piece-small"
:class="['piece-' + playerIndex]"
style="position: absolute; top: 6px;"
:style="{ right: (n-1)*12 + 'px' }">
</div>
</div>
</div>
</div>
</template>
<script>
export default {
name: "UrPlayerView",
props: [
"game",
"playerIndex",
"onPlaceNew",
"gamePieces",
"onPlaceNewHighlight",
"mouseleave"
],
data() {
return {};
},
methods: {
placeNew: function() {
this.onPlaceNew(this.playerIndex);
}
},
computed: {
remaining: function() {
return this.gamePieces[this.playerIndex].filter(i => i === 0).length;
},
out: function() {
return this.gamePieces[this.playerIndex].filter(i => i === 15).length;
},
canPlaceNew: function() {
return (
this.game.currentPlayer == this.playerIndex &&
this.game.isMoveTime &&
this.game.canMove_qt1dr2$(this.playerIndex, 0, this.game.roll)
);
}
}
};
</script>
<style scoped>
.player-active-indicator {
background: black;
border-radius: 100%;
width: 20px;
height: 20px;
}
.number {
margin: 2px;
font-weight: bold;
font-size: 2em;
}
.piece-small {
background-size: cover;
width: 24px;
height: 24px;
border: 1px solid black;
}
.pieces-container {
position: relative;
}
</style>
UrRoll.vue
<template>
<div class="ur-roll">
<div class="ur-dice" @click="onclick()" :class="{ moveable: usable }">
<div v-for="i in 4" class="ur-die">
<div v-if="rolls[i - 1]" class="ur-die-filled"></div>
</div>
</div>
<span>{{ roll }}</span>
</div>
</template>
<script>
function shuffle(array) {
// https://stackoverflow.com/a/2450976/1310566
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;
}
export default {
name: "UrRoll",
props: ["roll", "usable", "onDoRoll"],
data() {
return { rolls: [false, false, false, false] };
},
watch: {
roll: function(newValue, oldValue) {
console.log("Set roll to " + newValue);
if (newValue < 0) {
return;
}
this.rolls.fill(false);
this.rolls.fill(true, 0, newValue);
console.log(this.rolls);
shuffle(this.rolls);
console.log("After shuffle:");
console.log(this.rolls);
}
},
methods: {
onclick: function() {
this.onDoRoll();
}
}
};
</script>
<style scoped>
.ur-roll {
margin-top: 10px;
}
.ur-roll span {
font-size: 2em;
font-weight: bold;
}
.ur-dice {
width: 320px;
height: 64px;
margin: 5px auto 5px auto;
display: flex;
justify-content: space-between;
}
.ur-die-filled {
background: black;
border-radius: 100%;
width: 20%;
height: 20%;
}
.ur-die {
display: flex;
justify-content: center;
align-items: center;
width: 64px;
border: 1px solid black;
border-radius: 12px;
}
</style>
javascript game css ecmascript-6 vue.js
add a comment |
up vote
12
down vote
favorite
Background
After learning Kotlin for Advent of Code in December, I started looking into cross-compiling Kotlin for both the JVM and to JavaScript. Then I wrote a game server in Kotlin and also a simple game implementation of a game known as The Royal Game of Ur. Game logic by itself doesn't do much good though without a beautiful client to play it with (few people likes sending data manually). So I decided to make one in what have become my favorite JavaScript framework (everyone must have one, right?).
The repository containing both the client and the server can be found here: https://github.com/Zomis/Server2
Play the game
You can now play The Royal Game of UR with a server (a simple AI just making a random move is also available to play against) or without a server. (If you can't get the server to work, play the version without a server).
Please note that these will be updated continuously and may not reflect the code in this question.
Rules of The Royal Game of Ur
Or rather, my rules.
Two players are fighting to be the first player who races all their 7 pieces to the exit.
The pieces walk like this:
v<<1 E<< 1 = First tile
>>>>>>>| E = Exit
^<<2 E<<
- Only player 1 can use the top row, only player 2 can use the bottom row. Both players share the middle row.
- The first tile for Player 1 is the '1' in the top row. Player 2's first tile is the '2' in the bottom row.
- Players take turns in rolling the four boolean dice. Then you move a piece a number of steps that equals the sum of these four booleans.
- Five tiles are marked with flowers. When a piece lands on a flower the player get to roll again.
- As long as a tile is on a flower another piece may not knock it out (only relevant for the middle flower).
Main Questions
- Do I have too many / too few components? I am aiming to make several other games in Vue so I like to make things re-useable.
- How are my Vue skills?
- Can anything be done better with regards to how I am using Vue?
- I am nowhere near a UX-designer, but how is the user experience?
- Any other feedback also welcome.
Code
Some code that is not included below:
require("../../../games-js/web/games-js")
: This is the Kotlin code for the game model. This is code that has been transpiled to JavaScript from Kotlin.
import Socket from "../socket"
: This is an utility class for handling the potential WebSocket connection. The code below is checking if the Socket is connected and can handle both scenarios.
RoyalGameOfUR.vue
<template>
<div>
<h1>{{ game }} : {{ gameId }}</h1>
<div>
<div>{{ gameOverMessage }}</div>
</div>
<div class="board-parent">
<UrPlayerView v-bind:game="ur" v-bind:playerIndex="0"
:gamePieces="gamePieces"
:onPlaceNewHighlight="onPlaceNewHighlight"
:mouseleave="mouseleave"
:onPlaceNew="placeNew" />
<div class="ur-board">
<div class="ur-pieces-bg">
<div v-for="idx in 20" class="piece piece-bg">
</div>
<div class="piece-black" style="grid-area: 1 / 5 / 2 / 7"></div>
<div class="piece-black" style="grid-area: 3 / 5 / 4 / 7"></div>
</div>
<div class="ur-pieces-flowers">
<UrFlower :x="0" :y="0" />
<UrFlower :x="3" :y="1" />
<UrFlower :x="0" :y="2" />
<UrFlower :x="6" :y="0" />
<UrFlower :x="6" :y="2" />
</div>
<div class="ur-pieces-player">
<transition name="fade">
<UrPiece v-if="destination !== null" :piece="destination" class="piece highlighted"
:mouseover="doNothing" :mouseleave="doNothing"
:class="{['piece-' + destination.player]: true}">
</UrPiece>
</transition>
<UrPiece v-for="piece in playerPieces"
:key="piece.key"
class="piece"
:mouseover="mouseover" :mouseleave="mouseleave"
:class="{['piece-' + piece.player]: true, 'moveable':
ur.isMoveTime && piece.player == ur.currentPlayer &&
ur.canMove_qt1dr2$(ur.currentPlayer, piece.position, ur.roll)}"
:piece="piece"
:onclick="onClick">
</UrPiece>
</div>
</div>
<UrPlayerView v-bind:game="ur" v-bind:playerIndex="1"
:gamePieces="gamePieces"
:onPlaceNewHighlight="onPlaceNewHighlight"
:mouseleave="mouseleave"
:onPlaceNew="placeNew" />
<UrRoll :roll="lastRoll" :usable="ur.roll < 0 && canControlCurrentPlayer" :onDoRoll="onDoRoll" />
</div>
</div>
</template>
<script>
import Socket from "../socket";
import UrPlayerView from "./ur/UrPlayerView";
import UrPiece from "./ur/UrPiece";
import UrRoll from "./ur/UrRoll";
import UrFlower from "./ur/UrFlower";
var games = require("../../../games-js/web/games-js");
if (typeof games["games-js"] !== "undefined") {
// This is needed when doing a production build, but is not used for `npm run dev` locally.
games = games["games-js"];
}
let urgame = new games.net.zomis.games.ur.RoyalGameOfUr_init();
console.log(urgame.toString());
function piecesToObjects(array, playerIndex) {
var playerPieces = array[playerIndex].filter(i => i > 0 && i < 15);
var arrayCopy = ; // Convert Int32Array to Object array
playerPieces.forEach(it => arrayCopy.push(it));
function mapping(position) {
var y = playerIndex == 0 ? 0 : 2;
if (position > 4 && position < 13) {
y = 1;
}
var x =
y == 1
? position - 5
: position <= 4 ? 4 - position : 4 + 8 + 8 - position;
return {
x: x,
y: y,
player: playerIndex,
key: playerIndex + "_" + position,
position: position
};
}
for (var i = 0; i < arrayCopy.length; i++) {
arrayCopy[i] = mapping(arrayCopy[i]);
}
return arrayCopy;
}
export default {
name: "RoyalGameOfUR",
props: ["yourIndex", "game", "gameId"],
data() {
return {
highlighted: null,
lastRoll: 0,
gamePieces: ,
playerPieces: ,
lastMove: 0,
ur: urgame,
gameOverMessage: null
};
},
created() {
if (this.yourIndex < 0) {
Socket.send(
`v1:{ "type": "observer", "game": "${this.game}", "gameId": "${
this.gameId
}", "observer": "start" }`
);
}
Socket.$on("type:PlayerEliminated", this.messageEliminated);
Socket.$on("type:GameMove", this.messageMove);
Socket.$on("type:GameState", this.messageState);
Socket.$on("type:IllegalMove", this.messageIllegal);
this.playerPieces = this.calcPlayerPieces();
},
beforeDestroy() {
Socket.$off("type:PlayerEliminated", this.messageEliminated);
Socket.$off("type:GameMove", this.messageMove);
Socket.$off("type:GameState", this.messageState);
Socket.$off("type:IllegalMove", this.messageIllegal);
},
components: {
UrPlayerView,
UrRoll,
UrFlower,
UrPiece
},
methods: {
doNothing: function() {},
action: function(name, data) {
if (Socket.isConnected()) {
let json = `v1:{ "game": "UR", "gameId": "${
this.gameId
}", "type": "move", "moveType": "${name}", "move": ${data} }`;
Socket.send(json);
} else {
console.log(
"Before Action: " + name + ":" + data + " - " + this.ur.toString()
);
if (name === "roll") {
let rollResult = this.ur.doRoll();
this.rollUpdate(rollResult);
} else {
console.log(
"move: " + name + " = " + data + " curr " + this.ur.currentPlayer
);
var moveResult = this.ur.move_qt1dr2$(
this.ur.currentPlayer,
data,
this.ur.roll
);
console.log("result: " + moveResult);
this.playerPieces = this.calcPlayerPieces();
}
console.log(this.ur.toString());
}
},
placeNew: function(playerIndex) {
if (this.canPlaceNew) {
this.action("move", 0);
}
},
onClick: function(piece) {
if (piece.player !== this.ur.currentPlayer) {
return;
}
if (!this.ur.isMoveTime) {
return;
}
console.log("OnClick in URView: " + piece.x + ", " + piece.y);
this.action("move", piece.position);
},
messageEliminated(e) {
console.log(`Recieved eliminated: ${JSON.stringify(e)}`);
this.gameOverMessage = e;
},
messageMove(e) {
console.log(`Recieved move: ${e.moveType}: ${e.move}`);
if (e.moveType == "move") {
this.ur.move_qt1dr2$(this.ur.currentPlayer, e.move, this.ur.roll);
}
this.playerPieces = this.calcPlayerPieces();
// A move has been done - check if it is my turn.
console.log("After Move: " + this.ur.toString());
},
messageState(e) {
console.log(`MessageState: ${e.roll}`);
if (typeof e.roll !== "undefined") {
this.ur.doRoll_za3lpa$(e.roll);
this.rollUpdate(e.roll);
}
console.log("AfterState: " + this.ur.toString());
},
messageIllegal(e) {
console.log("IllegalMove: " + JSON.stringify(e));
},
rollUpdate(rollValue) {
this.lastRoll = rollValue;
},
onDoRoll() {
this.action("roll", -1);
},
onPlaceNewHighlight(playerIndex) {
if (playerIndex !== this.ur.currentPlayer) {
return;
}
this.highlighted = { player: playerIndex, position: 0 };
},
mouseover(piece) {
if (piece.player !== this.ur.currentPlayer) {
return;
}
this.highlighted = piece;
},
mouseleave() {
this.highlighted = null;
},
calcPlayerPieces() {
let pieces = this.ur.piecesCopy;
this.gamePieces = this.ur.piecesCopy;
let obj0 = piecesToObjects(pieces, 0);
let obj1 = piecesToObjects(pieces, 1);
let result = ;
for (var i = 0; i < obj0.length; i++) {
result.push(obj0[i]);
}
for (var i = 0; i < obj1.length; i++) {
result.push(obj1[i]);
}
console.log(result);
return result;
}
},
computed: {
canControlCurrentPlayer: function() {
return this.ur.currentPlayer == this.yourIndex || !Socket.isConnected();
},
destination: function() {
if (this.highlighted === null) {
return null;
}
if (!this.ur.isMoveTime) {
return null;
}
if (
!this.ur.canMove_qt1dr2$(
this.ur.currentPlayer,
this.highlighted.position,
this.ur.roll
)
) {
return null;
}
let resultPosition = this.highlighted.position + this.ur.roll;
let result = piecesToObjects(
[[resultPosition], [resultPosition]],
this.highlighted.player
);
return result[0];
},
canPlaceNew: function() {
return (
this.canControlCurrentPlayer &&
this.ur.canMove_qt1dr2$(this.ur.currentPlayer, 0, this.ur.roll)
);
}
}
};
</script>
<style>
.piece-0 {
background-color: blue;
}
.ur-pieces-player .piece {
margin: auto;
width: 48px;
height: 48px;
}
.piece-1 {
background-color: red;
}
.piece-flower {
opacity: 0.5;
background-image: url('../assets/ur/flower.svg');
margin: auto;
}
.board-parent {
position: relative;
}
.piece-bg {
background-color: white;
border: 1px solid black;
}
.ur-board {
position: relative;
width: 512px;
height: 192px;
min-width: 512px;
min-height: 192px;
overflow: hidden;
border: 12px solid #6D5720;
border-radius: 12px;
margin: auto;
}
.ur-pieces-flowers {
z-index: 60;
}
.ur-pieces-flowers, .ur-pieces-player,
.ur-pieces-bg {
display: grid;
grid-template-columns: repeat(8, 1fr);
grid-template-rows: repeat(3, 1fr);
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
.ur-pieces-player .piece {
z-index: 70;
}
.piece {
background-size: cover;
z-index: 40;
width: 100%;
height: 100%;
}
.piece-black {
background-color: #7f7f7f;
}
.player-view {
width: 512px;
height: 50px;
margin: auto;
display: flex;
flex-flow: row;
justify-content: space-between;
align-items: center;
}
.side {
display: flex;
flex-flow: row;
}
.piece.highlighted {
opacity: 0.5;
box-shadow: 0 0 10px 8px black;
}
.side-out {
flex-flow: row-reverse;
}
.moveable {
cursor: pointer;
animation: glow 1s infinite alternate;
}
@keyframes glow {
from {
box-shadow: 0 0 10px -10px #aef4af;
}
to {
box-shadow: 0 0 10px 10px #aef4af;
}
}
.fade-enter-active, .fade-leave-active {
transition: opacity .5s;
}
.fade-enter, .fade-leave-to {
opacity: 0;
}
</style>
UrFlower.vue
<template>
<div class="piece piece-flower"
v-bind:style="{ 'grid-area': (y+1) + '/' + (x+1) }">
</div>
</template>
<script>
export default {
name: "UrFlower",
props: ["x", "y"]
};
</script>
UrPiece.vue
<template>
<transition name="fade">
<div class="piece"
v-on:click="click(piece)"
:class="piece.id"
@mouseover="mouseover(piece)" @mouseleave="mouseleave()"
v-bind:style="{ gridArea: (piece.y+1) + '/' + (piece.x+1) }">
</div>
</transition>
</template>
<script>
export default {
name: "UrPiece",
props: ["piece", "onclick", "mouseover", "mouseleave"],
methods: {
click: function(piece) {
console.log(piece);
this.onclick(piece);
}
}
};
</script>
UrPlayerView.vue
<template>
<div class="player-view">
<div class="side side-remaining">
<div class="number">{{ remaining }}</div>
<div class="pieces-container">
<div v-for="n in remaining" class="piece-small pointer"
:class="{ ['piece-' + playerIndex]: true, moveable: canPlaceNew && n == remaining }"
@mouseover="onPlaceNewHighlight(playerIndex)" @mouseleave="mouseleave()"
style="position: absolute; top: 6px;"
:style="{ left: (n-1)*12 + 'px' }" v-on:click="placeNew()">
</div>
</div>
</div>
<transition name="fade">
<div class="player-active-indicator" v-if="game.currentPlayer == playerIndex"></div>
</transition>
<div class="side side-out">
<div class="number">{{ out }}</div>
<div class="pieces-container">
<div v-for="n in out" class="piece-small"
:class="['piece-' + playerIndex]"
style="position: absolute; top: 6px;"
:style="{ right: (n-1)*12 + 'px' }">
</div>
</div>
</div>
</div>
</template>
<script>
export default {
name: "UrPlayerView",
props: [
"game",
"playerIndex",
"onPlaceNew",
"gamePieces",
"onPlaceNewHighlight",
"mouseleave"
],
data() {
return {};
},
methods: {
placeNew: function() {
this.onPlaceNew(this.playerIndex);
}
},
computed: {
remaining: function() {
return this.gamePieces[this.playerIndex].filter(i => i === 0).length;
},
out: function() {
return this.gamePieces[this.playerIndex].filter(i => i === 15).length;
},
canPlaceNew: function() {
return (
this.game.currentPlayer == this.playerIndex &&
this.game.isMoveTime &&
this.game.canMove_qt1dr2$(this.playerIndex, 0, this.game.roll)
);
}
}
};
</script>
<style scoped>
.player-active-indicator {
background: black;
border-radius: 100%;
width: 20px;
height: 20px;
}
.number {
margin: 2px;
font-weight: bold;
font-size: 2em;
}
.piece-small {
background-size: cover;
width: 24px;
height: 24px;
border: 1px solid black;
}
.pieces-container {
position: relative;
}
</style>
UrRoll.vue
<template>
<div class="ur-roll">
<div class="ur-dice" @click="onclick()" :class="{ moveable: usable }">
<div v-for="i in 4" class="ur-die">
<div v-if="rolls[i - 1]" class="ur-die-filled"></div>
</div>
</div>
<span>{{ roll }}</span>
</div>
</template>
<script>
function shuffle(array) {
// https://stackoverflow.com/a/2450976/1310566
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;
}
export default {
name: "UrRoll",
props: ["roll", "usable", "onDoRoll"],
data() {
return { rolls: [false, false, false, false] };
},
watch: {
roll: function(newValue, oldValue) {
console.log("Set roll to " + newValue);
if (newValue < 0) {
return;
}
this.rolls.fill(false);
this.rolls.fill(true, 0, newValue);
console.log(this.rolls);
shuffle(this.rolls);
console.log("After shuffle:");
console.log(this.rolls);
}
},
methods: {
onclick: function() {
this.onDoRoll();
}
}
};
</script>
<style scoped>
.ur-roll {
margin-top: 10px;
}
.ur-roll span {
font-size: 2em;
font-weight: bold;
}
.ur-dice {
width: 320px;
height: 64px;
margin: 5px auto 5px auto;
display: flex;
justify-content: space-between;
}
.ur-die-filled {
background: black;
border-radius: 100%;
width: 20%;
height: 20%;
}
.ur-die {
display: flex;
justify-content: center;
align-items: center;
width: 64px;
border: 1px solid black;
border-radius: 12px;
}
</style>
javascript game css ecmascript-6 vue.js
add a comment |
up vote
12
down vote
favorite
up vote
12
down vote
favorite
Background
After learning Kotlin for Advent of Code in December, I started looking into cross-compiling Kotlin for both the JVM and to JavaScript. Then I wrote a game server in Kotlin and also a simple game implementation of a game known as The Royal Game of Ur. Game logic by itself doesn't do much good though without a beautiful client to play it with (few people likes sending data manually). So I decided to make one in what have become my favorite JavaScript framework (everyone must have one, right?).
The repository containing both the client and the server can be found here: https://github.com/Zomis/Server2
Play the game
You can now play The Royal Game of UR with a server (a simple AI just making a random move is also available to play against) or without a server. (If you can't get the server to work, play the version without a server).
Please note that these will be updated continuously and may not reflect the code in this question.
Rules of The Royal Game of Ur
Or rather, my rules.
Two players are fighting to be the first player who races all their 7 pieces to the exit.
The pieces walk like this:
v<<1 E<< 1 = First tile
>>>>>>>| E = Exit
^<<2 E<<
- Only player 1 can use the top row, only player 2 can use the bottom row. Both players share the middle row.
- The first tile for Player 1 is the '1' in the top row. Player 2's first tile is the '2' in the bottom row.
- Players take turns in rolling the four boolean dice. Then you move a piece a number of steps that equals the sum of these four booleans.
- Five tiles are marked with flowers. When a piece lands on a flower the player get to roll again.
- As long as a tile is on a flower another piece may not knock it out (only relevant for the middle flower).
Main Questions
- Do I have too many / too few components? I am aiming to make several other games in Vue so I like to make things re-useable.
- How are my Vue skills?
- Can anything be done better with regards to how I am using Vue?
- I am nowhere near a UX-designer, but how is the user experience?
- Any other feedback also welcome.
Code
Some code that is not included below:
require("../../../games-js/web/games-js")
: This is the Kotlin code for the game model. This is code that has been transpiled to JavaScript from Kotlin.
import Socket from "../socket"
: This is an utility class for handling the potential WebSocket connection. The code below is checking if the Socket is connected and can handle both scenarios.
RoyalGameOfUR.vue
<template>
<div>
<h1>{{ game }} : {{ gameId }}</h1>
<div>
<div>{{ gameOverMessage }}</div>
</div>
<div class="board-parent">
<UrPlayerView v-bind:game="ur" v-bind:playerIndex="0"
:gamePieces="gamePieces"
:onPlaceNewHighlight="onPlaceNewHighlight"
:mouseleave="mouseleave"
:onPlaceNew="placeNew" />
<div class="ur-board">
<div class="ur-pieces-bg">
<div v-for="idx in 20" class="piece piece-bg">
</div>
<div class="piece-black" style="grid-area: 1 / 5 / 2 / 7"></div>
<div class="piece-black" style="grid-area: 3 / 5 / 4 / 7"></div>
</div>
<div class="ur-pieces-flowers">
<UrFlower :x="0" :y="0" />
<UrFlower :x="3" :y="1" />
<UrFlower :x="0" :y="2" />
<UrFlower :x="6" :y="0" />
<UrFlower :x="6" :y="2" />
</div>
<div class="ur-pieces-player">
<transition name="fade">
<UrPiece v-if="destination !== null" :piece="destination" class="piece highlighted"
:mouseover="doNothing" :mouseleave="doNothing"
:class="{['piece-' + destination.player]: true}">
</UrPiece>
</transition>
<UrPiece v-for="piece in playerPieces"
:key="piece.key"
class="piece"
:mouseover="mouseover" :mouseleave="mouseleave"
:class="{['piece-' + piece.player]: true, 'moveable':
ur.isMoveTime && piece.player == ur.currentPlayer &&
ur.canMove_qt1dr2$(ur.currentPlayer, piece.position, ur.roll)}"
:piece="piece"
:onclick="onClick">
</UrPiece>
</div>
</div>
<UrPlayerView v-bind:game="ur" v-bind:playerIndex="1"
:gamePieces="gamePieces"
:onPlaceNewHighlight="onPlaceNewHighlight"
:mouseleave="mouseleave"
:onPlaceNew="placeNew" />
<UrRoll :roll="lastRoll" :usable="ur.roll < 0 && canControlCurrentPlayer" :onDoRoll="onDoRoll" />
</div>
</div>
</template>
<script>
import Socket from "../socket";
import UrPlayerView from "./ur/UrPlayerView";
import UrPiece from "./ur/UrPiece";
import UrRoll from "./ur/UrRoll";
import UrFlower from "./ur/UrFlower";
var games = require("../../../games-js/web/games-js");
if (typeof games["games-js"] !== "undefined") {
// This is needed when doing a production build, but is not used for `npm run dev` locally.
games = games["games-js"];
}
let urgame = new games.net.zomis.games.ur.RoyalGameOfUr_init();
console.log(urgame.toString());
function piecesToObjects(array, playerIndex) {
var playerPieces = array[playerIndex].filter(i => i > 0 && i < 15);
var arrayCopy = ; // Convert Int32Array to Object array
playerPieces.forEach(it => arrayCopy.push(it));
function mapping(position) {
var y = playerIndex == 0 ? 0 : 2;
if (position > 4 && position < 13) {
y = 1;
}
var x =
y == 1
? position - 5
: position <= 4 ? 4 - position : 4 + 8 + 8 - position;
return {
x: x,
y: y,
player: playerIndex,
key: playerIndex + "_" + position,
position: position
};
}
for (var i = 0; i < arrayCopy.length; i++) {
arrayCopy[i] = mapping(arrayCopy[i]);
}
return arrayCopy;
}
export default {
name: "RoyalGameOfUR",
props: ["yourIndex", "game", "gameId"],
data() {
return {
highlighted: null,
lastRoll: 0,
gamePieces: ,
playerPieces: ,
lastMove: 0,
ur: urgame,
gameOverMessage: null
};
},
created() {
if (this.yourIndex < 0) {
Socket.send(
`v1:{ "type": "observer", "game": "${this.game}", "gameId": "${
this.gameId
}", "observer": "start" }`
);
}
Socket.$on("type:PlayerEliminated", this.messageEliminated);
Socket.$on("type:GameMove", this.messageMove);
Socket.$on("type:GameState", this.messageState);
Socket.$on("type:IllegalMove", this.messageIllegal);
this.playerPieces = this.calcPlayerPieces();
},
beforeDestroy() {
Socket.$off("type:PlayerEliminated", this.messageEliminated);
Socket.$off("type:GameMove", this.messageMove);
Socket.$off("type:GameState", this.messageState);
Socket.$off("type:IllegalMove", this.messageIllegal);
},
components: {
UrPlayerView,
UrRoll,
UrFlower,
UrPiece
},
methods: {
doNothing: function() {},
action: function(name, data) {
if (Socket.isConnected()) {
let json = `v1:{ "game": "UR", "gameId": "${
this.gameId
}", "type": "move", "moveType": "${name}", "move": ${data} }`;
Socket.send(json);
} else {
console.log(
"Before Action: " + name + ":" + data + " - " + this.ur.toString()
);
if (name === "roll") {
let rollResult = this.ur.doRoll();
this.rollUpdate(rollResult);
} else {
console.log(
"move: " + name + " = " + data + " curr " + this.ur.currentPlayer
);
var moveResult = this.ur.move_qt1dr2$(
this.ur.currentPlayer,
data,
this.ur.roll
);
console.log("result: " + moveResult);
this.playerPieces = this.calcPlayerPieces();
}
console.log(this.ur.toString());
}
},
placeNew: function(playerIndex) {
if (this.canPlaceNew) {
this.action("move", 0);
}
},
onClick: function(piece) {
if (piece.player !== this.ur.currentPlayer) {
return;
}
if (!this.ur.isMoveTime) {
return;
}
console.log("OnClick in URView: " + piece.x + ", " + piece.y);
this.action("move", piece.position);
},
messageEliminated(e) {
console.log(`Recieved eliminated: ${JSON.stringify(e)}`);
this.gameOverMessage = e;
},
messageMove(e) {
console.log(`Recieved move: ${e.moveType}: ${e.move}`);
if (e.moveType == "move") {
this.ur.move_qt1dr2$(this.ur.currentPlayer, e.move, this.ur.roll);
}
this.playerPieces = this.calcPlayerPieces();
// A move has been done - check if it is my turn.
console.log("After Move: " + this.ur.toString());
},
messageState(e) {
console.log(`MessageState: ${e.roll}`);
if (typeof e.roll !== "undefined") {
this.ur.doRoll_za3lpa$(e.roll);
this.rollUpdate(e.roll);
}
console.log("AfterState: " + this.ur.toString());
},
messageIllegal(e) {
console.log("IllegalMove: " + JSON.stringify(e));
},
rollUpdate(rollValue) {
this.lastRoll = rollValue;
},
onDoRoll() {
this.action("roll", -1);
},
onPlaceNewHighlight(playerIndex) {
if (playerIndex !== this.ur.currentPlayer) {
return;
}
this.highlighted = { player: playerIndex, position: 0 };
},
mouseover(piece) {
if (piece.player !== this.ur.currentPlayer) {
return;
}
this.highlighted = piece;
},
mouseleave() {
this.highlighted = null;
},
calcPlayerPieces() {
let pieces = this.ur.piecesCopy;
this.gamePieces = this.ur.piecesCopy;
let obj0 = piecesToObjects(pieces, 0);
let obj1 = piecesToObjects(pieces, 1);
let result = ;
for (var i = 0; i < obj0.length; i++) {
result.push(obj0[i]);
}
for (var i = 0; i < obj1.length; i++) {
result.push(obj1[i]);
}
console.log(result);
return result;
}
},
computed: {
canControlCurrentPlayer: function() {
return this.ur.currentPlayer == this.yourIndex || !Socket.isConnected();
},
destination: function() {
if (this.highlighted === null) {
return null;
}
if (!this.ur.isMoveTime) {
return null;
}
if (
!this.ur.canMove_qt1dr2$(
this.ur.currentPlayer,
this.highlighted.position,
this.ur.roll
)
) {
return null;
}
let resultPosition = this.highlighted.position + this.ur.roll;
let result = piecesToObjects(
[[resultPosition], [resultPosition]],
this.highlighted.player
);
return result[0];
},
canPlaceNew: function() {
return (
this.canControlCurrentPlayer &&
this.ur.canMove_qt1dr2$(this.ur.currentPlayer, 0, this.ur.roll)
);
}
}
};
</script>
<style>
.piece-0 {
background-color: blue;
}
.ur-pieces-player .piece {
margin: auto;
width: 48px;
height: 48px;
}
.piece-1 {
background-color: red;
}
.piece-flower {
opacity: 0.5;
background-image: url('../assets/ur/flower.svg');
margin: auto;
}
.board-parent {
position: relative;
}
.piece-bg {
background-color: white;
border: 1px solid black;
}
.ur-board {
position: relative;
width: 512px;
height: 192px;
min-width: 512px;
min-height: 192px;
overflow: hidden;
border: 12px solid #6D5720;
border-radius: 12px;
margin: auto;
}
.ur-pieces-flowers {
z-index: 60;
}
.ur-pieces-flowers, .ur-pieces-player,
.ur-pieces-bg {
display: grid;
grid-template-columns: repeat(8, 1fr);
grid-template-rows: repeat(3, 1fr);
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
.ur-pieces-player .piece {
z-index: 70;
}
.piece {
background-size: cover;
z-index: 40;
width: 100%;
height: 100%;
}
.piece-black {
background-color: #7f7f7f;
}
.player-view {
width: 512px;
height: 50px;
margin: auto;
display: flex;
flex-flow: row;
justify-content: space-between;
align-items: center;
}
.side {
display: flex;
flex-flow: row;
}
.piece.highlighted {
opacity: 0.5;
box-shadow: 0 0 10px 8px black;
}
.side-out {
flex-flow: row-reverse;
}
.moveable {
cursor: pointer;
animation: glow 1s infinite alternate;
}
@keyframes glow {
from {
box-shadow: 0 0 10px -10px #aef4af;
}
to {
box-shadow: 0 0 10px 10px #aef4af;
}
}
.fade-enter-active, .fade-leave-active {
transition: opacity .5s;
}
.fade-enter, .fade-leave-to {
opacity: 0;
}
</style>
UrFlower.vue
<template>
<div class="piece piece-flower"
v-bind:style="{ 'grid-area': (y+1) + '/' + (x+1) }">
</div>
</template>
<script>
export default {
name: "UrFlower",
props: ["x", "y"]
};
</script>
UrPiece.vue
<template>
<transition name="fade">
<div class="piece"
v-on:click="click(piece)"
:class="piece.id"
@mouseover="mouseover(piece)" @mouseleave="mouseleave()"
v-bind:style="{ gridArea: (piece.y+1) + '/' + (piece.x+1) }">
</div>
</transition>
</template>
<script>
export default {
name: "UrPiece",
props: ["piece", "onclick", "mouseover", "mouseleave"],
methods: {
click: function(piece) {
console.log(piece);
this.onclick(piece);
}
}
};
</script>
UrPlayerView.vue
<template>
<div class="player-view">
<div class="side side-remaining">
<div class="number">{{ remaining }}</div>
<div class="pieces-container">
<div v-for="n in remaining" class="piece-small pointer"
:class="{ ['piece-' + playerIndex]: true, moveable: canPlaceNew && n == remaining }"
@mouseover="onPlaceNewHighlight(playerIndex)" @mouseleave="mouseleave()"
style="position: absolute; top: 6px;"
:style="{ left: (n-1)*12 + 'px' }" v-on:click="placeNew()">
</div>
</div>
</div>
<transition name="fade">
<div class="player-active-indicator" v-if="game.currentPlayer == playerIndex"></div>
</transition>
<div class="side side-out">
<div class="number">{{ out }}</div>
<div class="pieces-container">
<div v-for="n in out" class="piece-small"
:class="['piece-' + playerIndex]"
style="position: absolute; top: 6px;"
:style="{ right: (n-1)*12 + 'px' }">
</div>
</div>
</div>
</div>
</template>
<script>
export default {
name: "UrPlayerView",
props: [
"game",
"playerIndex",
"onPlaceNew",
"gamePieces",
"onPlaceNewHighlight",
"mouseleave"
],
data() {
return {};
},
methods: {
placeNew: function() {
this.onPlaceNew(this.playerIndex);
}
},
computed: {
remaining: function() {
return this.gamePieces[this.playerIndex].filter(i => i === 0).length;
},
out: function() {
return this.gamePieces[this.playerIndex].filter(i => i === 15).length;
},
canPlaceNew: function() {
return (
this.game.currentPlayer == this.playerIndex &&
this.game.isMoveTime &&
this.game.canMove_qt1dr2$(this.playerIndex, 0, this.game.roll)
);
}
}
};
</script>
<style scoped>
.player-active-indicator {
background: black;
border-radius: 100%;
width: 20px;
height: 20px;
}
.number {
margin: 2px;
font-weight: bold;
font-size: 2em;
}
.piece-small {
background-size: cover;
width: 24px;
height: 24px;
border: 1px solid black;
}
.pieces-container {
position: relative;
}
</style>
UrRoll.vue
<template>
<div class="ur-roll">
<div class="ur-dice" @click="onclick()" :class="{ moveable: usable }">
<div v-for="i in 4" class="ur-die">
<div v-if="rolls[i - 1]" class="ur-die-filled"></div>
</div>
</div>
<span>{{ roll }}</span>
</div>
</template>
<script>
function shuffle(array) {
// https://stackoverflow.com/a/2450976/1310566
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;
}
export default {
name: "UrRoll",
props: ["roll", "usable", "onDoRoll"],
data() {
return { rolls: [false, false, false, false] };
},
watch: {
roll: function(newValue, oldValue) {
console.log("Set roll to " + newValue);
if (newValue < 0) {
return;
}
this.rolls.fill(false);
this.rolls.fill(true, 0, newValue);
console.log(this.rolls);
shuffle(this.rolls);
console.log("After shuffle:");
console.log(this.rolls);
}
},
methods: {
onclick: function() {
this.onDoRoll();
}
}
};
</script>
<style scoped>
.ur-roll {
margin-top: 10px;
}
.ur-roll span {
font-size: 2em;
font-weight: bold;
}
.ur-dice {
width: 320px;
height: 64px;
margin: 5px auto 5px auto;
display: flex;
justify-content: space-between;
}
.ur-die-filled {
background: black;
border-radius: 100%;
width: 20%;
height: 20%;
}
.ur-die {
display: flex;
justify-content: center;
align-items: center;
width: 64px;
border: 1px solid black;
border-radius: 12px;
}
</style>
javascript game css ecmascript-6 vue.js
Background
After learning Kotlin for Advent of Code in December, I started looking into cross-compiling Kotlin for both the JVM and to JavaScript. Then I wrote a game server in Kotlin and also a simple game implementation of a game known as The Royal Game of Ur. Game logic by itself doesn't do much good though without a beautiful client to play it with (few people likes sending data manually). So I decided to make one in what have become my favorite JavaScript framework (everyone must have one, right?).
The repository containing both the client and the server can be found here: https://github.com/Zomis/Server2
Play the game
You can now play The Royal Game of UR with a server (a simple AI just making a random move is also available to play against) or without a server. (If you can't get the server to work, play the version without a server).
Please note that these will be updated continuously and may not reflect the code in this question.
Rules of The Royal Game of Ur
Or rather, my rules.
Two players are fighting to be the first player who races all their 7 pieces to the exit.
The pieces walk like this:
v<<1 E<< 1 = First tile
>>>>>>>| E = Exit
^<<2 E<<
- Only player 1 can use the top row, only player 2 can use the bottom row. Both players share the middle row.
- The first tile for Player 1 is the '1' in the top row. Player 2's first tile is the '2' in the bottom row.
- Players take turns in rolling the four boolean dice. Then you move a piece a number of steps that equals the sum of these four booleans.
- Five tiles are marked with flowers. When a piece lands on a flower the player get to roll again.
- As long as a tile is on a flower another piece may not knock it out (only relevant for the middle flower).
Main Questions
- Do I have too many / too few components? I am aiming to make several other games in Vue so I like to make things re-useable.
- How are my Vue skills?
- Can anything be done better with regards to how I am using Vue?
- I am nowhere near a UX-designer, but how is the user experience?
- Any other feedback also welcome.
Code
Some code that is not included below:
require("../../../games-js/web/games-js")
: This is the Kotlin code for the game model. This is code that has been transpiled to JavaScript from Kotlin.
import Socket from "../socket"
: This is an utility class for handling the potential WebSocket connection. The code below is checking if the Socket is connected and can handle both scenarios.
RoyalGameOfUR.vue
<template>
<div>
<h1>{{ game }} : {{ gameId }}</h1>
<div>
<div>{{ gameOverMessage }}</div>
</div>
<div class="board-parent">
<UrPlayerView v-bind:game="ur" v-bind:playerIndex="0"
:gamePieces="gamePieces"
:onPlaceNewHighlight="onPlaceNewHighlight"
:mouseleave="mouseleave"
:onPlaceNew="placeNew" />
<div class="ur-board">
<div class="ur-pieces-bg">
<div v-for="idx in 20" class="piece piece-bg">
</div>
<div class="piece-black" style="grid-area: 1 / 5 / 2 / 7"></div>
<div class="piece-black" style="grid-area: 3 / 5 / 4 / 7"></div>
</div>
<div class="ur-pieces-flowers">
<UrFlower :x="0" :y="0" />
<UrFlower :x="3" :y="1" />
<UrFlower :x="0" :y="2" />
<UrFlower :x="6" :y="0" />
<UrFlower :x="6" :y="2" />
</div>
<div class="ur-pieces-player">
<transition name="fade">
<UrPiece v-if="destination !== null" :piece="destination" class="piece highlighted"
:mouseover="doNothing" :mouseleave="doNothing"
:class="{['piece-' + destination.player]: true}">
</UrPiece>
</transition>
<UrPiece v-for="piece in playerPieces"
:key="piece.key"
class="piece"
:mouseover="mouseover" :mouseleave="mouseleave"
:class="{['piece-' + piece.player]: true, 'moveable':
ur.isMoveTime && piece.player == ur.currentPlayer &&
ur.canMove_qt1dr2$(ur.currentPlayer, piece.position, ur.roll)}"
:piece="piece"
:onclick="onClick">
</UrPiece>
</div>
</div>
<UrPlayerView v-bind:game="ur" v-bind:playerIndex="1"
:gamePieces="gamePieces"
:onPlaceNewHighlight="onPlaceNewHighlight"
:mouseleave="mouseleave"
:onPlaceNew="placeNew" />
<UrRoll :roll="lastRoll" :usable="ur.roll < 0 && canControlCurrentPlayer" :onDoRoll="onDoRoll" />
</div>
</div>
</template>
<script>
import Socket from "../socket";
import UrPlayerView from "./ur/UrPlayerView";
import UrPiece from "./ur/UrPiece";
import UrRoll from "./ur/UrRoll";
import UrFlower from "./ur/UrFlower";
var games = require("../../../games-js/web/games-js");
if (typeof games["games-js"] !== "undefined") {
// This is needed when doing a production build, but is not used for `npm run dev` locally.
games = games["games-js"];
}
let urgame = new games.net.zomis.games.ur.RoyalGameOfUr_init();
console.log(urgame.toString());
function piecesToObjects(array, playerIndex) {
var playerPieces = array[playerIndex].filter(i => i > 0 && i < 15);
var arrayCopy = ; // Convert Int32Array to Object array
playerPieces.forEach(it => arrayCopy.push(it));
function mapping(position) {
var y = playerIndex == 0 ? 0 : 2;
if (position > 4 && position < 13) {
y = 1;
}
var x =
y == 1
? position - 5
: position <= 4 ? 4 - position : 4 + 8 + 8 - position;
return {
x: x,
y: y,
player: playerIndex,
key: playerIndex + "_" + position,
position: position
};
}
for (var i = 0; i < arrayCopy.length; i++) {
arrayCopy[i] = mapping(arrayCopy[i]);
}
return arrayCopy;
}
export default {
name: "RoyalGameOfUR",
props: ["yourIndex", "game", "gameId"],
data() {
return {
highlighted: null,
lastRoll: 0,
gamePieces: ,
playerPieces: ,
lastMove: 0,
ur: urgame,
gameOverMessage: null
};
},
created() {
if (this.yourIndex < 0) {
Socket.send(
`v1:{ "type": "observer", "game": "${this.game}", "gameId": "${
this.gameId
}", "observer": "start" }`
);
}
Socket.$on("type:PlayerEliminated", this.messageEliminated);
Socket.$on("type:GameMove", this.messageMove);
Socket.$on("type:GameState", this.messageState);
Socket.$on("type:IllegalMove", this.messageIllegal);
this.playerPieces = this.calcPlayerPieces();
},
beforeDestroy() {
Socket.$off("type:PlayerEliminated", this.messageEliminated);
Socket.$off("type:GameMove", this.messageMove);
Socket.$off("type:GameState", this.messageState);
Socket.$off("type:IllegalMove", this.messageIllegal);
},
components: {
UrPlayerView,
UrRoll,
UrFlower,
UrPiece
},
methods: {
doNothing: function() {},
action: function(name, data) {
if (Socket.isConnected()) {
let json = `v1:{ "game": "UR", "gameId": "${
this.gameId
}", "type": "move", "moveType": "${name}", "move": ${data} }`;
Socket.send(json);
} else {
console.log(
"Before Action: " + name + ":" + data + " - " + this.ur.toString()
);
if (name === "roll") {
let rollResult = this.ur.doRoll();
this.rollUpdate(rollResult);
} else {
console.log(
"move: " + name + " = " + data + " curr " + this.ur.currentPlayer
);
var moveResult = this.ur.move_qt1dr2$(
this.ur.currentPlayer,
data,
this.ur.roll
);
console.log("result: " + moveResult);
this.playerPieces = this.calcPlayerPieces();
}
console.log(this.ur.toString());
}
},
placeNew: function(playerIndex) {
if (this.canPlaceNew) {
this.action("move", 0);
}
},
onClick: function(piece) {
if (piece.player !== this.ur.currentPlayer) {
return;
}
if (!this.ur.isMoveTime) {
return;
}
console.log("OnClick in URView: " + piece.x + ", " + piece.y);
this.action("move", piece.position);
},
messageEliminated(e) {
console.log(`Recieved eliminated: ${JSON.stringify(e)}`);
this.gameOverMessage = e;
},
messageMove(e) {
console.log(`Recieved move: ${e.moveType}: ${e.move}`);
if (e.moveType == "move") {
this.ur.move_qt1dr2$(this.ur.currentPlayer, e.move, this.ur.roll);
}
this.playerPieces = this.calcPlayerPieces();
// A move has been done - check if it is my turn.
console.log("After Move: " + this.ur.toString());
},
messageState(e) {
console.log(`MessageState: ${e.roll}`);
if (typeof e.roll !== "undefined") {
this.ur.doRoll_za3lpa$(e.roll);
this.rollUpdate(e.roll);
}
console.log("AfterState: " + this.ur.toString());
},
messageIllegal(e) {
console.log("IllegalMove: " + JSON.stringify(e));
},
rollUpdate(rollValue) {
this.lastRoll = rollValue;
},
onDoRoll() {
this.action("roll", -1);
},
onPlaceNewHighlight(playerIndex) {
if (playerIndex !== this.ur.currentPlayer) {
return;
}
this.highlighted = { player: playerIndex, position: 0 };
},
mouseover(piece) {
if (piece.player !== this.ur.currentPlayer) {
return;
}
this.highlighted = piece;
},
mouseleave() {
this.highlighted = null;
},
calcPlayerPieces() {
let pieces = this.ur.piecesCopy;
this.gamePieces = this.ur.piecesCopy;
let obj0 = piecesToObjects(pieces, 0);
let obj1 = piecesToObjects(pieces, 1);
let result = ;
for (var i = 0; i < obj0.length; i++) {
result.push(obj0[i]);
}
for (var i = 0; i < obj1.length; i++) {
result.push(obj1[i]);
}
console.log(result);
return result;
}
},
computed: {
canControlCurrentPlayer: function() {
return this.ur.currentPlayer == this.yourIndex || !Socket.isConnected();
},
destination: function() {
if (this.highlighted === null) {
return null;
}
if (!this.ur.isMoveTime) {
return null;
}
if (
!this.ur.canMove_qt1dr2$(
this.ur.currentPlayer,
this.highlighted.position,
this.ur.roll
)
) {
return null;
}
let resultPosition = this.highlighted.position + this.ur.roll;
let result = piecesToObjects(
[[resultPosition], [resultPosition]],
this.highlighted.player
);
return result[0];
},
canPlaceNew: function() {
return (
this.canControlCurrentPlayer &&
this.ur.canMove_qt1dr2$(this.ur.currentPlayer, 0, this.ur.roll)
);
}
}
};
</script>
<style>
.piece-0 {
background-color: blue;
}
.ur-pieces-player .piece {
margin: auto;
width: 48px;
height: 48px;
}
.piece-1 {
background-color: red;
}
.piece-flower {
opacity: 0.5;
background-image: url('../assets/ur/flower.svg');
margin: auto;
}
.board-parent {
position: relative;
}
.piece-bg {
background-color: white;
border: 1px solid black;
}
.ur-board {
position: relative;
width: 512px;
height: 192px;
min-width: 512px;
min-height: 192px;
overflow: hidden;
border: 12px solid #6D5720;
border-radius: 12px;
margin: auto;
}
.ur-pieces-flowers {
z-index: 60;
}
.ur-pieces-flowers, .ur-pieces-player,
.ur-pieces-bg {
display: grid;
grid-template-columns: repeat(8, 1fr);
grid-template-rows: repeat(3, 1fr);
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
.ur-pieces-player .piece {
z-index: 70;
}
.piece {
background-size: cover;
z-index: 40;
width: 100%;
height: 100%;
}
.piece-black {
background-color: #7f7f7f;
}
.player-view {
width: 512px;
height: 50px;
margin: auto;
display: flex;
flex-flow: row;
justify-content: space-between;
align-items: center;
}
.side {
display: flex;
flex-flow: row;
}
.piece.highlighted {
opacity: 0.5;
box-shadow: 0 0 10px 8px black;
}
.side-out {
flex-flow: row-reverse;
}
.moveable {
cursor: pointer;
animation: glow 1s infinite alternate;
}
@keyframes glow {
from {
box-shadow: 0 0 10px -10px #aef4af;
}
to {
box-shadow: 0 0 10px 10px #aef4af;
}
}
.fade-enter-active, .fade-leave-active {
transition: opacity .5s;
}
.fade-enter, .fade-leave-to {
opacity: 0;
}
</style>
UrFlower.vue
<template>
<div class="piece piece-flower"
v-bind:style="{ 'grid-area': (y+1) + '/' + (x+1) }">
</div>
</template>
<script>
export default {
name: "UrFlower",
props: ["x", "y"]
};
</script>
UrPiece.vue
<template>
<transition name="fade">
<div class="piece"
v-on:click="click(piece)"
:class="piece.id"
@mouseover="mouseover(piece)" @mouseleave="mouseleave()"
v-bind:style="{ gridArea: (piece.y+1) + '/' + (piece.x+1) }">
</div>
</transition>
</template>
<script>
export default {
name: "UrPiece",
props: ["piece", "onclick", "mouseover", "mouseleave"],
methods: {
click: function(piece) {
console.log(piece);
this.onclick(piece);
}
}
};
</script>
UrPlayerView.vue
<template>
<div class="player-view">
<div class="side side-remaining">
<div class="number">{{ remaining }}</div>
<div class="pieces-container">
<div v-for="n in remaining" class="piece-small pointer"
:class="{ ['piece-' + playerIndex]: true, moveable: canPlaceNew && n == remaining }"
@mouseover="onPlaceNewHighlight(playerIndex)" @mouseleave="mouseleave()"
style="position: absolute; top: 6px;"
:style="{ left: (n-1)*12 + 'px' }" v-on:click="placeNew()">
</div>
</div>
</div>
<transition name="fade">
<div class="player-active-indicator" v-if="game.currentPlayer == playerIndex"></div>
</transition>
<div class="side side-out">
<div class="number">{{ out }}</div>
<div class="pieces-container">
<div v-for="n in out" class="piece-small"
:class="['piece-' + playerIndex]"
style="position: absolute; top: 6px;"
:style="{ right: (n-1)*12 + 'px' }">
</div>
</div>
</div>
</div>
</template>
<script>
export default {
name: "UrPlayerView",
props: [
"game",
"playerIndex",
"onPlaceNew",
"gamePieces",
"onPlaceNewHighlight",
"mouseleave"
],
data() {
return {};
},
methods: {
placeNew: function() {
this.onPlaceNew(this.playerIndex);
}
},
computed: {
remaining: function() {
return this.gamePieces[this.playerIndex].filter(i => i === 0).length;
},
out: function() {
return this.gamePieces[this.playerIndex].filter(i => i === 15).length;
},
canPlaceNew: function() {
return (
this.game.currentPlayer == this.playerIndex &&
this.game.isMoveTime &&
this.game.canMove_qt1dr2$(this.playerIndex, 0, this.game.roll)
);
}
}
};
</script>
<style scoped>
.player-active-indicator {
background: black;
border-radius: 100%;
width: 20px;
height: 20px;
}
.number {
margin: 2px;
font-weight: bold;
font-size: 2em;
}
.piece-small {
background-size: cover;
width: 24px;
height: 24px;
border: 1px solid black;
}
.pieces-container {
position: relative;
}
</style>
UrRoll.vue
<template>
<div class="ur-roll">
<div class="ur-dice" @click="onclick()" :class="{ moveable: usable }">
<div v-for="i in 4" class="ur-die">
<div v-if="rolls[i - 1]" class="ur-die-filled"></div>
</div>
</div>
<span>{{ roll }}</span>
</div>
</template>
<script>
function shuffle(array) {
// https://stackoverflow.com/a/2450976/1310566
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;
}
export default {
name: "UrRoll",
props: ["roll", "usable", "onDoRoll"],
data() {
return { rolls: [false, false, false, false] };
},
watch: {
roll: function(newValue, oldValue) {
console.log("Set roll to " + newValue);
if (newValue < 0) {
return;
}
this.rolls.fill(false);
this.rolls.fill(true, 0, newValue);
console.log(this.rolls);
shuffle(this.rolls);
console.log("After shuffle:");
console.log(this.rolls);
}
},
methods: {
onclick: function() {
this.onDoRoll();
}
}
};
</script>
<style scoped>
.ur-roll {
margin-top: 10px;
}
.ur-roll span {
font-size: 2em;
font-weight: bold;
}
.ur-dice {
width: 320px;
height: 64px;
margin: 5px auto 5px auto;
display: flex;
justify-content: space-between;
}
.ur-die-filled {
background: black;
border-radius: 100%;
width: 20%;
height: 20%;
}
.ur-die {
display: flex;
justify-content: center;
align-items: center;
width: 64px;
border: 1px solid black;
border-radius: 12px;
}
</style>
javascript game css ecmascript-6 vue.js
javascript game css ecmascript-6 vue.js
edited Nov 25 at 6:29
Sᴀᴍ Onᴇᴌᴀ
7,88561750
7,88561750
asked Mar 30 at 21:40
Simon Forsberg♦
48.5k7128286
48.5k7128286
add a comment |
add a comment |
2 Answers
2
active
oldest
votes
up vote
7
down vote
accepted
warning: cheesy meme with bad pun below - if you don't like those, then please skip it...
Ermagherd
Question responses
Do I have too many / too few components? I am aiming to make several other games in Vue so I like to make things re-useable.
I think the current components are divided well. The existing components make sense.
How are my Vue skills?
Usage of Vue looks good. There are a few general JS aspects that I have feedback for (see below, under last "question") but usage of Vue components and other constructs looks good.
Can anything be done better with regards to how I am using Vue?
Bearing in mind I am not an expert VueJS user and have only been working with it on small projects in the past year, I can't really think of anything... If you really wanted you could consider using slots somehow, or an Event bus if the components became more separated but that might not be nessary since everything is contained in the main RoyalGameOfUR component.
If I think of anything else, I will surely update this answer.
I am nowhere near a UX-designer, but how is the user experience?
The layout of the game components is okay, though it would be helpful to have more text prompting the user what to do, or at least the rules and game play instructions somewhere (e.g. in a text box, linked to another page, etc.). In the same vain, I see an uncaught exception in the console if the user clicks the dice when it isn't time to roll. One could catch the exception and alert the user about what happened.
Any other feedback also welcome.
Feedback
Wow that is a really elegant application! Well done! I haven't used the grid
styles yet but hope to in the future.
I did notice that after rolling the dice, when selecting a piece from a stack, it doesn't matter which player is the current player - I can click on either stack (though only a piece from the current player's stack will get moved).
I did notice an error once about this.onclick is not defined
but I didn't observe the path to reproduce it. If I see it again I will let you know.
Suggestions
JS
let
& const
I see the code utilizes let
in a few places but otherwise just var
. It would be wise to start using const
anywhere a value is stored but never re-assigned - then use let
if re-assignment is necessary. Using var
outside of a function declares a global variable1...I only spot one of those in your post (i.e. var games
) but if there were other places where you wanted a variable in another file called games
then this could lead to unintentional value over-writing.
Array copying
In piecesToObjects()
, I see these lines:
var arrayCopy = ; // Convert Int32Array to Object array
playerPieces.forEach(it => arrayCopy.push(it));
You could utilize Array.from()
to copy the array, then use array.map()
to call mapping()
instead of using the for
loop. Originally I was thinking that the forEach
could be eliminated but there is a need to get a regular array instead of the typed array (i.e. Int32Array). If the array being copied (i.e. array
) was a regular array, then you likely could just use .map()
- see this jsPerf to see how much quicker that mapping could be.
return Array.from(playerPieces).map(mapping);
And that function mapping
could be pulled out of piecesToObjects
if playerIndex
is accepted as the first parameter, and then playerIndex can be sent on each iteration using Function.bind() - i.e. using a partially applied function.
return Array.from(playerPieces).map(mapping.bind(null, playerIndex));
Nested Ternary operator
Bearing in mind that this might just be maintained by you, if somebody else wanted to update the code, that person might find the line below less readable than several normal if
blocks. My former supervisor had a rule: no more than one ternary operator in one expression - especially if it made the line longer than ~100 characters.
var x =
y == 1
? position - 5
: position <= 4 ? 4 - position : 4 + 8 + 8 - position;
Something a little more readable might be:
var x;
if (y == 1) {
x = position - 5;
}
else {
x = position <= 4 ? 4 - position : 4 + 8 + 8 - position;
}
0-based Flower grid areas
Why add 1 to the x and y values in UrFlower's template? Perhaps you are so used to 0-based indexes and wanted to keep those values in the markup orthogonal with your ways... Those flowers could be put unto an array and looped over using v-for
... but for 5 flowers that might be too excessive...
CSS
Inline style vs CSS
There are static inline style attributes in UrPlayerView.vue - e.g. :
div v-for="n in remaining" class="piece-small pointer"
:class="{ ['piece-' + playerIndex]: true, moveable: canPlaceNew && n == remaining }"
@mouseover="onPlaceNewHighlight(playerIndex)" @mouseleave="mouseleave()"
style="position: absolute; top: 6px;"
and
<div v-for="n in out" class="piece-small"
:class="['piece-' + playerIndex]"
style="position: absolute; top: 6px;"
The position and top styles could be put into the existing ruleset for .piece-small
...
1https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/var#Description
1
I think the Vue<style scoped>
is not the same as a HTML 5<style scoped>
. Vue adds some data-things to only apply the style to things from the same component. vue-loader.vuejs.org/en/features/scoped-css.html
– Simon Forsberg♦
Apr 6 at 8:28
Ah - my colleague setup the vue loader config before I started working on the project and I must have skipped over that section when learning about vue loader. Good correction!
– Sᴀᴍ Onᴇᴌᴀ
Apr 6 at 18:24
add a comment |
up vote
1
down vote
[
I have become more acquainted with ecmascript-6 since I gave the answer back in April, and realize now that because you are using ecmascript-6 features like const
, let
and arrow functions, other es-6 features could be used as well.
For instance, the following lines in the shuffle()
function:
// And swap it with the current element.
temporaryValue = array[currentIndex];
array[currentIndex] = array[randomIndex];
array[randomIndex] = temporaryValue;
Could be simplified to a single line using (Array) destructuring assignment
[array[currentIndex], array[randomIndex]] = [array[randomIndex], array[currentIndex]];
I also suggested using Array.from()
to copy the array of pieces in piecesToObjects()
, and while that is part of the es-6 standard, the spread syntax
could be used instead of calling that function. Instead of a final statement like
return Array.from(playerPieces).map(mapping);
You should be able to use that spread syntax:
return [...playerPieces].map(mapping);
add a comment |
2 Answers
2
active
oldest
votes
2 Answers
2
active
oldest
votes
active
oldest
votes
active
oldest
votes
up vote
7
down vote
accepted
warning: cheesy meme with bad pun below - if you don't like those, then please skip it...
Ermagherd
Question responses
Do I have too many / too few components? I am aiming to make several other games in Vue so I like to make things re-useable.
I think the current components are divided well. The existing components make sense.
How are my Vue skills?
Usage of Vue looks good. There are a few general JS aspects that I have feedback for (see below, under last "question") but usage of Vue components and other constructs looks good.
Can anything be done better with regards to how I am using Vue?
Bearing in mind I am not an expert VueJS user and have only been working with it on small projects in the past year, I can't really think of anything... If you really wanted you could consider using slots somehow, or an Event bus if the components became more separated but that might not be nessary since everything is contained in the main RoyalGameOfUR component.
If I think of anything else, I will surely update this answer.
I am nowhere near a UX-designer, but how is the user experience?
The layout of the game components is okay, though it would be helpful to have more text prompting the user what to do, or at least the rules and game play instructions somewhere (e.g. in a text box, linked to another page, etc.). In the same vain, I see an uncaught exception in the console if the user clicks the dice when it isn't time to roll. One could catch the exception and alert the user about what happened.
Any other feedback also welcome.
Feedback
Wow that is a really elegant application! Well done! I haven't used the grid
styles yet but hope to in the future.
I did notice that after rolling the dice, when selecting a piece from a stack, it doesn't matter which player is the current player - I can click on either stack (though only a piece from the current player's stack will get moved).
I did notice an error once about this.onclick is not defined
but I didn't observe the path to reproduce it. If I see it again I will let you know.
Suggestions
JS
let
& const
I see the code utilizes let
in a few places but otherwise just var
. It would be wise to start using const
anywhere a value is stored but never re-assigned - then use let
if re-assignment is necessary. Using var
outside of a function declares a global variable1...I only spot one of those in your post (i.e. var games
) but if there were other places where you wanted a variable in another file called games
then this could lead to unintentional value over-writing.
Array copying
In piecesToObjects()
, I see these lines:
var arrayCopy = ; // Convert Int32Array to Object array
playerPieces.forEach(it => arrayCopy.push(it));
You could utilize Array.from()
to copy the array, then use array.map()
to call mapping()
instead of using the for
loop. Originally I was thinking that the forEach
could be eliminated but there is a need to get a regular array instead of the typed array (i.e. Int32Array). If the array being copied (i.e. array
) was a regular array, then you likely could just use .map()
- see this jsPerf to see how much quicker that mapping could be.
return Array.from(playerPieces).map(mapping);
And that function mapping
could be pulled out of piecesToObjects
if playerIndex
is accepted as the first parameter, and then playerIndex can be sent on each iteration using Function.bind() - i.e. using a partially applied function.
return Array.from(playerPieces).map(mapping.bind(null, playerIndex));
Nested Ternary operator
Bearing in mind that this might just be maintained by you, if somebody else wanted to update the code, that person might find the line below less readable than several normal if
blocks. My former supervisor had a rule: no more than one ternary operator in one expression - especially if it made the line longer than ~100 characters.
var x =
y == 1
? position - 5
: position <= 4 ? 4 - position : 4 + 8 + 8 - position;
Something a little more readable might be:
var x;
if (y == 1) {
x = position - 5;
}
else {
x = position <= 4 ? 4 - position : 4 + 8 + 8 - position;
}
0-based Flower grid areas
Why add 1 to the x and y values in UrFlower's template? Perhaps you are so used to 0-based indexes and wanted to keep those values in the markup orthogonal with your ways... Those flowers could be put unto an array and looped over using v-for
... but for 5 flowers that might be too excessive...
CSS
Inline style vs CSS
There are static inline style attributes in UrPlayerView.vue - e.g. :
div v-for="n in remaining" class="piece-small pointer"
:class="{ ['piece-' + playerIndex]: true, moveable: canPlaceNew && n == remaining }"
@mouseover="onPlaceNewHighlight(playerIndex)" @mouseleave="mouseleave()"
style="position: absolute; top: 6px;"
and
<div v-for="n in out" class="piece-small"
:class="['piece-' + playerIndex]"
style="position: absolute; top: 6px;"
The position and top styles could be put into the existing ruleset for .piece-small
...
1https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/var#Description
1
I think the Vue<style scoped>
is not the same as a HTML 5<style scoped>
. Vue adds some data-things to only apply the style to things from the same component. vue-loader.vuejs.org/en/features/scoped-css.html
– Simon Forsberg♦
Apr 6 at 8:28
Ah - my colleague setup the vue loader config before I started working on the project and I must have skipped over that section when learning about vue loader. Good correction!
– Sᴀᴍ Onᴇᴌᴀ
Apr 6 at 18:24
add a comment |
up vote
7
down vote
accepted
warning: cheesy meme with bad pun below - if you don't like those, then please skip it...
Ermagherd
Question responses
Do I have too many / too few components? I am aiming to make several other games in Vue so I like to make things re-useable.
I think the current components are divided well. The existing components make sense.
How are my Vue skills?
Usage of Vue looks good. There are a few general JS aspects that I have feedback for (see below, under last "question") but usage of Vue components and other constructs looks good.
Can anything be done better with regards to how I am using Vue?
Bearing in mind I am not an expert VueJS user and have only been working with it on small projects in the past year, I can't really think of anything... If you really wanted you could consider using slots somehow, or an Event bus if the components became more separated but that might not be nessary since everything is contained in the main RoyalGameOfUR component.
If I think of anything else, I will surely update this answer.
I am nowhere near a UX-designer, but how is the user experience?
The layout of the game components is okay, though it would be helpful to have more text prompting the user what to do, or at least the rules and game play instructions somewhere (e.g. in a text box, linked to another page, etc.). In the same vain, I see an uncaught exception in the console if the user clicks the dice when it isn't time to roll. One could catch the exception and alert the user about what happened.
Any other feedback also welcome.
Feedback
Wow that is a really elegant application! Well done! I haven't used the grid
styles yet but hope to in the future.
I did notice that after rolling the dice, when selecting a piece from a stack, it doesn't matter which player is the current player - I can click on either stack (though only a piece from the current player's stack will get moved).
I did notice an error once about this.onclick is not defined
but I didn't observe the path to reproduce it. If I see it again I will let you know.
Suggestions
JS
let
& const
I see the code utilizes let
in a few places but otherwise just var
. It would be wise to start using const
anywhere a value is stored but never re-assigned - then use let
if re-assignment is necessary. Using var
outside of a function declares a global variable1...I only spot one of those in your post (i.e. var games
) but if there were other places where you wanted a variable in another file called games
then this could lead to unintentional value over-writing.
Array copying
In piecesToObjects()
, I see these lines:
var arrayCopy = ; // Convert Int32Array to Object array
playerPieces.forEach(it => arrayCopy.push(it));
You could utilize Array.from()
to copy the array, then use array.map()
to call mapping()
instead of using the for
loop. Originally I was thinking that the forEach
could be eliminated but there is a need to get a regular array instead of the typed array (i.e. Int32Array). If the array being copied (i.e. array
) was a regular array, then you likely could just use .map()
- see this jsPerf to see how much quicker that mapping could be.
return Array.from(playerPieces).map(mapping);
And that function mapping
could be pulled out of piecesToObjects
if playerIndex
is accepted as the first parameter, and then playerIndex can be sent on each iteration using Function.bind() - i.e. using a partially applied function.
return Array.from(playerPieces).map(mapping.bind(null, playerIndex));
Nested Ternary operator
Bearing in mind that this might just be maintained by you, if somebody else wanted to update the code, that person might find the line below less readable than several normal if
blocks. My former supervisor had a rule: no more than one ternary operator in one expression - especially if it made the line longer than ~100 characters.
var x =
y == 1
? position - 5
: position <= 4 ? 4 - position : 4 + 8 + 8 - position;
Something a little more readable might be:
var x;
if (y == 1) {
x = position - 5;
}
else {
x = position <= 4 ? 4 - position : 4 + 8 + 8 - position;
}
0-based Flower grid areas
Why add 1 to the x and y values in UrFlower's template? Perhaps you are so used to 0-based indexes and wanted to keep those values in the markup orthogonal with your ways... Those flowers could be put unto an array and looped over using v-for
... but for 5 flowers that might be too excessive...
CSS
Inline style vs CSS
There are static inline style attributes in UrPlayerView.vue - e.g. :
div v-for="n in remaining" class="piece-small pointer"
:class="{ ['piece-' + playerIndex]: true, moveable: canPlaceNew && n == remaining }"
@mouseover="onPlaceNewHighlight(playerIndex)" @mouseleave="mouseleave()"
style="position: absolute; top: 6px;"
and
<div v-for="n in out" class="piece-small"
:class="['piece-' + playerIndex]"
style="position: absolute; top: 6px;"
The position and top styles could be put into the existing ruleset for .piece-small
...
1https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/var#Description
1
I think the Vue<style scoped>
is not the same as a HTML 5<style scoped>
. Vue adds some data-things to only apply the style to things from the same component. vue-loader.vuejs.org/en/features/scoped-css.html
– Simon Forsberg♦
Apr 6 at 8:28
Ah - my colleague setup the vue loader config before I started working on the project and I must have skipped over that section when learning about vue loader. Good correction!
– Sᴀᴍ Onᴇᴌᴀ
Apr 6 at 18:24
add a comment |
up vote
7
down vote
accepted
up vote
7
down vote
accepted
warning: cheesy meme with bad pun below - if you don't like those, then please skip it...
Ermagherd
Question responses
Do I have too many / too few components? I am aiming to make several other games in Vue so I like to make things re-useable.
I think the current components are divided well. The existing components make sense.
How are my Vue skills?
Usage of Vue looks good. There are a few general JS aspects that I have feedback for (see below, under last "question") but usage of Vue components and other constructs looks good.
Can anything be done better with regards to how I am using Vue?
Bearing in mind I am not an expert VueJS user and have only been working with it on small projects in the past year, I can't really think of anything... If you really wanted you could consider using slots somehow, or an Event bus if the components became more separated but that might not be nessary since everything is contained in the main RoyalGameOfUR component.
If I think of anything else, I will surely update this answer.
I am nowhere near a UX-designer, but how is the user experience?
The layout of the game components is okay, though it would be helpful to have more text prompting the user what to do, or at least the rules and game play instructions somewhere (e.g. in a text box, linked to another page, etc.). In the same vain, I see an uncaught exception in the console if the user clicks the dice when it isn't time to roll. One could catch the exception and alert the user about what happened.
Any other feedback also welcome.
Feedback
Wow that is a really elegant application! Well done! I haven't used the grid
styles yet but hope to in the future.
I did notice that after rolling the dice, when selecting a piece from a stack, it doesn't matter which player is the current player - I can click on either stack (though only a piece from the current player's stack will get moved).
I did notice an error once about this.onclick is not defined
but I didn't observe the path to reproduce it. If I see it again I will let you know.
Suggestions
JS
let
& const
I see the code utilizes let
in a few places but otherwise just var
. It would be wise to start using const
anywhere a value is stored but never re-assigned - then use let
if re-assignment is necessary. Using var
outside of a function declares a global variable1...I only spot one of those in your post (i.e. var games
) but if there were other places where you wanted a variable in another file called games
then this could lead to unintentional value over-writing.
Array copying
In piecesToObjects()
, I see these lines:
var arrayCopy = ; // Convert Int32Array to Object array
playerPieces.forEach(it => arrayCopy.push(it));
You could utilize Array.from()
to copy the array, then use array.map()
to call mapping()
instead of using the for
loop. Originally I was thinking that the forEach
could be eliminated but there is a need to get a regular array instead of the typed array (i.e. Int32Array). If the array being copied (i.e. array
) was a regular array, then you likely could just use .map()
- see this jsPerf to see how much quicker that mapping could be.
return Array.from(playerPieces).map(mapping);
And that function mapping
could be pulled out of piecesToObjects
if playerIndex
is accepted as the first parameter, and then playerIndex can be sent on each iteration using Function.bind() - i.e. using a partially applied function.
return Array.from(playerPieces).map(mapping.bind(null, playerIndex));
Nested Ternary operator
Bearing in mind that this might just be maintained by you, if somebody else wanted to update the code, that person might find the line below less readable than several normal if
blocks. My former supervisor had a rule: no more than one ternary operator in one expression - especially if it made the line longer than ~100 characters.
var x =
y == 1
? position - 5
: position <= 4 ? 4 - position : 4 + 8 + 8 - position;
Something a little more readable might be:
var x;
if (y == 1) {
x = position - 5;
}
else {
x = position <= 4 ? 4 - position : 4 + 8 + 8 - position;
}
0-based Flower grid areas
Why add 1 to the x and y values in UrFlower's template? Perhaps you are so used to 0-based indexes and wanted to keep those values in the markup orthogonal with your ways... Those flowers could be put unto an array and looped over using v-for
... but for 5 flowers that might be too excessive...
CSS
Inline style vs CSS
There are static inline style attributes in UrPlayerView.vue - e.g. :
div v-for="n in remaining" class="piece-small pointer"
:class="{ ['piece-' + playerIndex]: true, moveable: canPlaceNew && n == remaining }"
@mouseover="onPlaceNewHighlight(playerIndex)" @mouseleave="mouseleave()"
style="position: absolute; top: 6px;"
and
<div v-for="n in out" class="piece-small"
:class="['piece-' + playerIndex]"
style="position: absolute; top: 6px;"
The position and top styles could be put into the existing ruleset for .piece-small
...
1https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/var#Description
warning: cheesy meme with bad pun below - if you don't like those, then please skip it...
Ermagherd
Question responses
Do I have too many / too few components? I am aiming to make several other games in Vue so I like to make things re-useable.
I think the current components are divided well. The existing components make sense.
How are my Vue skills?
Usage of Vue looks good. There are a few general JS aspects that I have feedback for (see below, under last "question") but usage of Vue components and other constructs looks good.
Can anything be done better with regards to how I am using Vue?
Bearing in mind I am not an expert VueJS user and have only been working with it on small projects in the past year, I can't really think of anything... If you really wanted you could consider using slots somehow, or an Event bus if the components became more separated but that might not be nessary since everything is contained in the main RoyalGameOfUR component.
If I think of anything else, I will surely update this answer.
I am nowhere near a UX-designer, but how is the user experience?
The layout of the game components is okay, though it would be helpful to have more text prompting the user what to do, or at least the rules and game play instructions somewhere (e.g. in a text box, linked to another page, etc.). In the same vain, I see an uncaught exception in the console if the user clicks the dice when it isn't time to roll. One could catch the exception and alert the user about what happened.
Any other feedback also welcome.
Feedback
Wow that is a really elegant application! Well done! I haven't used the grid
styles yet but hope to in the future.
I did notice that after rolling the dice, when selecting a piece from a stack, it doesn't matter which player is the current player - I can click on either stack (though only a piece from the current player's stack will get moved).
I did notice an error once about this.onclick is not defined
but I didn't observe the path to reproduce it. If I see it again I will let you know.
Suggestions
JS
let
& const
I see the code utilizes let
in a few places but otherwise just var
. It would be wise to start using const
anywhere a value is stored but never re-assigned - then use let
if re-assignment is necessary. Using var
outside of a function declares a global variable1...I only spot one of those in your post (i.e. var games
) but if there were other places where you wanted a variable in another file called games
then this could lead to unintentional value over-writing.
Array copying
In piecesToObjects()
, I see these lines:
var arrayCopy = ; // Convert Int32Array to Object array
playerPieces.forEach(it => arrayCopy.push(it));
You could utilize Array.from()
to copy the array, then use array.map()
to call mapping()
instead of using the for
loop. Originally I was thinking that the forEach
could be eliminated but there is a need to get a regular array instead of the typed array (i.e. Int32Array). If the array being copied (i.e. array
) was a regular array, then you likely could just use .map()
- see this jsPerf to see how much quicker that mapping could be.
return Array.from(playerPieces).map(mapping);
And that function mapping
could be pulled out of piecesToObjects
if playerIndex
is accepted as the first parameter, and then playerIndex can be sent on each iteration using Function.bind() - i.e. using a partially applied function.
return Array.from(playerPieces).map(mapping.bind(null, playerIndex));
Nested Ternary operator
Bearing in mind that this might just be maintained by you, if somebody else wanted to update the code, that person might find the line below less readable than several normal if
blocks. My former supervisor had a rule: no more than one ternary operator in one expression - especially if it made the line longer than ~100 characters.
var x =
y == 1
? position - 5
: position <= 4 ? 4 - position : 4 + 8 + 8 - position;
Something a little more readable might be:
var x;
if (y == 1) {
x = position - 5;
}
else {
x = position <= 4 ? 4 - position : 4 + 8 + 8 - position;
}
0-based Flower grid areas
Why add 1 to the x and y values in UrFlower's template? Perhaps you are so used to 0-based indexes and wanted to keep those values in the markup orthogonal with your ways... Those flowers could be put unto an array and looped over using v-for
... but for 5 flowers that might be too excessive...
CSS
Inline style vs CSS
There are static inline style attributes in UrPlayerView.vue - e.g. :
div v-for="n in remaining" class="piece-small pointer"
:class="{ ['piece-' + playerIndex]: true, moveable: canPlaceNew && n == remaining }"
@mouseover="onPlaceNewHighlight(playerIndex)" @mouseleave="mouseleave()"
style="position: absolute; top: 6px;"
and
<div v-for="n in out" class="piece-small"
:class="['piece-' + playerIndex]"
style="position: absolute; top: 6px;"
The position and top styles could be put into the existing ruleset for .piece-small
...
1https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/var#Description
edited May 14 at 21:34
answered Apr 5 at 2:23
Sᴀᴍ Onᴇᴌᴀ
7,88561750
7,88561750
1
I think the Vue<style scoped>
is not the same as a HTML 5<style scoped>
. Vue adds some data-things to only apply the style to things from the same component. vue-loader.vuejs.org/en/features/scoped-css.html
– Simon Forsberg♦
Apr 6 at 8:28
Ah - my colleague setup the vue loader config before I started working on the project and I must have skipped over that section when learning about vue loader. Good correction!
– Sᴀᴍ Onᴇᴌᴀ
Apr 6 at 18:24
add a comment |
1
I think the Vue<style scoped>
is not the same as a HTML 5<style scoped>
. Vue adds some data-things to only apply the style to things from the same component. vue-loader.vuejs.org/en/features/scoped-css.html
– Simon Forsberg♦
Apr 6 at 8:28
Ah - my colleague setup the vue loader config before I started working on the project and I must have skipped over that section when learning about vue loader. Good correction!
– Sᴀᴍ Onᴇᴌᴀ
Apr 6 at 18:24
1
1
I think the Vue
<style scoped>
is not the same as a HTML 5 <style scoped>
. Vue adds some data-things to only apply the style to things from the same component. vue-loader.vuejs.org/en/features/scoped-css.html– Simon Forsberg♦
Apr 6 at 8:28
I think the Vue
<style scoped>
is not the same as a HTML 5 <style scoped>
. Vue adds some data-things to only apply the style to things from the same component. vue-loader.vuejs.org/en/features/scoped-css.html– Simon Forsberg♦
Apr 6 at 8:28
Ah - my colleague setup the vue loader config before I started working on the project and I must have skipped over that section when learning about vue loader. Good correction!
– Sᴀᴍ Onᴇᴌᴀ
Apr 6 at 18:24
Ah - my colleague setup the vue loader config before I started working on the project and I must have skipped over that section when learning about vue loader. Good correction!
– Sᴀᴍ Onᴇᴌᴀ
Apr 6 at 18:24
add a comment |
up vote
1
down vote
[
I have become more acquainted with ecmascript-6 since I gave the answer back in April, and realize now that because you are using ecmascript-6 features like const
, let
and arrow functions, other es-6 features could be used as well.
For instance, the following lines in the shuffle()
function:
// And swap it with the current element.
temporaryValue = array[currentIndex];
array[currentIndex] = array[randomIndex];
array[randomIndex] = temporaryValue;
Could be simplified to a single line using (Array) destructuring assignment
[array[currentIndex], array[randomIndex]] = [array[randomIndex], array[currentIndex]];
I also suggested using Array.from()
to copy the array of pieces in piecesToObjects()
, and while that is part of the es-6 standard, the spread syntax
could be used instead of calling that function. Instead of a final statement like
return Array.from(playerPieces).map(mapping);
You should be able to use that spread syntax:
return [...playerPieces].map(mapping);
add a comment |
up vote
1
down vote
[
I have become more acquainted with ecmascript-6 since I gave the answer back in April, and realize now that because you are using ecmascript-6 features like const
, let
and arrow functions, other es-6 features could be used as well.
For instance, the following lines in the shuffle()
function:
// And swap it with the current element.
temporaryValue = array[currentIndex];
array[currentIndex] = array[randomIndex];
array[randomIndex] = temporaryValue;
Could be simplified to a single line using (Array) destructuring assignment
[array[currentIndex], array[randomIndex]] = [array[randomIndex], array[currentIndex]];
I also suggested using Array.from()
to copy the array of pieces in piecesToObjects()
, and while that is part of the es-6 standard, the spread syntax
could be used instead of calling that function. Instead of a final statement like
return Array.from(playerPieces).map(mapping);
You should be able to use that spread syntax:
return [...playerPieces].map(mapping);
add a comment |
up vote
1
down vote
up vote
1
down vote
[
I have become more acquainted with ecmascript-6 since I gave the answer back in April, and realize now that because you are using ecmascript-6 features like const
, let
and arrow functions, other es-6 features could be used as well.
For instance, the following lines in the shuffle()
function:
// And swap it with the current element.
temporaryValue = array[currentIndex];
array[currentIndex] = array[randomIndex];
array[randomIndex] = temporaryValue;
Could be simplified to a single line using (Array) destructuring assignment
[array[currentIndex], array[randomIndex]] = [array[randomIndex], array[currentIndex]];
I also suggested using Array.from()
to copy the array of pieces in piecesToObjects()
, and while that is part of the es-6 standard, the spread syntax
could be used instead of calling that function. Instead of a final statement like
return Array.from(playerPieces).map(mapping);
You should be able to use that spread syntax:
return [...playerPieces].map(mapping);
[
I have become more acquainted with ecmascript-6 since I gave the answer back in April, and realize now that because you are using ecmascript-6 features like const
, let
and arrow functions, other es-6 features could be used as well.
For instance, the following lines in the shuffle()
function:
// And swap it with the current element.
temporaryValue = array[currentIndex];
array[currentIndex] = array[randomIndex];
array[randomIndex] = temporaryValue;
Could be simplified to a single line using (Array) destructuring assignment
[array[currentIndex], array[randomIndex]] = [array[randomIndex], array[currentIndex]];
I also suggested using Array.from()
to copy the array of pieces in piecesToObjects()
, and while that is part of the es-6 standard, the spread syntax
could be used instead of calling that function. Instead of a final statement like
return Array.from(playerPieces).map(mapping);
You should be able to use that spread syntax:
return [...playerPieces].map(mapping);
answered Nov 25 at 6:28
Sᴀᴍ Onᴇᴌᴀ
7,88561750
7,88561750
add a comment |
add a comment |
Thanks for contributing an answer to Code Review Stack Exchange!
- Please be sure to answer the question. Provide details and share your research!
But avoid …
- Asking for help, clarification, or responding to other answers.
- Making statements based on opinion; back them up with references or personal experience.
Use MathJax to format equations. MathJax reference.
To learn more, see our tips on writing great answers.
Some of your past answers have not been well-received, and you're in danger of being blocked from answering.
Please pay close attention to the following guidance:
- Please be sure to answer the question. Provide details and share your research!
But avoid …
- Asking for help, clarification, or responding to other answers.
- Making statements based on opinion; back them up with references or personal experience.
To learn more, see our tips on writing great answers.
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
StackExchange.ready(
function () {
StackExchange.openid.initPostLogin('.new-post-login', 'https%3a%2f%2fcodereview.stackexchange.com%2fquestions%2f190896%2fvue-its-the-royal-game-of-ur%23new-answer', 'question_page');
}
);
Post as a guest
Required, but never shown
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown