From 755cf7088a9c95b8e26829c6c0cab43369d34422 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Godin?= Date: Thu, 17 Apr 2025 01:45:31 +0200 Subject: [PATCH] Save game state and restore it from saved file --- src/game-save.vala | 143 ++++++++++++++++++++++++++++++++++++++++ src/game.vala | 37 ++++++++++- src/gnome-mahjongg.vala | 80 +++++++++++++++++++--- src/map.vala | 4 ++ src/meson.build | 1 + 5 files changed, 253 insertions(+), 12 deletions(-) create mode 100644 src/game-save.vala diff --git a/src/game-save.vala b/src/game-save.vala new file mode 100644 index 0000000..02c455b --- /dev/null +++ b/src/game-save.vala @@ -0,0 +1,143 @@ +// SPDX-FileCopyrightText: 2010-2025 Mahjongg Contributors +// SPDX-FileCopyrightText: 2010-2013 Robert Ancell +// SPDX-License-Identifier: GPL-2.0-or-later + +public class GameSave { + public string filename; + public string map_name; + public double elapsed_time; + public int move_number; + public int32 seed; + public List tiles; + + public GameSave (string filename) { + this.filename = filename; + } + + public void load () throws Error { + string data; + size_t length; + FileUtils.get_contents (filename, out data, out length); + + var parser = MarkupParser (); + + parser.start_element = start_element_cb; + parser.text = null; + parser.passthrough = null; + parser.error = null; + + var parse_context = new MarkupParseContext (parser, 0, this, null); + try { + parse_context.parse (data, (ssize_t) length); + } + catch (MarkupError e) { + } + } + + public void write (Game game) { + StringBuilder builder = new StringBuilder (); + + builder.append_printf ("\n", + game.map.name, + game.elapsed.to_string (), + game.current_move_number, + game.seed + ); + + builder.append ("\t\n"); + + foreach (unowned var tile in game.tiles) { + builder.append_printf ( + "\t\t\n", + tile.number, + tile.visible ? "true" : "false", + tile.move_number, + tile.slot.x, + tile.slot.y, + tile.slot.layer + ); + } + + builder.append ("\t\n"); + builder.append ("\n"); + + try { + DirUtils.create_with_parents (Path.get_dirname (filename), 0775); + FileUtils.set_contents (filename, builder.str); + } + catch (FileError e) { + warning ("Failed to save the game : %s", e.message); + } + } + + public bool exists () { + return FileUtils.test (filename, FileTest.EXISTS); + } + + public void delete () { + if (!exists ()) + return; + + int result = FileUtils.remove (filename); + + if (result == -1) + warning ("Failed to remove the save file."); + + map_name = ""; + elapsed_time = 0.0; + move_number = 0; + seed = 0; + tiles = new List (); + } + + private string? get_attribute (string[] attribute_names, string[] attribute_values, string name, + string? default = null) { + for (var i = 0; attribute_names[i] != null; i++) { + if (attribute_names[i].down () == name) + return attribute_values[i]; + } + + return default; + } + + private double get_attribute_d (string[] attribute_names, string[] attribute_values, string name, + double default = 0.0) { + var a = get_attribute (attribute_names, attribute_values, name); + if (a == null) + return default; + else + return double.parse (a); + } + + private void start_element_cb (MarkupParseContext context, string element_name, string[] attribute_names, + string[] attribute_values) throws MarkupError { + /* Identify the tag. */ + switch (element_name.down ()) { + case "game": + map_name = get_attribute (attribute_names, attribute_values, "map_name", ""); + elapsed_time = get_attribute_d (attribute_names, attribute_values, "elapsed_time"); + move_number = (int) get_attribute_d (attribute_names, attribute_values, "move_number"); + seed = (int) get_attribute_d (attribute_names, attribute_values, "seed"); + break; + + case "tile": + int tile_number = (int) (get_attribute_d (attribute_names, attribute_values, "number")); + bool visible = get_attribute (attribute_names, attribute_values, "visible") == "true"; + int move_number = (int) (get_attribute_d (attribute_names, attribute_values, "move_number")); + int x = (int) (get_attribute_d (attribute_names, attribute_values, "x")); + int y = (int) (get_attribute_d (attribute_names, attribute_values, "y")); + int layer = (int) (get_attribute_d (attribute_names, attribute_values, "layer")); + + Slot slot = new Slot (x, y, layer); + + Tile tile = new Tile (slot) { + number = tile_number, + visible = visible, + move_number = move_number + }; + + tiles.append (tile); + break; + } + } +} diff --git a/src/game.vala b/src/game.vala index cc265f2..e806f21 100644 --- a/src/game.vala +++ b/src/game.vala @@ -86,7 +86,6 @@ public class Game { public bool inspecting; private Rand random; - private int32 seed; private int move_number; private Tile? hint_tiles[2]; @@ -107,6 +106,8 @@ public class Game { public signal void paused_changed (); public signal void tick (); + public int32 seed { get; set; } + public bool started { get { return clock != null; } } @@ -156,6 +157,10 @@ public class Game { } } + public int current_move_number { + get { return move_number; } + } + public uint moves_left { get { return find_matches ().length; } } @@ -209,13 +214,19 @@ public class Game { } } - public Game (Map map, int32 seed) { + public Game (Map map, GameSave? save = null) { this.map = map; - this.seed = seed; /* Create blank tiles in the locations required in the map */ create_tiles (); + if (save != null) { + restore_game (save); + return; + } + + this.seed = Random.int_range (0, int32.MAX); + /* Start with a board consisting of visible blank tiles. Walk through all * tiles, choosing and removing tile pairs until we have a solvable board. * @@ -465,6 +476,26 @@ public class Game { redraw_all_tiles (); } + private void restore_game (GameSave save) { + move_number = save.move_number; + clock_elapsed = save.elapsed_time; + seed = save.seed; + + foreach (unowned var tile in tiles) { + foreach (unowned var t in save.tiles) { + if (tile.slot.equals (t.slot)) { + tile.number = t.number; + tile.move_number = t.move_number; + tile.visible = t.visible; + } + } + } + + redraw_all_tiles (); + clock = new Timer (); + paused = true; + } + private unowned int[] shuffle_pair_numbers (int[] pair_numbers) { /* Fisher-Yates Shuffle */ for (var i = 0; i < pair_numbers.length; i++) { diff --git a/src/gnome-mahjongg.vala b/src/gnome-mahjongg.vala index d786a41..6ff9071 100644 --- a/src/gnome-mahjongg.vala +++ b/src/gnome-mahjongg.vala @@ -4,6 +4,7 @@ public class Mahjongg : Adw.Application { private History history; + private GameSave game_save; private List maps; private Settings settings; @@ -101,7 +102,15 @@ public class Mahjongg : Adw.Application { settings.bind ("window-is-maximized", window, "maximized", SettingsBindFlags.DEFAULT); var rotate_map = (layout_rotation == "random"); - new_game (rotate_map); + + var save_path = Path.build_filename (Environment.get_user_data_dir (), "gnome-mahjongg", "gamesave"); + game_save = new GameSave (save_path); + + if (game_save.exists ()) { + restore_game (rotate_map); + } else { + new_game (rotate_map); + } settings.changed.connect (conf_value_changed_cb); } @@ -128,6 +137,9 @@ public class Mahjongg : Adw.Application { if (game_view != null) game_view.game.destroy_timers (); + if (game_view.game.started && game_view.game.can_move && !game_view.game.inspecting) + game_save.write (game_view.game); + settings.apply (); base.shutdown (); } @@ -197,6 +209,7 @@ public class Mahjongg : Adw.Application { var completed_entry = new HistoryEntry (date, game_view.game.map.score_name, duration, player); history.add (completed_entry); history.save (); + game_save.delete (); game_view.game.inspecting = true; show_scores (completed_entry.name, completed_entry); } @@ -228,6 +241,7 @@ public class Mahjongg : Adw.Application { new_game (); break; case "quit": + game_save.delete (); window.destroy (); break; default: @@ -377,23 +391,28 @@ Copyright © 1998–2008 Free Software Foundation, Inc.""", private void restart_game () { game_view.game.restart (); + game_save.delete (); if (game_view.game.paused) pause_cb (); update_ui (); } - private unowned Map find_map () { + private unowned Map? find_map_by_name (string name) { foreach (unowned var m in maps) { - if (m.name == settings.get_string ("mapset")) { + if (m.name == name) { return m; } } - // Map wasn't found. Return the default (first) map. - return maps.nth_data (0); + return null; } private unowned Map get_next_map (bool rotate_map) { - unowned var map = find_map (); + unowned var map = find_map_by_name (settings.get_string ("mapset")); + + // Map wasn't found. Get the default (first) map. + if (map == null) + map = maps.nth_data (0); + if (rotate_map) { switch (settings.get_string ("map-rotation")) { case "sequential": @@ -445,17 +464,60 @@ Copyright © 1998–2008 Free Software Foundation, Inc.""", * map according to the ``map-rotation`` setting. */ private void new_game (bool rotate_map = true) { + game_save.delete (); + start_game (rotate_map, get_next_map (rotate_map)); + } + + private void restore_game (bool rotate_map = true) { + try { + game_save.load (); + } + catch (Error e) { + warning ("Could not load game save %s: %s\n", game_save.filename, e.message); + start_game (rotate_map, get_next_map (rotate_map)); + return; + } + + Map map = find_map_by_name (game_save.map_name); + + if (map == null) { + warning ("Map '%s' not found in available maps.\n", game_save.map_name); + start_game (rotate_map, get_next_map (rotate_map)); + return; + } + + var match_tiles_counter = 0; + foreach (unowned var slot in map.slots) { + foreach (unowned var tile in game_save.tiles) { + if (slot.equals (tile.slot)) { + match_tiles_counter++; + break; + } + } + } + + bool use_game_save = (map.slots.length () == match_tiles_counter); + + start_game (rotate_map, map, use_game_save); + } + + private void start_game (bool rotate_map, Map map, bool use_game_save = false) { new_game_view (rotate_map); - var seed = Random.int_range (0, int32.MAX); - game_view.game = new Game (get_next_map (rotate_map), seed); + if (use_game_save) { + game_view.game = new Game (map, game_save); + window.pause (); + } else { + game_view.game = new Game (map); + window.unpause (); + } + game_view.game.attempt_move.connect (attempt_move_cb); game_view.game.moved.connect (moved_cb); game_view.game.tick.connect (tick_cb); tick_cb (); update_ui (); - window.unpause (); } private void tick_cb () { diff --git a/src/map.vala b/src/map.vala index 3e5fac1..ba1e228 100644 --- a/src/map.vala +++ b/src/map.vala @@ -12,6 +12,10 @@ public class Slot { this.y = y; this.layer = layer; } + + public bool equals (Slot b) { + return this.x == b.x && this.y == b.y && this.layer == b.layer; + } } private static int compare_slots (Slot a, Slot b) { diff --git a/src/meson.build b/src/meson.build index cdf062e..827bfa7 100644 --- a/src/meson.build +++ b/src/meson.build @@ -9,6 +9,7 @@ gnome_mahjongg = executable( 'game-view.vala', 'gnome-mahjongg.vala', 'history.vala', + 'game-save.vala', 'map.vala', 'score-dialog.vala', 'window.vala', -- GitLab