18. Jan. 2023, 17:00-18:30
Still missing functionality:
to be continued next session
We already start a new game when the page is loading:
$( document ).ready(function () {
init()
})
So let's do the same thing, when someone clicks the "start new" button:
$( document ).ready(function () {
init()
$( '#button-start-new' ).on('click', init)
})
function reset () {
// just hide and unsolve all items no matter what there previous state was
for (let row=0; row<4; row++) {
for (let col=0; col<6; col++) {
gameState.grid[row][col].hidden = true
gameState.grid[row][col].solved = false
}
}
$( '.card' ).children().hide()
}
And of course we also have to add an event listener to the reset button:
$( document ).ready(function () {
init()
$( '#button-start-new' ).on('click', init)
$( '#button-restart' ).on('click', reset)
})
Lets start with two empty functions as event handlers for the two buttons:
function toggleFavourite () {
console.log('toggling favourite: not yet implemented')
}
function startFavourite () {
console.log('starting favourite: not yet implemented')
}
$( document ).ready(function () {
init()
$( '#button-start-new' ).on('click', init)
$( '#button-restart' ).on('click', reset)
$( '#button-favourite' ).on('click', toggleFavourite)
$( '#button-start-favourite' ).on('click', startFavourite)
})
Let's first implement the toggleFavourite function. And also create a helper function so we can check if the current game setup has the same items as our favourite:
function currentGameIsFavourite () {
// if no favourite is set yet, we can already return false
if (!gameState.favourite) return false
// now check for every item in the current item set if it occurs in favourite
// if all items are found, we have the same game. otherwise we can return false
for (let i in currentGameItems) {
if (!gameState.favourite.includes(currentGameItems[i])) return false
}
return true
}
function toggleFavourite () {
if ( currentGameIsFavourite() ) {
gameState.favourite = false
$( '#button-favourite' ).removeClass('highlight-green')
} else {
gameState.favourite = currentGameItems
$( '#button-favourite' ).addClass('highlight-green')
}
}
We also have to add this CSS class to our style file:
.highlight-green {
background-color: lightgreen;
}
Now we have to add something to the init function! Because after every new game is generated we have to check if it coincidentally has the same items as (and therefore is) our favourite.
So in the init() function add the end let's add a check:
// now create the memory cards from this grid
createCards(gameState.grid)
if ( currentGameIsFavourite() ) {
$( '#button-favourite' ).addClass('highlight-green')
} else {
$( '#button-favourite' ).removeClass('highlight-green')
}
gameState.status = 'initialised'
For the
feature we almost have to do the same as in the init function,
except for the initial filling of the
currentGameItems
.
So lets try to be DRY(
Don't Repeat Yourself
) and refactor the init() first:
function init () {
// create new random array of current game items
let memoryPool = [...memoryItemPool]
currentGameItems = []
while (currentGameItems.length < 12) {
let i = randInt(memoryPool.length)
let [item] = memoryPool.splice(i, 1)
currentGameItems.push(item)
}
// Now run the stage 2 of initialisation
// (which is the same for a new as well as favourite games)
initStage2()
}
function initStage2 () {
// create a grid with pairs of game items
let currentItemPool = [...currentGameItems]
currentItemPool.push(...currentGameItems)
gameState.grid = []
for (let row=0; row<4; row++) {
gameState.grid.push([])
for (let col=0; col<6; col++) {
let i = randInt(currentItemPool.length)
let [item] = currentItemPool.splice(i, 1)
gameState.grid[row].push({
item: item,
hidden: true,
solved: false,
})
}
}
// now create the memory cards from this grid
createCards(gameState.grid)
if ( currentGameIsFavourite() ) {
$( '#button-favourite' ).addClass('highlight-green')
} else {
$( '#button-favourite' ).removeClass('highlight-green')
}
gameState.status = 'initialised'
}
Now the only thing left to do for the
startFavourite()
is:
function startFavourite () {
// just create the currentGameItems as a copy from our favourites and then
// continue with stage 2 of the initialisation process
currentGameItems = [...gameState.favourite]
initStage2()
}
A general word of guidance: Use the console, young Codawan!
// we can do all the things we do in the script also live on the console:
$( '.card' ).children().toggle()
Let's add two little helper functions we can use on the console quickly:
// the following functions are just helping us in debugging
// tac: toggle all cards
function tac () {
$( '.card' ).children().toggle()
}
// lac: log all cards
function lac () {
console.log("Current game grid:")
for (let i in gameState.grid) {
let line = ''
for (let k in gameState.grid[i]) {
line += gameState.grid[i][k].item + ' '
}
console.log(line)
}
}
We'll leave the hearts out in this version (similar to difficulty). But we'll add also counters for games started and completed, and an average for how many cards have been uncovered per game.
So let's change the stats object in the gameState from:
stats: {
health: 0,
count: 0,
},
to:
stats: {
currentCount: 0,
gamesStarted: 0,
gamesCompleted: 0,
countPerCompleted: 0,
},
Before we update the stats themselves, let's create a function to update the actual display of these stats.
function renderStats () {
$stats = $( '#stats' )
$stats.children().remove()
$stats.append(
$( `Cards opened in the current game: ${gameState.stats.currentCount}` )
)
$stats.append(
$( `Games started: ${gameState.stats.gamesStarted}` )
)
$stats.append(
$( `Games completed: ${gameState.stats.gamesCompleted}` )
)
$stats.append(
$( `Average cards opened in all completed games: ${gameState.stats.countPerCompleted}` )
)
}
Now let's update and rerender the stats after a card is opened.
We have to add some code at the right place in the
clickMemoryCard
function.
if ( gameState.grid[row][col].hidden ) {
gameState.grid[row][col].hidden = false
// reveal the card slowly and afterwards process the grid
$card.children().show( 'slow', processGrid )
// increase the current stat counter and rerender stats
gameState.stats.currentCount++
renderStats()
}
Do the same for games started, at the end of the
initStage2
function:
gameState.status = 'initialised'
gameState.stats.gamesStarted++
gameState.stats.currentCount = 0
renderStats()
Now for the completed games. We have to check in the
processGrid
function, after a pair was
found, if all the cards have been uncovered and update the game
state accordingly.
if (openUnsolved[0].card.item === openUnsolved[1].card.item) {
openUnsolved[0].card.solved = true
openUnsolved[1].card.solved = true
gameState.processing = false
// if all cards are now opened, the game is completed
if ( isGameCompleted() ) {
gameState.status = 'done'
let stats = gameState.stats // just to make the next lines a bit shorter
let totalClicks = stats.countPerCompleted * stats.gamesCompleted + stats.currentCount
stats.gamesCompleted++
stats.countPerCompleted = totalClicks / stats.gamesCompleted
renderStats()
}
return
}
We still need to implement the
isGameCompleted
function.
function isGameCompleted () {
//if (gameState.stats.currentCount > 3) return true // use this line for debugging
// the game is completed, if all cards on the grid have set solved to true
for (let x in gameState.grid) {
for (let y in gameState.grid[x]) {
if (!gameState.grid[x][y].solved) return false
}
}
return true
}
A bug was found when starting a favourite game. This only came up when testing the gamesStarted counter. What if the user clicks on but there is no favourite set?
Let's do a simple fix for now
function startFavourite () {
// if no fave is set, just do a simple alert for a start
if (!gameState.favourite) {
alert('You did not set a favourite game.')
return
}
// just create the currentGameItems as a copy from our favourites and then
// continue with stage 2 of the initialisation process
currentGameItems = [...gameState.favourite]
initStage2()
}
In order to not loose our game progress, when we close the browser window, we can facilite localStorage.
Besides clearing the whole storage and removing single items (check the link above for details), we can set and get items in the web browser storage for this page like that:
localStorage.setItem('aFunnyVar', 'holds some random content here')
const funnyVar = localStorage.getItem('aFunnyVar')
Use the "Storage" section in you web dev tools to inspect your "Local Storage"
But we can only store strings in localStorage. It would be more
convenient to be able to just store our gameState
object.
Well ... JSON FTW! Specifically the
JSON.stringify()
and
JSON.parse()
functions.
// try it out in the console of an opened game
let serializedGameState = JSON.stringify(gameState)
console.log(serializedGameState)
// and to unserialize (or unstringify):
let newGameState = JSON.parse(serializedGameState)
console.log(newGameState)
With that, storing and loading the gameState is easy as that:
function storeState () {
localStorage.setItem('gameState', JSON.stringify(gameState))
}
function loadState () {
gameState = JSON.parse(localStorage.getItem('gameState'))
}
But when we execute the loadState function, we'll get an error, because our gameState is defined as a constant. And here we are not only changing the contents of the gameState object, but the object itself. So let's change
const gameState = {
to:
let gameState = {
Now we only have to store the game state, whenever anything relevant happens. In our case:
While the advanced version would be to write setters and getters
for our gameState object, which handles storing automagically,
the quick fix is to add storeState()
to
the end of the reset
function and
somewhere in the renderState
function.
And when we load the page, and there is already a stored gameState, we'll have to skip initialisation and only create the actual HTML cards accordingly.
First, we'll update our document-ready function:
$( document ).ready(function () {
if (!localStorage.getItem('gameState')) {
init()
} else {
loadState()
}
$( '#button-start-new' ).on('click', init)
$( '#button-restart' ).on('click', reset)
$( '#button-favourite' ).on('click', toggleFavourite)
$( '#button-start-favourite' ).on('click', startFavourite)
})
Now we have to extend the loadState function to handle rerendering:
function loadState () {
gameState = JSON.parse(localStorage.getItem('gameState'))
createCards(gameState.grid)
// as all cards are created hidden by default, show all those cards that are already opened
for (let x in gameState.grid) {
for (let y in gameState.grid[x]) {
if (!gameState.grid[x][y].hidden) {
$( '#cards' ).children().eq(x).children().eq(y).children().show()
}
}
}
renderStats()
}
Well, there is still a little bug with the favourites, because we loose the currentGameItems on a reload. Because they are not part of the gameState. But this could be changed with some refactoring.
But ...
I think we are good for now
Still missing from the initial concept:
You could:
Minimal effort for this exercise: some styling
If you want to go for substantial extensions and changes, this can already count towards your final project.