Source: load_and_save.js

// Saving and loading game state (from cookies)
// We opted for naive and inefficient saving for simplicty
// though interface flexibility allows to easily replace it

/**
 * Set's a cookie
 * 
 * @param {string} name Cookie name
 * @param {string} value Cookie value
 */
function set_cookie(name, value) {
  Cookies.set(name, value, { sameSite: 'strict', expires: 10 * 365 })
}

/**
 *  
 * @param {string} name Name of the cookie to get value of
 * @return {string|undefined}
 */
function get_cookie(name) {
  return Cookies.get(name)
}
/**
 * 
 * @param {string} name Name of the cookie to remove
 */
function remove_cookie(name) {
  Cookies.remove(name)
}

/**
 * Saves user acknowledgement of cookies
 */
function accept_cookies() {
  set_cookie("cookies-accepted", "true")
  check_cookies_accepted()
}
/**
 * Displays a message if cookies weren't acknowledged
 */
function check_cookies_accepted() {
  if (get_cookie("cookies-accepted") != undefined)
    document.getElementById("cookies-popup").style.display = "none"
}

/**
 * Removes all cookies related to game state
 */
function reset_game_state() {
  let game_state = load_game_state()
  for (let game of game_state.saved_games)
    remove_cookie(`saved-game: ${game.name}`)

  for (let user_level of game_state.user_levels)
    remove_cookie(`user-level: ${user_level.name}`)

  for (let user_level_game of game_state.user_level_games)
    remove_cookie(`user-level-game: ${user_level_game.name}`)
  remove_cookie('game-state')
}

/**
 * Saves serialized game state elements in cookies
 */
class SaveToCookie {
  save_game_state(game_state, serialized) {
    this.clear_unused_cookies(game_state)
    let cookie_name = `game-state`
    set_cookie(cookie_name, serialized)
  }

  save_game(name, serialized) {
    let cookie_name = `saved-game: ${name}`
    set_cookie(cookie_name, serialized)
  }

  save_user_level(name, serialized) {
    let cookie_name = `user-level: ${name}`
    set_cookie(cookie_name, serialized)
  }

  save_paused_user_level(name, serialized) {
    let cookie_name = `paused-user-level: ${name}`
    set_cookie(cookie_name, serialized)
  }

  clear_unused_cookies(new_game_state) {
    let deserializer = new BasicDeserializer()
    let loader = new LoadFromCookie()
    let serialized = loader.load_game_state()

    if (serialized == undefined) return
    let current_game_state = deserializer.deserialize_game_state(serialized)

    for (let name of current_game_state.saved_games)
      if (new_game_state.saved_games.every(g => g.name != name)) {
        remove_cookie(`saved-game: ${name}`)
      }

    for (let name of current_game_state.user_levels)
      if (new_game_state.user_levels.every(l => l.name != name)) {
        remove_cookie(`user-level: ${name}`)
      }

    for (let name of current_game_state.paused_user_levels)
      if (new_game_state.paused_user_levels.every(l => l.name != name)) {
        remove_cookie(`paused-user-level: ${name}`)
      }

  }
}

/**
 * Loads serialized game state elements from cookies
 */
class LoadFromCookie {
  load_game_state() {
    let cookie_name = `game-state`
    return get_cookie(cookie_name)
  }

  load_game(name) {
    let cookie_name = `saved-game: ${name}`
    return get_cookie(cookie_name)
  }

  load_user_level(name) {
    let cookie_name = `user-level: ${name}`
    return get_cookie(cookie_name)
  }

  load_paused_user_level(name) {
    let cookie_name = `paused-user-level: ${name}`
    return get_cookie(cookie_name)
  }
}

class BasicSerializer {
  serialize_game_state(game_state) {
    let game_state_to_save = new GameState()
    game_state_to_save.ranking = game_state.ranking
    game_state_to_save.next_user_level_index = game_state.next_user_level_index

    // Single cookie has a size limit, so every game and level are stored separately
    game_state_to_save.saved_games = Array.from(game_state.saved_games, e => e.name)
    game_state_to_save.user_levels = Array.from(game_state.user_levels, e => e.name)
    game_state_to_save.paused_user_levels = Array.from(game_state.paused_user_levels, e => e.name)
    return JSON.stringify(game_state_to_save)
  }

  serialize_game(game) {
    return JSON.stringify(game)
  }
  serialize_level(level) {
    return JSON.stringify(level)
  }
}

class BasicDeserializer {
  deserialize_game_state(serialized) {
    return JSON.parse(serialized)
  }

  deserialize_game(serialized) {
    return JSON.parse(serialized)
  }
  deserialize_level(serialized) {
    return JSON.parse(serialized)
  }
}


/** Saves game state using provided serializer and saver
 * 
 * @param {GameState} game_state 
 * @param {Serializer} serializer 
 * @param {Saver} saver 
 */
function save_game_state(
  game_state,
  serializer = new BasicSerializer(),
  saver = new SaveToCookie()) {

  saver.save_game_state(game_state, serializer.serialize_game_state(game_state))
  for (let game of game_state.saved_games)
    saver.save_game(game.name, serializer.serialize_game(game))

  for (let level of game_state.user_levels)
    saver.save_user_level(level.name, serializer.serialize_level(level))

  for (let level of game_state.paused_user_levels)
    saver.save_paused_user_level(level.name, serializer.serialize_level(level))

}
/** Loads game state using provided deserializer and loader
 * 
 * @param {Deserializer} deserializer 
 * @param {Loader} loader 
 */
function load_game_state(
  deserializer = new BasicDeserializer(),
  loader = new LoadFromCookie()) {

  let game_state = new GameState()
  let loaded_state = loader.load_game_state()
  if (loaded_state != undefined) {
    let state = deserializer.deserialize_game_state(loaded_state)
    game_state.ranking = state.ranking
    game_state.next_user_level_index = state.next_user_level_index

    for (let name of state.saved_games)
      game_state.saved_games.push(deserializer.deserialize_game(loader.load_game(name)))

    for (let name of state.user_levels)
      game_state.user_levels.push(deserializer.deserialize_level(loader.load_user_level(name)))

    for (let name of state.paused_user_levels)
      game_state.paused_user_levels.push(deserializer.deserialize_level(loader.load_paused_user_level(name)))
  }

  return game_state
}