It is that time of the year again, when ~gome calls us together to add something cool to our webpages. As per usual, I had no idea what to do, but then I discovered The Royal Game of Ur and figured that it would be quite trivial thing to program. And so I decided that this is what I should do this year, and to make it a bit more interesting, I have decided I would fit the entire project into a single '.html' file, so that you can easily download it and play it offline.
You can find the game here!
As all things should be, the game is built on a bunch of HTML <table>s. Two, to be precise: one is the game board and the other holds the dice.
Each field of the board has it's 'y-x' coordinates as its ID. Further classes are used for stylistic reasons. The final board looks like so:
<table id="game-board">
<!-- row-column -->
<tr>
<td id="0-0" class="rosette"></td>
<td id="0-1" class=""></td>
<td id="0-2"></td>
<td id="0-3"></td>
<td id="0-4" class="special-field"></td>
<td id="0-5" class="special-field"></td>
<td id="0-6" class="rosette"></td>
<td id="0-7"></td>
</tr>
<tr>
<td id="1-0"></td>
<td id="1-1"></td>
<td id="1-2"></td>
<td id="1-3" class="rosette"></td>
<td id="1-4"></td>
<td id="1-5"></td>
<td id="1-6"></td>
<td id="1-7"></td>
</tr>
<tr>
<td id="2-0" class="rosette"></td>
<td id="2-1"></td>
<td id="2-2"></td>
<td id="2-3"></td>
<td id="2-4" class="special-field"></td>
<td id="2-5" class="special-field"></td>
<td id="2-6" class="rosette"></td>
<td id="2-7"></td>
</tr>
</table>
Stones are represented as <div>s styled to be round. The unfielded stone pile displays a number, which is simply placed inside the stone <div>. Dice are represented in a similar manner, but styled to look like triangles instead.
This naturally leads to CSS, so here is how you make '<div>'s into circles and triangles.
/* circle */
.black-stone, .white-stone {
width: var(--stone-radius);
height: var(--stone-radius);
border-radius: var(--stone-radius);
margin: auto;
text-align: center;
font-size: calc(var(--stone-radius) * 0.75);
}
.black-stone {
background-color: var(--black);
color: var(--white);
}
.white-stone {
background-color: var(--white);
color: var(--black);
}
/* triangle */
/* https://www.w3schools.com/howto/tryit.asp?filename=tryhow_css_shapes_triangle-up */
.die-hit, .die-miss {
margin: auto;
width: 0;
height: 0;
border-left: calc(var(--triangle-side-size) / 2) solid transparent;
border-right: calc(var(--triangle-side-size) / 2) solid transparent;
}
.die-hit {
border-bottom: var(--triangle-side-size) solid var(--white);
}
.die-miss {
border-bottom: var(--triangle-side-size) solid var(--black);
}
You might have noticed, that I like CSS variables a lot. Not only do they allow you to add a little 'config' section to your CSS like so:
:root {
--border-size: 3px;
--field-size: 6rem;
--stone-radius: 4rem;
--dice-field-size: 4rem;
--triangle-side-size: 2.5rem;
--black: darkslategray;
--white: snow;
--border-color: mediumblue;
--alert-border-color: crimson;
--dice-pool-border-color: var(--border-color);
--dice-pool-roll-border-color: seagreen;
--field-color: wheat;
--rosette-color: palegreen;
--special-field-color: tan;
--field-highlight: crimson;
}
/* small devices */
@media (max-width: 800px) {
:root {
--field-size: 2.5rem;
--stone-radius: 2rem;
--dice-field-size: 3rem;
--triangle-side-size: 2rem;
}
}
/* smaller devices */
@media (max-width: 400px) {
:root {
--field-size: 2rem;
--stone-radius: 1.5rem;
--dice-field-size: 2rem;
--triangle-side-size: 1.5rem;
}
}
but you can also change them from JavaScript, which I use for blinking the border.
function sleep(time) {
return new Promise((resolve) => setTimeout(resolve, time));
}
async function blinkBorder() {
let root = document.querySelector(':root');
let styles = getComputedStyle(root);
let normalBorderColor = styles.getPropertyValue('--border-color');
let alertBroderColor = styles.getPropertyValue('--alert-border-color');
for (let i = 0; i < 3; i++) {
await sleep(200);
root.style.setProperty('--border-color', alertBroderColor);
await sleep(200);
root.style.setProperty('--border-color', normalBorderColor);
}
}
Also, you can nest blocks in modern CSS, which is very nice.
foo {
...
}
foo bar {
...
}
/* same as */
foo {
...
bar {
...
}
}
The base of the engine is a bunch of global variables. Specifically a map, which holds the paths of each player, positions of the rosettes and the state of the field.
const humanMap = ['2-4', '2-3', '2-2', '2-1', '2-0', '1-0', '1-1', '1-2',
'1-3', '1-4', '1-5', '1-6', '1-7', '2-7', '2-6', '2-5'];
const aiMap = ['0-4', '0-3', '0-2', '0-1', '0-0', '1-0', '1-1', '1-2',
'1-3', '1-4', '1-5', '1-6', '1-7', '0-7', '0-6', '0-5'];
const rosettes = ['0-0', '2-0', '1-3', '0-6', '2-6'];
Each round, dice throws are simulated and all possible moves detected. Then it is up to the players to take their turns, modify the <table> and pass the turn around.
Human interaction is done with a simple pull system. Each field in the <table> is given an 'onclick', which writes its ID in a special variable.
let board = document.getElementById('game-board');
for (let tr of board.lastChild.children) { // has <tbody> for reasons
for (let td of tr.children) {
td.addEventListener('click', () => {
lastClickedField = td.id;
})
}
}
Then, a 'pull' function sets the variable to nothing and waits, until it changes.
async function pullField() {
lastClickedField = null;
while (lastClickedField == null) {
await sleep(100);
}
return lastClickedField;
}
Pulling is repeated until a valid move is chosen.
The AI is very primitive. It tries every possible move and chooses the one which would end up in a highest evaluated board. Board is evaluated as follows:
It is as simple as AIs get, but it's also a very simple game, so no need to complicate things.
And this is how it works...
Mostly...
For more info, there is the GitHub repo, but you should be able to just press '<F12>' and look for answers there. (or '<C-S>' to save the page!)