Hi all,

        I think it would be useful to automate feedback for tuning campaigns,
particularly wrt. the experience of beginning players.  Naturally, we
need to ask permission and collect the minimal relevant information.

        The following patch logs user behaviour: now I have to write the code
(another thread probably) which uploads previous log files to the
server.

Feedback on C++ (lack-of-) style welcome.

Features:
1) Begs user to allow uploads, asks experience level.
2) Generates a random key as the token to identify each user.
3) Writes files in .../upload/00000001, etc.
4) Log all wins, losses, loads/reloads/starts.  Loads include how much
gold, the level of every unrenamable character, and number of each unit
level > 1.

I think I'm getting some of the data from the wrong places, as it seems
unreliable (eg player gold on loading from snapshot).

Here's a beginner example (I've added some explanatory comments in []):

29:start-campaign=TUTORIAL
29:start-difficulty=NORMAL
29:start-scenario=tutorial
29:start-version=
        [29 seconds in, we start tutorial]
259:victory=tutorial-2
259:victory-turns=7
        [Finish tutorial level 1 in 7 turns 230 secs later]
593:victory=null
593:victory-turns=8
        [Finish second level of tutorial]
596:end-campaign=TUTORIAL
        [Tutorial complete]
622:start-campaign=CAMPAIGN_HEIR_TO_THE_THRONE
622:start-difficulty=EASY
622:start-scenario=The_Elves_Besieged
622:start-version=           [Start HTTT on easy next]
821:defeat=The_Elves_Besieged
        [We lost HTTT level 1 on EASY]
830:start-campaign=CAMPAIGN_HEIR_TO_THE_THRONE
830:start-difficulty=EASY
830:start-scenario=The_Elves_Besieged
830:start-version=1.1-svn
830:start-turn=3
        [We reload at turn 3]
1222:victory=The_Elves_Besieged
1222:victory-turns=9
        [We won this time, in 9 turns]

Cheers!
Rusty.

Index: src/upload_log.cpp
===================================================================
--- src/upload_log.cpp  (revision 0)
+++ src/upload_log.cpp  (revision 0)
@@ -0,0 +1,149 @@
+/* $Id$ */
+/*
+   Copyright (C) 2005 by Rusty Russell <[EMAIL PROTECTED]>
+   Part of the Battle for Wesnoth Project http://www.wesnoth.org/
+
+   This program is free software; you can redistribute it and/or modify
+   it under the terms of the GNU General Public License.
+   This program is distributed in the hope that it will be useful,
+   but WITHOUT ANY WARRANTY.
+
+   See the COPYING file for more details.
+*/
+#include "global.hpp"
+
+#define GETTEXT_DOMAIN "wesnoth-lib"
+
+#include "filesystem.hpp"
+#include "gettext.hpp"
+#include "preferences.hpp"
+#include "show_dialog.hpp"
+#include "upload_log.hpp"
+#include "serialization/parser.hpp"
+
+#include <vector>
+#include <string>
+#include <iomanip>
+#include <sstream>
+
+namespace upload_log
+{
+scoped_ostream *out;
+
+manager::~manager()
+{
+       delete out;
+}
+
+static std::string new_filename()
+{
+       std::vector<std::string> files;
+       std::stringstream fname;
+       unsigned int num = 1;
+
+       // These are sorted, so we can simply add one to last one.
+       get_files_in_dir(get_upload_dir(), &files);
+
+       if (!files.empty())
+               num = lexical_cast<int>(files.back())+1;
+
+       fname << std::setw(8) << std::setfill('0') << num;
+       return get_upload_dir() + "/" + fname.str();
+}
+
+static void add(const t_string &key, const t_string &value,
+               int time = SDL_GetTicks())
+{
+       if (!preferences::upload_log())
+               return;
+
+       if (!out) {
+               out = new scoped_ostream(ostream_file(new_filename()));
+       }
+       **out << time/1000 << ":" << key << "=" << value << "\n";
+       (**out).flush();
+}
+
+// User finishes a scenario.
+void defeat(const game_state &game)
+{
+       add("defeat", game.scenario);
+}
+
+void victory(const game_state &game, int turns)
+{
+       int now = SDL_GetTicks();
+
+       add("victory", game.scenario, now);
+       add("victory-turns", lexical_cast<std::string>(turns), now);
+}
+
+// User starts a game (may be new campaign or saved).
+void start(game_state &state, const t_string &start)
+{
+       int now = SDL_GetTicks();
+       std::map<std::string, int> tally;
+
+       add("start-campaign", state.campaign_define, now);
+       add("start-difficulty", state.difficulty, now);
+       add("start-scenario", state.scenario, now);
+       add("start-version", state.version, now);
+
+       // FIXME: Assumes first player is "us"; is that valid?
+       player_info &player = state.players.begin()->second;
+       // FIXME: -1000000 for mid-game saves, random number for new 
campaigns...
+       if (player.gold > 0)
+               add("start-gold", lexical_cast<std::string>(player.gold), now);
+       if (!start.empty())
+               add("start-turn", start, now);
+
+       for (std::vector<unit>::iterator i = player.available_units.begin();
+            i != player.available_units.end();
+            ++i) {
+               // Record details for significant units
+               if (i->unrenamable()) {
+                       add("start-unit-special", i->description() + ":"
+                           + lexical_cast<std::string>(i->type().level()));
+               } else if (i->type().level() > 1) {
+                       tally[i->type().id()]++;
+               }
+       }
+
+       for (std::map<std::string, int>::iterator i = tally.begin();
+            i != tally.end();
+            i++) {
+               add("start-unit", i->first + ":" + 
+                   lexical_cast<std::string>(i->second));
+       }
+}
+
+// Campaign ended successfully.
+void end(game_state &state)
+{
+       add("end-campaign", state.campaign_define);
+}
+
+// Append useful information.
+void add_information(const t_string &key, const t_string &value);
+
+void show_beg_dialog(display& disp)
+{
+       int res;
+       std::vector<std::string> experience_options;
+
+       experience_options.push_back(_("I am not used to strategy games"));
+       experience_options.push_back(_("I have played similar strategy games"));
+       experience_options.push_back(_("I have mastered similar strategy 
games"));
+       experience_options.push_back(_("I have played Wesnoth before"));
+       experience_options.push_back(_("I am an experienced Wesnoth player"));
+
+       res = gui::show_dialog(disp, NULL,
+                              _("Help us make Wesnoth better for you!"),
+                              _("Wesnoth relies on volunteers like yourself.  
We particularly need feedback from beginners and new players, so we can ensure 
Wesnoth is as much fun as possible.  Please allow Wesnoth to upload an 
anonymous log about your gameplay to wesnoth.org for this purpose.\nSelect your 
approximate experience level or select cancel to disable this feature:"),
+                              gui::OK_CANCEL, &experience_options);
+       preferences::set_upload_log(res != -1);
+       if (res != -1)
+               preferences::set_upload_experience_level(res);
+}
+
+};
Index: src/playlevel.cpp
===================================================================
--- src/playlevel.cpp   (revision 9227)
+++ src/playlevel.cpp   (working copy)
@@ -42,6 +42,7 @@
 #include "statistics.hpp"
 #include "tooltips.hpp"
 #include "unit_display.hpp"
+#include "upload_log.hpp"
 #include "util.hpp"
 #include "video.hpp"
 
@@ -610,9 +611,12 @@
                        } catch(end_level_exception&) {
                        }
 
-                       if (!obs)
+                       if (!obs) {
+                               if (state_of_game.campaign_type == "scenario"
+                                       || state_of_game.campaign_type == 
"tutorial")
+                                       upload_log::defeat(state_of_game);
                                return DEFEAT;
-                       else
+                       } else
                                return QUIT;
                } else if (end_level.result == VICTORY || end_level.result == 
LEVEL_CONTINUE ||
                           end_level.result == LEVEL_CONTINUE_NO_SAVE) {
@@ -621,6 +625,12 @@
                        } catch(end_level_exception&) {
                        }
 
+                       if (!obs && end_level.result == VICTORY
+                               && (state_of_game.campaign_type == "scenario"
+                                       || state_of_game.campaign_type == 
"tutorial")) {
+                               upload_log::victory(state_of_game, 
status.turn());
+                       }
+
                        if(state_of_game.scenario == (*level)["id"]) {
                                state_of_game.scenario = 
(*level)["next_scenario"];
                        }
Index: src/filesystem.cpp
===================================================================
--- src/filesystem.cpp  (revision 9227)
+++ src/filesystem.cpp  (working copy)
@@ -283,6 +283,12 @@
        return get_dir(dir_path);
 }
 
+std::string get_upload_dir()
+{
+       const std::string dir_path = get_user_data_dir() + "/upload";
+       return get_dir(dir_path);
+}
+
 std::string get_dir(const std::string& dir_path)
 {
 #ifdef _WIN32
Index: src/upload_log.hpp
===================================================================
--- src/upload_log.hpp  (revision 0)
+++ src/upload_log.hpp  (revision 0)
@@ -0,0 +1,47 @@
+/* $Id$ */
+/*
+   Copyright (C) 2005 by Rusty Russell <[EMAIL PROTECTED]>
+   Part of the Battle for Wesnoth Project http://www.wesnoth.org/
+
+   This program is free software; you can redistribute it and/or modify
+   it under the terms of the GNU General Public License.
+   This program is distributed in the hope that it will be useful,
+   but WITHOUT ANY WARRANTY.
+
+   See the COPYING file for more details.
+*/
+
+#ifndef UPLOAD_LOG_H_INCLUDED
+#define UPLOAD_LOG_H_INCLUDED
+#include "config.hpp"
+#include "display.hpp"
+#include "gamestatus.hpp"
+#include "tstring.hpp"
+#include "unit_types.hpp"
+
+namespace upload_log
+{
+struct manager
+{
+       manager() { };
+       ~manager();
+};
+
+// Please please please upload stats?
+void show_beg_dialog(display& disp);
+
+// User finishes a level.
+void defeat(const game_state &game);
+void victory(const game_state &game, int turn);
+
+// User starts a game (may be new campaign or saved).
+void start(game_state &state, const t_string &start);
+
+// Campaign ended successfully.
+void end(game_state &state);
+
+// Append useful information.
+void add_information(const t_string &key, const t_string &value);
+};
+
+#endif // UPLOAD_LOG_H_INCLUDED
Index: src/titlescreen.cpp
===================================================================
--- src/titlescreen.cpp (revision 9227)
+++ src/titlescreen.cpp (working copy)
@@ -28,6 +28,7 @@
 #include "sdl_utils.hpp"
 #include "show_dialog.hpp"
 #include "titlescreen.hpp"
+#include "upload_log.hpp"
 #include "util.hpp"
 #include "video.hpp"
 #include "serialization/parser.hpp"
@@ -289,6 +290,9 @@
 
        update_whole_screen();
 
+       if (!preferences::upload_log_defined())
+               return BEG_FOR_UPLOAD;
+
        LOG_DP << "entering interactive loop...\n";
 
        for(;;) {
Index: src/Makefile.am
===================================================================
--- src/Makefile.am     (revision 9227)
+++ src/Makefile.am     (working copy)
@@ -102,6 +102,7 @@
        unit.cpp \
        unit_display.cpp \
        unit_types.cpp \
+       upload_log.cpp \
        variable.cpp \
        video.cpp \
        serialization/binary_or_text.cpp \
Index: src/playcampaign.cpp
===================================================================
--- src/playcampaign.cpp        (revision 9227)
+++ src/playcampaign.cpp        (working copy)
@@ -27,6 +27,7 @@
 #include "dialogs.hpp"
 #include "gettext.hpp"
 #include "game_errors.hpp"
+#include "upload_log.hpp"
 #include "wassert.hpp"
 
 #define LOG_G LOG_STREAM(info, general)
@@ -152,6 +153,8 @@
                scenario = &starting_pos;
                state = read_game(units_data, &state.snapshot);
        }
+       if (type == "scenario" || type == "tutorial")
+               upload_log::start(state, starting_pos["turn_at"]);
 
        controller_map controllers;
 
Index: src/game.cpp
===================================================================
--- src/game.cpp        (revision 9227)
+++ src/game.cpp        (working copy)
@@ -51,6 +51,7 @@
 #include "titlescreen.hpp"
 #include "util.hpp"
 #include "unit_types.hpp"
+#include "upload_log.hpp"
 #include "unit.hpp"
 #include "video.hpp"
 #include "wassert.hpp"
@@ -106,6 +107,7 @@
        bool change_language();
 
        void show_preferences();
+       void show_upload_begging();
 
        enum RELOAD_GAME_DATA { RELOAD_DATA, NO_RELOAD_DATA };
        void play_game(RELOAD_GAME_DATA reload=RELOAD_DATA);
@@ -139,6 +141,7 @@
        const image::manager image_manager_;
        const events::event_context main_event_context_;
        const hotkey::manager hotkey_manager_;
+       const upload_log::manager upload_log_manager_;
        binary_paths_manager paths_manager_;
 
        bool test_mode_, multiplayer_mode_, no_gui_;
@@ -1242,6 +1245,13 @@
        disp().redraw_everything();
 }
 
+void game_controller::show_upload_begging()
+{
+       upload_log::show_beg_dialog(disp());
+
+       disp().redraw_everything();
+}
+
 //this function reads the game configuration, searching for valid cached 
copies first
 void game_controller::read_game_cfg(const preproc_map& defines, config& cfg, 
bool use_cache)
 {
@@ -1458,6 +1468,7 @@
                // don't show The End for multiplayer scenario
                // change this if MP campaigns are implemented
                if(result == VICTORY && (state_.campaign_type.empty() || 
state_.campaign_type != "multiplayer")) {
+                       upload_log::end(state_);
                        the_end(disp());
                        about::show_about(disp());
                }
@@ -1731,6 +1742,9 @@
                } else if(res == gui::SHOW_ABOUT) {
                        about::show_about(game.disp());
                        continue;
+               } else if(res == gui::BEG_FOR_UPLOAD) {
+                       game.show_upload_begging();
+                       continue;
                }
 
                if (recorder.at_end()){
Index: src/filesystem.hpp
===================================================================
--- src/filesystem.hpp  (revision 9227)
+++ src/filesystem.hpp  (working copy)
@@ -51,6 +51,7 @@
 std::string get_cache_dir();
 std::string get_intl_dir();
 std::string get_screenshot_dir();
+std::string get_upload_dir();
 std::string get_user_data_dir();
 
 std::string get_cwd();
Index: src/preferences.cpp
===================================================================
--- src/preferences.cpp (revision 9227)
+++ src/preferences.cpp (working copy)
@@ -769,6 +769,38 @@
        fps = value;
 }
 
+bool upload_log()
+{
+       if (!upload_log_defined())
+               return false;
+       return prefs["upload_log"] == "yes";
+}
+
+bool upload_log_defined()
+{
+       const string_map::const_iterator i = prefs.values.find("upload_log");
+       return (i != prefs.values.end());
+}
+
+void set_upload_experience_level(int level)
+{
+       prefs["upload_experience"] = lexical_cast<std::string>(level);
+}
+
+void set_upload_log(bool value)
+{
+       prefs["upload_log"] = value ? "yes" : "no";
+
+       // We create a unique id for each person.
+       if (prefs.values.find("upload_id") == prefs.values.end()) {
+               // SDL_GetTicks() is a good value, as it depends on when they 
clicked
+               srand(time(NULL));
+               prefs["upload_id"]
+                       = lexical_cast<std::string>(rand())
+                       + lexical_cast<std::string>(SDL_GetTicks());
+       }
+}
+
 bool compress_saves()
 {
        return prefs["compress_saves"] != "no";
Index: src/titlescreen.hpp
===================================================================
--- src/titlescreen.hpp (revision 9227)
+++ src/titlescreen.hpp (working copy)
@@ -20,7 +20,7 @@
 namespace gui {
 
 enum TITLE_RESULT { TUTORIAL = 0, NEW_CAMPAIGN, MULTIPLAYER, LOAD_GAME,
-                    CHANGE_LANGUAGE, EDIT_PREFERENCES, SHOW_ABOUT, QUIT_GAME, 
TITLE_CONTINUE };
+                    CHANGE_LANGUAGE, EDIT_PREFERENCES, SHOW_ABOUT, QUIT_GAME, 
TITLE_CONTINUE, BEG_FOR_UPLOAD };
 
 TITLE_RESULT show_title(display& screen, config& tips_of_day, int* ntip);
 
Index: src/preferences_display.cpp
===================================================================
--- src/preferences_display.cpp (revision 9227)
+++ src/preferences_display.cpp (working copy)
@@ -21,6 +21,7 @@
 #include "marked-up_text.hpp"
 #include "preferences_display.hpp"
 #include "show_dialog.hpp"
+#include "upload_log.hpp"
 #include "video.hpp"
 #include "wml_separators.hpp"
 #include "widgets/button.hpp"
@@ -192,6 +193,7 @@
 
        gui::slider music_slider_, sound_slider_, scroll_slider_, 
gamma_slider_, chat_lines_slider_;
        gui::button fullscreen_button_, turbo_button_, show_ai_moves_button_,
+                       upload_log_button_,
                    show_grid_button_, show_lobby_joins_button_, 
show_floating_labels_button_, turn_dialog_button_,
                    turn_bell_button_, show_team_colours_button_, 
show_colour_cursors_button_,
                    show_haloing_button_, video_mode_button_, theme_button_, 
hotkeys_button_, gamma_button_,
@@ -214,6 +216,7 @@
          scroll_slider_(disp.video()), gamma_slider_(disp.video()), 
chat_lines_slider_(disp.video()),
          fullscreen_button_(disp.video(), _("Toggle Full Screen"), 
gui::button::TYPE_CHECK),
          turbo_button_(disp.video(), _("Accelerated Speed"), 
gui::button::TYPE_CHECK),
+         upload_log_button_(disp.video(), _("Upload Log"), 
gui::button::TYPE_CHECK),
          show_ai_moves_button_(disp.video(), _("Skip AI Moves"), 
gui::button::TYPE_CHECK),
          show_grid_button_(disp.video(), _("Show Grid"), 
gui::button::TYPE_CHECK),
          show_lobby_joins_button_(disp.video(), _("Show lobby joins"), 
gui::button::TYPE_CHECK),
@@ -289,6 +292,9 @@
        turbo_button_.set_check(turbo());
        turbo_button_.set_help_string(_("Make units move and fight faster"));
 
+       upload_log_button_.set_check(preferences::upload_log());
+       upload_log_button_.set_help_string(_("Help Wesnoth improve by sending 
statistics on games to developers"));
+
        show_ai_moves_button_.set_check(!show_ai_moves());
        show_ai_moves_button_.set_help_string(_("Do not animate AI units 
moving"));
 
@@ -344,6 +350,7 @@
        SDL_Rect scroll_rect = { rect.x + scroll_label_.width(), ypos,
                                 rect.w - scroll_label_.width() - border, 0 };
        scroll_slider_.set_location(scroll_rect);
+       ypos += item_interline; upload_log_button_.set_location(rect.x, ypos);
        ypos += item_interline; turbo_button_.set_location(rect.x, ypos);
        ypos += item_interline; show_ai_moves_button_.set_location(rect.x, 
ypos);
        ypos += item_interline; turn_dialog_button_.set_location(rect.x, ypos);
@@ -413,6 +420,8 @@
 {
        if (turbo_button_.pressed())
                set_turbo(turbo_button_.checked());
+       if (upload_log_button_.pressed())
+               preferences::set_upload_log(upload_log_button_.checked());
        if (show_ai_moves_button_.pressed())
                set_show_ai_moves(!show_ai_moves_button_.checked());
        if (show_grid_button_.pressed())
@@ -546,6 +555,7 @@
        scroll_label_.hide(hide_general);
        scroll_slider_.hide(hide_general);
        turbo_button_.hide(hide_general);
+       upload_log_button_.hide(hide_general);
        show_ai_moves_button_.hide(hide_general);
        turn_dialog_button_.hide(hide_general);
        turn_bell_button_.hide(hide_general);
Index: src/preferences.hpp
===================================================================
--- src/preferences.hpp (revision 9227)
+++ src/preferences.hpp (working copy)
@@ -178,6 +178,11 @@
        bool flip_time();
        void set_flip_time(bool value);
 
+       bool upload_log();
+       bool upload_log_defined();
+       void set_upload_log(bool value);
+       void set_upload_experience_level(int level);
+
        // Multiplayer functions
        bool chat_timestamp();
        void set_chat_timestamp(bool value);

-- 
 ccontrol: http://ozlabs.org/~rusty/ccontrol


Reply via email to