diff --git a/libraries/app/database_api.cpp b/libraries/app/database_api.cpp index 811aa446..b8c0c280 100644 --- a/libraries/app/database_api.cpp +++ b/libraries/app/database_api.cpp @@ -1932,7 +1932,8 @@ void database_api_impl::on_objects_changed(const vector& ids) /// if a connection hangs then this could get backed up and result in /// a failure to exit cleanly. fc::async([capture_this,this,updates,market_broadcast_queue](){ - if( _subscribe_callback ) _subscribe_callback( updates ); + if( _subscribe_callback ) + _subscribe_callback( updates ); for( const auto& item : market_broadcast_queue ) { diff --git a/libraries/app/impacted.cpp b/libraries/app/impacted.cpp index b7e3a3d6..7cfbafe8 100644 --- a/libraries/app/impacted.cpp +++ b/libraries/app/impacted.cpp @@ -212,6 +212,10 @@ struct get_impacted_account_visitor _impacted.insert( op.payer_account_id ); _impacted.insert( op.player_account_id ); } + void operator()( const game_move_operation& op ) + { + _impacted.insert( op.player_account_id ); + } }; diff --git a/libraries/chain/db_init.cpp b/libraries/chain/db_init.cpp index 80005410..2f9c3b8f 100644 --- a/libraries/chain/db_init.cpp +++ b/libraries/chain/db_init.cpp @@ -177,6 +177,7 @@ void database::initialize_evaluators() register_evaluator(); register_evaluator(); register_evaluator(); + register_evaluator(); } void database::initialize_indexes() diff --git a/libraries/chain/game_object.cpp b/libraries/chain/game_object.cpp index b13e9410..4031890f 100644 --- a/libraries/chain/game_object.cpp +++ b/libraries/chain/game_object.cpp @@ -23,6 +23,8 @@ */ #include #include +#include +#include #include #include @@ -48,11 +50,13 @@ namespace graphene { namespace chain { {} }; - struct game_complete + struct game_move { database& db; - game_id_type game_id; - game_complete(database& db, game_id_type game_id) : db(db), game_id(game_id) {}; + const game_move_operation& move; + game_move(database& db, const game_move_operation& move) : + db(db), move(move) + {} }; struct game_state_machine_ : public msm::front::state_machine_def @@ -62,53 +66,129 @@ namespace graphene { namespace chain { typedef int no_message_queue; // States - struct waiting_on_previous_gamees : public msm::front::state<>{}; - struct game_in_progress : public msm::front::state<> + struct waiting_for_game_to_start : public msm::front::state<> {}; + struct expecting_commit_moves : public msm::front::state<> { void on_entry(const initiate_game& event, game_state_machine_& fsm) { game_object& game = *fsm.game_obj; fc_ilog(fc::logger::get("tournament"), - "game ${id} is now in progress", + "game ${id} is now in progress, expecting commit moves", ("id", game.id)); + fc_ilog(fc::logger::get("tournament"), + "game ${id} is associtated with match ${match_id}", + ("id", game.id) + ("match_id", game.match_id)); + } + void on_entry(const game_move& event, game_state_machine_& fsm) + { + game_object& game = *fsm.game_obj; + + fc_ilog(fc::logger::get("tournament"), + "game ${id} received a commit move, still expecting another commit move", + ("id", game.id)); + } + }; + struct expecting_reveal_moves : public msm::front::state<> + { + void on_entry(const game_move& event, game_state_machine_& fsm) + { + game_object& game = *fsm.game_obj; + + if (event.move.move.which() == game_specific_moves::tag::value) + fc_ilog(fc::logger::get("tournament"), + "game ${id} received a commit move, now expecting reveal moves", + ("id", game.id)); + else + fc_ilog(fc::logger::get("tournament"), + "game ${id} received a reveal move, still expecting reveal moves", + ("id", game.id)); } }; struct game_complete : public msm::front::state<> { - void on_entry(const game_complete& event, game_state_machine_& fsm) - { - fc_ilog(fc::logger::get("tournament"), - "game ${id} is complete", - ("id", fsm.game_obj->id)); - } - void on_entry(const initiate_game& event, game_state_machine_& fsm) + void on_entry(const game_move& event, game_state_machine_& fsm) { game_object& game = *fsm.game_obj; fc_ilog(fc::logger::get("tournament"), - "game ${id} is complete, it was a buy", - ("id", game)); + "received a reveal move, game ${id} is complete", + ("id", fsm.game_obj->id)); + + // we now know who played what, figure out if we have a winner + const rock_paper_scissors_game_details& game_details = game.game_details.get(); + if (game_details.reveal_moves[0]->gesture == game_details.reveal_moves[1]->gesture) + ilog("The game was a tie, both players threw ${gesture}", ("gesture", game_details.reveal_moves[0]->gesture)); + else + { + const match_object& match_obj = game.match_id(event.db); + const tournament_object& tournament_obj = match_obj.tournament_id(event.db); + const rock_paper_scissors_game_options& game_options = tournament_obj.options.game_options.get(); + + unsigned winner = ((((int)game_details.reveal_moves[0]->gesture - + (int)game_details.reveal_moves[1]->gesture + + game_options.number_of_gestures) % game_options.number_of_gestures) + 1) % 2; + ilog("${gesture1} vs ${gesture2}, ${winner} wins", + ("gesture1", game_details.reveal_moves[1]->gesture) + ("gesture2", game_details.reveal_moves[0]->gesture) + ("winner", game_details.reveal_moves[winner]->gesture)); + game.winners.insert(game.players[winner]); + } + + + const match_object& match_obj = game.match_id(event.db); + event.db.modify(match_obj, [&](match_object& match) { + match.on_game_complete(event.db, game); + }); } }; - typedef waiting_on_previous_gamees initial_state; + typedef waiting_for_game_to_start initial_state; typedef game_state_machine_ x; // makes transition table cleaner - + // Guards - bool was_final_game(const game_complete& event) + bool already_have_other_commit(const game_move& event) { - fc_ilog(fc::logger::get("tournament"), - "In was_final_game guard, returning ${value}", - ("value", false));// game_obj->registered_players == game_obj->options.number_of_players - 1)); - return false; - //return game_obj->registered_players == game_obj->options.number_of_players - 1; + auto iter = std::find(game_obj->players.begin(), game_obj->players.end(), + event.move.player_account_id); + unsigned player_index = std::distance(game_obj->players.begin(), iter); + // hard-coded here for two-player games + unsigned other_player_index = player_index == 0 ? 1 : 0; + const rock_paper_scissors_game_details& game_details = game_obj->game_details.get(); + return game_details.commit_moves.at(other_player_index).valid(); } - bool game_is_a_buy(const initiate_game& event) + bool already_have_other_reveal(const game_move& event) { - return event.players.size() < 2; + auto iter = std::find(game_obj->players.begin(), game_obj->players.end(), + event.move.player_account_id); + unsigned player_index = std::distance(game_obj->players.begin(), iter); + // hard-coded here for two-player games + unsigned other_player_index = player_index == 0 ? 1 : 0; + const rock_paper_scissors_game_details& game_details = game_obj->game_details.get(); + return game_details.reveal_moves.at(other_player_index).valid(); } - + + void apply_commit_move(const game_move& event) + { + auto iter = std::find(game_obj->players.begin(), game_obj->players.end(), + event.move.player_account_id); + unsigned player_index = std::distance(game_obj->players.begin(), iter); + + rock_paper_scissors_game_details& details = game_obj->game_details.get(); + details.commit_moves[player_index] = event.move.move.get(); + } + + void apply_reveal_move(const game_move& event) + { + auto iter = std::find(game_obj->players.begin(), game_obj->players.end(), + event.move.player_account_id); + unsigned player_index = std::distance(game_obj->players.begin(), iter); + + rock_paper_scissors_game_details& details = game_obj->game_details.get(); + details.reveal_moves[player_index] = event.move.move.get(); + } + void start_next_game(const game_complete& event) { fc_ilog(fc::logger::get("tournament"), @@ -118,12 +198,17 @@ namespace graphene { namespace chain { // Transition table for tournament struct transition_table : mpl::vector< // Start Event Next Action Guard + // +-------------------------------+-------------------------+----------------------------+---------------------+----------------------+ + _row < waiting_for_game_to_start, initiate_game, expecting_commit_moves >, // +-------------------------------+-------------------------+----------------------------+---------------------+----------------------+ - _row < waiting_on_previous_gamees, initiate_game, game_in_progress >, - g_row < waiting_on_previous_gamees, initiate_game, game_complete, &x::game_is_a_buy >, + a_row < expecting_commit_moves, game_move, expecting_commit_moves, &x::apply_commit_move >, + row < expecting_commit_moves, game_move, expecting_reveal_moves, &x::apply_commit_move, &x::already_have_other_commit >, // +-------------------------------+-------------------------+----------------------------+---------------------+----------------------+ - a_row < game_in_progress, game_complete, game_in_progress, &x::start_next_game >, - g_row < game_in_progress, game_complete, game_complete, &x::was_final_game > + a_row < expecting_reveal_moves, game_move, expecting_reveal_moves, &x::apply_reveal_move >, + row < expecting_reveal_moves, game_move, game_complete, &x::apply_reveal_move, &x::already_have_other_reveal > + // +-------------------------------+-------------------------+----------------------------+---------------------+----------------------+ + //a_row < game_in_progress, game_complete, game_in_progress, &x::start_next_game >, + //g_row < game_in_progress, game_complete, game_complete, &x::was_final_game > // +---------------------------+-----------------------------+----------------------------+---------------------+----------------------+ > {}; @@ -148,6 +233,7 @@ namespace graphene { namespace chain { game_object::game_object(const game_object& rhs) : graphene::db::abstract_object(rhs), + match_id(rhs.match_id), players(rhs.players), winners(rhs.winners), game_details(rhs.game_details), @@ -161,6 +247,7 @@ namespace graphene { namespace chain { { //graphene::db::abstract_object::operator=(rhs); id = rhs.id; + match_id = rhs.match_id; players = rhs.players; winners = rhs.winners; game_details = rhs.game_details; @@ -214,6 +301,99 @@ namespace graphene { namespace chain { return state; } + void game_object::evaluate_move_operation(const database& db, const game_move_operation& op) const + { + const match_object& match_obj = match_id(db); + + if (game_details.which() == game_specific_details::tag::value) + { + if (op.move.which() == game_specific_moves::tag::value) + { + // Is this move made by a player in the match + auto iter = std::find(players.begin(), players.end(), + op.player_account_id); + if (iter == players.end()) + FC_THROW("Player ${account_id} is not a player in game ${game}", + ("account_id", op.player_account_id) + ("game", id)); + unsigned player_index = std::distance(players.begin(), iter); + + //const rock_paper_scissors_throw_commit& commit = op.move.get(); + + // are we expecting commits? + if (get_state() != game_state::expecting_commit_moves) + FC_THROW("Game ${game} is not accepting any commit moves", ("game", id)); + + // has this player committed already? + const rock_paper_scissors_game_details& details = game_details.get(); + if (details.commit_moves.at(player_index)) + FC_THROW("Player ${account_id} has already committed their move for game ${game}", + ("account_id", op.player_account_id) + ("game", id)); + // if all the above checks pass, then the move is accepted + } + else if (op.move.which() == game_specific_moves::tag::value) + { + // Is this move made by a player in the match + auto iter = std::find(players.begin(), players.end(), + op.player_account_id); + if (iter == players.end()) + FC_THROW("Player ${account_id} is not a player in game ${game}", + ("account_id", op.player_account_id) + ("game", id)); + unsigned player_index = std::distance(players.begin(), iter); + + // has this player committed already? + const rock_paper_scissors_game_details& details = game_details.get(); + if (!details.commit_moves.at(player_index)) + FC_THROW("Player ${account_id} cannot reveal a move which they did not commit in game ${game}", + ("account_id", op.player_account_id) + ("game", id)); + + // are we expecting reveals? + if (get_state() != game_state::expecting_reveal_moves) + FC_THROW("Game ${game} is not accepting any reveal moves", ("game", id)); + + const rock_paper_scissors_throw_commit& commit = *details.commit_moves.at(player_index); + const rock_paper_scissors_throw_reveal& reveal = op.move.get(); + + // does the reveal match the commit? + rock_paper_scissors_throw reconstructed_throw; + reconstructed_throw.nonce1 = commit.nonce1; + reconstructed_throw.nonce2 = reveal.nonce2; + reconstructed_throw.gesture = reveal.gesture; + fc::sha256 reconstructed_hash = reconstructed_throw.calculate_hash(); + + if (commit.throw_hash != reconstructed_hash) + FC_THROW("Reveal does not match commit's hash of ${commit_hash}", + ("commit_hash", commit.throw_hash)); + + // is the throw valid for this game + const match_object& match_obj = match_id(db); + const tournament_object& tournament_obj = match_obj.tournament_id(db); + const rock_paper_scissors_game_options& game_options = tournament_obj.options.game_options.get(); + if ((unsigned)reveal.gesture >= game_options.number_of_gestures) + FC_THROW("Gesture ${gesture_int} is not valid for this game", ("gesture", (unsigned)reveal.gesture)); + // if all the above checks pass, then the move is accepted + } + else + FC_THROW("The only valid moves in a rock-paper-scissors game are commit and reveal, not ${type}", + ("type", op.move.which())); + } + else + FC_THROW("Game of type ${type} not supported", ("type", game_details.which())); + } + + void game_object::on_move(database& db, const game_move_operation& op) + { + my->state_machine.process_event(game_move(db, op)); + } + + void game_object::start_game(database& db, const std::vector& players) + { + my->state_machine.process_event(initiate_game(db, players)); + } + void game_object::pack_impl(std::ostream& stream) const { boost::archive::binary_oarchive oa(stream, boost::archive::no_header|boost::archive::no_codecvt|boost::archive::no_xml_tag_checking); @@ -236,6 +416,7 @@ namespace fc { elog("In game_obj to_variant"); fc::mutable_variant_object o; o("id", game_obj.id) + ("match_id", game_obj.match_id) ("players", game_obj.players) ("winners", game_obj.winners) ("game_details", game_obj.game_details) @@ -249,6 +430,7 @@ namespace fc { { fc_elog(fc::logger::get("tournament"), "In game_obj from_variant"); game_obj.id = v["id"].as(); + game_obj.match_id = v["match_id"].as(); game_obj.players = v["players"].as >(); game_obj.winners = v["winners"].as >(); game_obj.game_details = v["game_details"].as(); diff --git a/libraries/chain/include/graphene/chain/game_object.hpp b/libraries/chain/include/graphene/chain/game_object.hpp index ed4aea0d..6f8ba82f 100644 --- a/libraries/chain/include/graphene/chain/game_object.hpp +++ b/libraries/chain/include/graphene/chain/game_object.hpp @@ -23,6 +23,8 @@ namespace graphene { namespace chain { enum class game_state { game_in_progress, + expecting_commit_moves, + expecting_reveal_moves, game_complete }; @@ -46,6 +48,10 @@ namespace graphene { namespace chain { game_object(const game_object& rhs); ~game_object(); game_object& operator=(const game_object& rhs); + + void evaluate_move_operation(const database& db, const game_move_operation& op) const; + void on_move(database& db, const game_move_operation& op); + void start_game(database& db, const std::vector& players); // serialization functions: // for serializing to raw, go through a temporary sstream object to avoid @@ -118,6 +124,8 @@ namespace graphene { namespace chain { FC_REFLECT_ENUM(graphene::chain::game_state, (game_in_progress) + (expecting_commit_moves) + (expecting_reveal_moves) (game_complete)) FC_REFLECT_TYPENAME(graphene::chain::game_object) // manually serialized diff --git a/libraries/chain/include/graphene/chain/match_object.hpp b/libraries/chain/include/graphene/chain/match_object.hpp index 65d17135..6f8423f4 100644 --- a/libraries/chain/include/graphene/chain/match_object.hpp +++ b/libraries/chain/include/graphene/chain/match_object.hpp @@ -49,6 +49,12 @@ namespace graphene { namespace chain { /// information about a match without having to request all game objects vector > game_winners; + /// A count of the number of wins for each player + vector number_of_wins; + + /// the total number of games that ended up in a tie/draw/stalemate + uint32_t number_of_ties; + // If the match is not yet complete, this will be empty // If the match is in the "match_complete" state, it will contain the // list of winners. @@ -84,6 +90,7 @@ namespace graphene { namespace chain { void pack_impl(std::ostream& stream) const; void unpack_impl(std::istream& stream); void on_initiate_match(database& db, const vector& players); + void on_game_complete(database& db, const game_object& game); game_id_type start_next_game(database& db, match_id_type match_id); class impl; @@ -104,9 +111,12 @@ namespace graphene { namespace chain { // instead of calling the derived pack, just serialize the one field in the base class // fc::raw::pack >(s, match_obj); fc::raw::pack(s, match_obj.id); + fc::raw::pack(s, match_obj.tournament_id); fc::raw::pack(s, match_obj.players); fc::raw::pack(s, match_obj.games); fc::raw::pack(s, match_obj.game_winners); + fc::raw::pack(s, match_obj.number_of_wins); + fc::raw::pack(s, match_obj.number_of_ties); fc::raw::pack(s, match_obj.match_winners); fc::raw::pack(s, match_obj.start_time); fc::raw::pack(s, match_obj.end_time); @@ -126,9 +136,12 @@ namespace graphene { namespace chain { // unpack all fields exposed in the header in the usual way //fc::raw::unpack >(s, match_obj); fc::raw::unpack(s, match_obj.id); + fc::raw::unpack(s, match_obj.tournament_id); fc::raw::unpack(s, match_obj.players); fc::raw::unpack(s, match_obj.games); fc::raw::unpack(s, match_obj.game_winners); + fc::raw::unpack(s, match_obj.number_of_wins); + fc::raw::unpack(s, match_obj.number_of_ties); fc::raw::unpack(s, match_obj.match_winners); fc::raw::unpack(s, match_obj.start_time); fc::raw::unpack(s, match_obj.end_time); diff --git a/libraries/chain/include/graphene/chain/protocol/operations.hpp b/libraries/chain/include/graphene/chain/protocol/operations.hpp index c5d1a084..31c0553d 100644 --- a/libraries/chain/include/graphene/chain/protocol/operations.hpp +++ b/libraries/chain/include/graphene/chain/protocol/operations.hpp @@ -94,7 +94,8 @@ namespace graphene { namespace chain { asset_claim_fees_operation, fba_distribute_operation, // VIRTUAL tournament_create_operation, - tournament_join_operation + tournament_join_operation, + game_move_operation > operation; /// @} // operations group diff --git a/libraries/chain/include/graphene/chain/protocol/rock_paper_scissors.hpp b/libraries/chain/include/graphene/chain/protocol/rock_paper_scissors.hpp index 1271757c..1d377e0f 100644 --- a/libraries/chain/include/graphene/chain/protocol/rock_paper_scissors.hpp +++ b/libraries/chain/include/graphene/chain/protocol/rock_paper_scissors.hpp @@ -48,10 +48,69 @@ namespace graphene { namespace chain { /// The number of seconds users are given to reveal their move, counted from the time of the /// block containing the second commit or the where the time_per_commit_move expired uint32_t time_per_reveal_move; + + /// The number of allowed gestures, must be either 3 or 5. If 3, the game is + /// standard rock-paper-scissors, if 5, it's + /// rock-paper-scissors-lizard-spock. + uint8_t number_of_gestures; + }; + + enum class rock_paper_scissors_gesture + { + rock, + paper, + scissors, + spock, + lizard + }; + + struct rock_paper_scissors_throw + { + uint64_t nonce1; + uint64_t nonce2; + rock_paper_scissors_gesture gesture; + fc::sha256 calculate_hash() const; + }; + + struct rock_paper_scissors_throw_commit + { + uint64_t nonce1; + fc::sha256 throw_hash; + bool operator<(const graphene::chain::rock_paper_scissors_throw_commit& rhs) const + { + return std::tie(nonce1, throw_hash) < std::tie(rhs.nonce1, rhs.throw_hash); + } + }; + + + + struct rock_paper_scissors_throw_reveal + { + uint64_t nonce2; + rock_paper_scissors_gesture gesture; }; } } -FC_REFLECT( graphene::chain::rock_paper_scissors_game_options, (insurance_enabled)(time_per_commit_move)(time_per_reveal_move) ) +FC_REFLECT( graphene::chain::rock_paper_scissors_game_options, (insurance_enabled)(time_per_commit_move)(time_per_reveal_move)(number_of_gestures) ) +FC_REFLECT_TYPENAME( graphene::chain::rock_paper_scissors_gesture) +FC_REFLECT_ENUM( graphene::chain::rock_paper_scissors_gesture, + (rock) + (paper) + (scissors) + (spock) + (lizard)) + +FC_REFLECT( graphene::chain::rock_paper_scissors_throw, + (nonce1) + (nonce2) + (gesture) ) + +FC_REFLECT( graphene::chain::rock_paper_scissors_throw_commit, + (nonce1) + (throw_hash) ) + +FC_REFLECT( graphene::chain::rock_paper_scissors_throw_reveal, + (nonce2)(gesture) ) diff --git a/libraries/chain/include/graphene/chain/protocol/tournament.hpp b/libraries/chain/include/graphene/chain/protocol/tournament.hpp index 2d167f1f..2c390fac 100644 --- a/libraries/chain/include/graphene/chain/protocol/tournament.hpp +++ b/libraries/chain/include/graphene/chain/protocol/tournament.hpp @@ -148,10 +148,36 @@ namespace graphene { namespace chain { void validate()const; }; + typedef fc::static_variant game_specific_moves; + + struct game_move_operation : public base_operation + { + struct fee_parameters_type { + share_type fee = GRAPHENE_BLOCKCHAIN_PRECISION; + }; + asset fee; + + /// the id of the game + game_id_type game_id; + + /// The account of the player making this move + account_id_type player_account_id; + + /// the move itself + game_specific_moves move; + + extensions_type extensions; + + account_id_type fee_payer()const { return player_account_id; } + share_type calculate_fee(const fee_parameters_type& k)const; + void validate()const; + }; + } } FC_REFLECT_ENUM( graphene::chain::game_type, (rock_paper_scissors)(GAME_TYPE_COUNT) ) FC_REFLECT_TYPENAME( graphene::chain::game_specific_options ) +FC_REFLECT_TYPENAME( graphene::chain::game_specific_moves ) FC_REFLECT( graphene::chain::tournament_options, (type_of_game) (registration_deadline) @@ -176,6 +202,13 @@ FC_REFLECT( graphene::chain::tournament_join_operation, (tournament_id) (buy_in) (extensions)) +FC_REFLECT( graphene::chain::game_move_operation, + (fee) + (game_id) + (player_account_id) + (move) + (extensions)) FC_REFLECT( graphene::chain::tournament_create_operation::fee_parameters_type, (fee) ) FC_REFLECT( graphene::chain::tournament_join_operation::fee_parameters_type, (fee) ) +FC_REFLECT( graphene::chain::game_move_operation::fee_parameters_type, (fee) ) diff --git a/libraries/chain/include/graphene/chain/rock_paper_scissors.hpp b/libraries/chain/include/graphene/chain/rock_paper_scissors.hpp index abfa6678..23015e3e 100644 --- a/libraries/chain/include/graphene/chain/rock_paper_scissors.hpp +++ b/libraries/chain/include/graphene/chain/rock_paper_scissors.hpp @@ -33,55 +33,24 @@ #include namespace graphene { namespace chain { - enum class rock_paper_scissors_throw - { - rock, - paper, - scissors - }; - struct rock_paper_scissors_move - { - uint64_t nonce1; - uint64_t nonce2; - rock_paper_scissors_throw move; - }; - struct rock_paper_scissors_commit - { - uint64_t nonce1; - fc::sha256 move_hash; - }; - struct rock_paper_scissors_reveal - { - uint64_t nonce2; - rock_paper_scissors_throw move; - }; - struct rock_paper_scissors_game_details { - fc::array, 2> commit_moves; - fc::array, 2> reveal_moves; + // note: I wanted to declare these as fixed arrays, but they don't serialize properly + //fc::array, 2> commit_moves; + //fc::array, 2> reveal_moves; + std::vector > commit_moves; + std::vector > reveal_moves; + rock_paper_scissors_game_details() : + commit_moves(2), + reveal_moves(2) + { + } }; typedef fc::static_variant game_specific_details; - } } -FC_REFLECT_ENUM( graphene::chain::rock_paper_scissors_throw, - (rock) - (paper) - (scissors)) - -FC_REFLECT( graphene::chain::rock_paper_scissors_move, - (nonce1) - (nonce2) - (move) ) - -FC_REFLECT( graphene::chain::rock_paper_scissors_commit, - (nonce1) - (move_hash) ) - -FC_REFLECT( graphene::chain::rock_paper_scissors_reveal, - (nonce2)(move) ) - FC_REFLECT( graphene::chain::rock_paper_scissors_game_details, (commit_moves)(reveal_moves) ) +FC_REFLECT_TYPENAME( graphene::chain::game_specific_details ) + diff --git a/libraries/chain/include/graphene/chain/tournament_evaluator.hpp b/libraries/chain/include/graphene/chain/tournament_evaluator.hpp index d5b70d04..ff70a52e 100644 --- a/libraries/chain/include/graphene/chain/tournament_evaluator.hpp +++ b/libraries/chain/include/graphene/chain/tournament_evaluator.hpp @@ -28,4 +28,16 @@ namespace graphene { namespace chain { void_result do_apply( const tournament_join_operation& o ); }; + class game_move_evaluator : public evaluator + { + private: + const game_object* _game_obj = nullptr; + public: + typedef game_move_operation operation_type; + + void_result do_evaluate( const game_move_operation& o ); + void_result do_apply( const game_move_operation& o ); + }; + + } } diff --git a/libraries/chain/match_object.cpp b/libraries/chain/match_object.cpp index 4a146b88..5627965c 100644 --- a/libraries/chain/match_object.cpp +++ b/libraries/chain/match_object.cpp @@ -53,8 +53,8 @@ namespace graphene { namespace chain { struct game_complete { database& db; - game_id_type game_id; - game_complete(database& db, game_id_type game_id) : db(db), game_id(game_id) {}; + const game_object& game; + game_complete(database& db, const game_object& game) : db(db), game(game) {}; }; struct match_state_machine_ : public msm::front::state_machine_def @@ -67,24 +67,40 @@ namespace graphene { namespace chain { struct waiting_on_previous_matches : public msm::front::state<>{}; struct match_in_progress : public msm::front::state<> { + void on_entry(const game_complete& event, match_state_machine_& fsm) + { + fc_ilog(fc::logger::get("tournament"), + "Game ${game_id} in match ${id} is complete", + ("game_id", event.game.id)("id", fsm.match_obj->id)); + } void on_entry(const initiate_match& event, match_state_machine_& fsm) { match_object& match = *fsm.match_obj; - match.players = event.players; - match.start_time = event.db.head_block_time(); fc_ilog(fc::logger::get("tournament"), "Match ${id} is now in progress", ("id", match.id)); + match.players = event.players; + match.number_of_wins.resize(match.players.size()); + match.start_time = event.db.head_block_time(); + + fsm.start_next_game(event.db); } }; struct match_complete : public msm::front::state<> { void on_entry(const game_complete& event, match_state_machine_& fsm) { + match_object& match = *fsm.match_obj; fc_ilog(fc::logger::get("tournament"), "Match ${id} is complete", - ("id", fsm.match_obj->id)); + ("id", match.id)); + + const tournament_object& tournament_obj = match.tournament_id(event.db); + event.db.modify(tournament_obj, [&](tournament_object& tournament) { + tournament.on_final_game_completed(); + }); + } void on_entry(const initiate_match& event, match_state_machine_& fsm) { @@ -93,6 +109,7 @@ namespace graphene { namespace chain { "Match ${id} is complete, it was a buy", ("id", match)); match.players = event.players; + match.number_of_wins.resize(match.players.size()); boost::copy(event.players, std::inserter(match.match_winners, match.match_winners.end())); match.start_time = event.db.head_block_time(); match.end_time = event.db.head_block_time(); @@ -105,11 +122,17 @@ namespace graphene { namespace chain { // Guards bool was_final_game(const game_complete& event) { - fc_ilog(fc::logger::get("tournament"), - "In was_final_game guard, returning ${value}", - ("value", false));// match_obj->registered_players == match_obj->options.number_of_players - 1)); + const tournament_object& tournament_obj = match_obj->tournament_id(event.db); + + for (unsigned i = 0; i < match_obj->players.size(); ++i) + { + // this guard is called before the winner of the current game factored in to our running totals, + // so we must add the current game to our count + uint32_t win_for_this_game = event.game.winners.find(match_obj->players[i]) != event.game.winners.end() ? 1 : 0; + if (match_obj->number_of_wins[i] + win_for_this_game >= tournament_obj.options.number_of_wins) + return true; + } return false; - //return match_obj->registered_players == match_obj->options.number_of_players - 1; } bool match_is_a_buy(const initiate_match& event) @@ -117,10 +140,35 @@ namespace graphene { namespace chain { return event.players.size() < 2; } - void start_next_game(const game_complete& event) + void record_completed_game(const game_complete& event) + { + if (event.game.winners.empty()) + ++match_obj->number_of_ties; + else + for (unsigned i = 0; i < match_obj->players.size(); ++i) + if (event.game.winners.find(match_obj->players[i]) != event.game.winners.end()) + ++match_obj->number_of_wins[i]; + match_obj->game_winners.emplace_back(event.game.winners); + } + + void start_next_game(database& db) { fc_ilog(fc::logger::get("tournament"), - "In start_next_game action"); + "In start_next_game"); + const game_object& game = + db.create( [&]( game_object& game ) { + game.match_id = match_obj->id; + game.players = match_obj->players; + game.game_details = rock_paper_scissors_game_details(); + game.start_game(db, game.players); + }); + match_obj->games.push_back(game.id); + } + + void record_and_start_next_game(const game_complete& event) + { + record_completed_game(event); + start_next_game(event.db); } // Transition table for tournament @@ -130,8 +178,8 @@ namespace graphene { namespace chain { _row < waiting_on_previous_matches, initiate_match, match_in_progress >, g_row < waiting_on_previous_matches, initiate_match, match_complete, &x::match_is_a_buy >, // +-------------------------------+-------------------------+----------------------------+---------------------+----------------------+ - a_row < match_in_progress, game_complete, match_in_progress, &x::start_next_game >, - g_row < match_in_progress, game_complete, match_complete, &x::was_final_game > + a_row < match_in_progress, game_complete, match_in_progress, &x::record_and_start_next_game >, + row < match_in_progress, game_complete, match_complete, &x::record_completed_game, &x::was_final_game > // +---------------------------+-----------------------------+----------------------------+---------------------+----------------------+ > {}; @@ -156,9 +204,12 @@ namespace graphene { namespace chain { match_object::match_object(const match_object& rhs) : graphene::db::abstract_object(rhs), + tournament_id(rhs.tournament_id), players(rhs.players), games(rhs.games), game_winners(rhs.game_winners), + number_of_wins(rhs.number_of_wins), + number_of_ties(rhs.number_of_ties), match_winners(rhs.match_winners), start_time(rhs.start_time), end_time(rhs.end_time), @@ -172,9 +223,12 @@ namespace graphene { namespace chain { { //graphene::db::abstract_object::operator=(rhs); id = rhs.id; + tournament_id = rhs.tournament_id; players = rhs.players; games = rhs.games; game_winners = rhs.game_winners; + number_of_wins = rhs.number_of_wins; + number_of_ties = rhs.number_of_ties; match_winners = rhs.match_winners; start_time = rhs.start_time; end_time = rhs.end_time; @@ -245,6 +299,11 @@ namespace graphene { namespace chain { my->state_machine.process_event(initiate_match(db, players)); } + void match_object::on_game_complete(database& db, const game_object& game) + { + my->state_machine.process_event(game_complete(db, game)); + } +#if 0 game_id_type match_object::start_next_game(database& db, match_id_type match_id) { const game_object& game = @@ -254,42 +313,49 @@ namespace graphene { namespace chain { }); return game.id; } +#endif } } // graphene::chain namespace fc { // Manually reflect match_object to variant to properly reflect "state" void to_variant(const graphene::chain::match_object& match_obj, fc::variant& v) - { + { try { fc_elog(fc::logger::get("tournament"), "In match_obj to_variant"); elog("In match_obj to_variant"); fc::mutable_variant_object o; o("id", match_obj.id) + ("tournament_id", match_obj.tournament_id) ("players", match_obj.players) ("games", match_obj.games) ("game_winners", match_obj.game_winners) + ("number_of_wins", match_obj.number_of_wins) + ("number_of_ties", match_obj.number_of_ties) ("match_winners", match_obj.match_winners) ("start_time", match_obj.start_time) ("end_time", match_obj.end_time) ("state", match_obj.get_state()); v = o; - } + } FC_RETHROW_EXCEPTIONS(warn, "") } // Manually reflect match_object to variant to properly reflect "state" void from_variant(const fc::variant& v, graphene::chain::match_object& match_obj) - { + { try { fc_elog(fc::logger::get("tournament"), "In match_obj from_variant"); match_obj.id = v["id"].as(); + match_obj.tournament_id = v["tournament_id"].as(); match_obj.players = v["players"].as >(); match_obj.games = v["games"].as >(); match_obj.game_winners = v["game_winners"].as > >(); + match_obj.number_of_wins = v["number_of_wins"].as >(); + match_obj.number_of_ties = v["number_of_ties"].as(); match_obj.match_winners = v["match_winners"].as >(); match_obj.start_time = v["start_time"].as(); match_obj.end_time = v["end_time"].as >(); graphene::chain::match_state state = v["state"].as(); const_cast(match_obj.my->state_machine.current_state())[0] = (int)state; - } + } FC_RETHROW_EXCEPTIONS(warn, "") } } //end namespace fc diff --git a/libraries/chain/protocol/tournament.cpp b/libraries/chain/protocol/tournament.cpp index 9a5cf074..f7faa93b 100644 --- a/libraries/chain/protocol/tournament.cpp +++ b/libraries/chain/protocol/tournament.cpp @@ -52,5 +52,14 @@ void tournament_join_operation::validate()const FC_ASSERT( fee.amount >= 0 ); } +share_type game_move_operation::calculate_fee(const fee_parameters_type& k)const +{ + return k.fee; +} + +void game_move_operation::validate()const +{ +} + } } // namespace graphene::chain diff --git a/libraries/chain/tournament_evaluator.cpp b/libraries/chain/tournament_evaluator.cpp index 8ba4d663..6bb62866 100644 --- a/libraries/chain/tournament_evaluator.cpp +++ b/libraries/chain/tournament_evaluator.cpp @@ -1,5 +1,6 @@ #include #include +#include #include #include #include @@ -101,7 +102,7 @@ namespace graphene { namespace chain { //const account_object& player_account = op.player_account_id(d); _buy_in_asset_type = &op.buy_in.asset_id(d); - // TODO FC_ASSERT(_tournament_obj->state == tournament_state::accepting_registrations); + FC_ASSERT(_tournament_obj->get_state() == tournament_state::accepting_registrations); FC_ASSERT(_tournament_details_obj->registered_players.size() < _tournament_obj->options.number_of_players, "Tournament is already full"); FC_ASSERT(d.head_block_time() <= _tournament_obj->options.registration_deadline, @@ -143,6 +144,21 @@ namespace graphene { namespace chain { return void_result(); } FC_CAPTURE_AND_RETHROW( (op) ) } + void_result game_move_evaluator::do_evaluate( const game_move_operation& o ) + { try { + const database& d = db(); + _game_obj = &o.game_id(d); + _game_obj->evaluate_move_operation(d, o); + return void_result(); + } FC_CAPTURE_AND_RETHROW( (o) ) } + + void_result game_move_evaluator::do_apply( const game_move_operation& o ) + { try { + db().modify(*_game_obj, [&](game_object& game_obj){ + game_obj.on_move(db(), o); + }); + return void_result(); + } FC_CAPTURE_AND_RETHROW( (o) ) } } } diff --git a/libraries/chain/tournament_object.cpp b/libraries/chain/tournament_object.cpp index 85434e87..0ee5be92 100644 --- a/libraries/chain/tournament_object.cpp +++ b/libraries/chain/tournament_object.cpp @@ -102,6 +102,7 @@ namespace graphene { namespace chain { db.create( [&]( match_object& match ) { match.tournament_id = tournament_id; match.players = players; + match.number_of_wins.resize(match.players.size()); match.start_time = db.head_block_time(); if (match.players.size() == 1) { @@ -445,6 +446,12 @@ namespace graphene { namespace chain { } } + fc::sha256 rock_paper_scissors_throw::calculate_hash() const + { + std::vector full_throw_packed(fc::raw::pack(*this)); + return fc::sha256::hash(full_throw_packed.data(), full_throw_packed.size()); + } + } } // graphene::chain namespace fc { diff --git a/libraries/wallet/include/graphene/wallet/wallet.hpp b/libraries/wallet/include/graphene/wallet/wallet.hpp index 07a16f4a..e2b04909 100644 --- a/libraries/wallet/include/graphene/wallet/wallet.hpp +++ b/libraries/wallet/include/graphene/wallet/wallet.hpp @@ -191,6 +191,8 @@ struct wallet_data key_label_index_type labeled_keys; blind_receipt_index_type blind_receipts; + std::map committed_game_moves; + string ws_server = "ws://localhost:8090"; string ws_user; string ws_password; @@ -1430,6 +1432,17 @@ class wallet_api */ tournament_object get_tournament(tournament_id_type id); + /** Play a move in the rock-paper-scissors game + * @param game_id the id of the game + * @param player_account the name of the player + * @param gesture rock, paper, or scissors + * @return the signed version of the transaction + */ + signed_transaction rps_throw(game_id_type game_id, + string player_account, + rock_paper_scissors_gesture gesture, + bool broadcast); + void dbg_make_uia(string creator, string symbol); void dbg_make_mia(string creator, string symbol); void flood_network(string prefix, uint32_t number_of_transactions); @@ -1472,6 +1485,7 @@ FC_REFLECT( graphene::wallet::wallet_data, (pending_account_registrations)(pending_witness_registrations) (labeled_keys) (blind_receipts) + (committed_game_moves) (ws_server) (ws_user) (ws_password) @@ -1619,6 +1633,7 @@ FC_API( graphene::wallet::wallet_api, (receive_blind_transfer) (tournament_create) (tournament_join) + (rps_throw) (get_upcoming_tournaments) (get_tournament) ) diff --git a/libraries/wallet/wallet.cpp b/libraries/wallet/wallet.cpp index 93e5e591..604a99f7 100644 --- a/libraries/wallet/wallet.cpp +++ b/libraries/wallet/wallet.cpp @@ -60,11 +60,17 @@ #include #include #include +#include #include #include + #include #include +#include +#include +#include + #include #include #include @@ -339,6 +345,16 @@ private: } } + // return true if any of my_accounts are players in this tournament + bool tournament_is_relevant_to_my_accounts(const tournament_object& tournament_obj) + { + tournament_details_object tournament_details = get_object(tournament_obj.tournament_details_id); + for (const account_object& account_obj : _wallet.my_accounts) + if (tournament_details.registered_players.find(account_obj.id) != tournament_details.registered_players.end()) + return true; + return false; + } + fc::mutex _subscribed_object_changed_mutex; void subscribed_object_changed(const variant& changed_objects_variant) { @@ -368,6 +384,14 @@ private: } tournament_cache.modify(tournament_cache_iter, [&](tournament_object& obj) { obj = current_tournament_obj; }); } + else if (tournament_is_relevant_to_my_accounts(current_tournament_obj)) + { + ilog ("We were just notified about an in-progress tournament ${id} relevant to our accounts", + ("id", current_tournament_obj.id)); + tournament_cache.insert(current_tournament_obj); + if (current_tournament_obj.get_state() == tournament_state::in_progress) + monitor_matches_in_tournament(current_tournament_obj); + } continue; } catch (const fc::exception& e) @@ -398,6 +422,30 @@ private: { // idump((e)); } + try + { + object_id_type id = changed_object_variant["id"].as(); + game_object current_game_obj = changed_object_variant.as(); + auto game_cache_iter = game_cache.find(id); + if (game_cache_iter != game_cache.end()) + { + const game_object& cached_game_obj = *game_cache_iter; + if (cached_game_obj.get_state() != current_game_obj.get_state()) + { + ilog("game ${id} changed state from ${old} to ${new}", + ("id", id) + ("old", cached_game_obj.get_state()) + ("new", current_game_obj.get_state())); + game_in_new_state(current_game_obj); + } + game_cache.modify(game_cache_iter, [&](game_object& obj) { obj = current_game_obj; }); + } + continue; + } + catch (const fc::exception& e) + { + // idump((e)); + } } } } @@ -787,8 +835,77 @@ public: vector< signed_transaction > import_balance( string name_or_id, const vector& wif_keys, bool broadcast ); + void game_in_new_state(const game_object& game_obj) + { try { + if (game_obj.get_state() == game_state::expecting_commit_moves) + { + if (game_obj.players.size() != 2) // we only support RPS, a 2 player game + return; + const rock_paper_scissors_game_details& rps_details = game_obj.game_details.get(); + for (unsigned i = 0; i < 2; ++i) + { + if (!rps_details.commit_moves.at(i)) // if this player hasn't committed their move + { + const account_id_type& account_id = game_obj.players[i]; + if (_wallet.my_accounts.find(account_id) != _wallet.my_accounts.end()) // and they're us + { + ilog("Game ${game_id}: it is ${account_name}'s turn to commit their move", + ("game_id", game_obj.id) + ("account_name", get_account(account_id).name)); + } + } + } + } + else if (game_obj.get_state() == game_state::expecting_reveal_moves) + { + if (game_obj.players.size() != 2) // we only support RPS, a 2 player game + return; + const rock_paper_scissors_game_details& rps_details = game_obj.game_details.get(); + for (unsigned i = 0; i < 2; ++i) + { + if (rps_details.commit_moves.at(i) && + !rps_details.reveal_moves.at(i)) // if this player has committed but not revealed + { + const account_id_type& account_id = game_obj.players[i]; + if (_wallet.my_accounts.find(account_id) != _wallet.my_accounts.end()) // and they're us + { + if (self.is_locked()) + ilog("Game ${game_id}: unable to broadcast ${account_name}'s reveal because the wallet is locked", + ("game_id", game_obj.id) + ("account_name", get_account(account_id).name)); + else + { + ilog("Game ${game_id}: it is ${account_name}'s turn to reveal their move", + ("game_id", game_obj.id) + ("account_name", get_account(account_id).name)); + + auto iter = _wallet.committed_game_moves.find(*rps_details.commit_moves.at(i)); + if (iter != _wallet.committed_game_moves.end()) + { + const rock_paper_scissors_throw_reveal& reveal = iter->second; + + game_move_operation move_operation; + move_operation.game_id = game_obj.id; + move_operation.player_account_id = account_id; + move_operation.move = reveal; + + signed_transaction trx; + trx.operations = {move_operation}; + set_operation_fees( trx, _remote_db->get_global_properties().parameters.current_fees); + trx.validate(); + ilog("Broadcasting reveal..."); + trx = sign_transaction(trx, true); + ilog("Reveal broadcast, transaction id is ${id}", ("id", trx.id())); + } + } + } + } + } + } + } FC_RETHROW_EXCEPTIONS(warn, "") } + void match_in_new_state(const match_object& match_obj) - { + { try { if (match_obj.get_state() == match_state::match_in_progress) { for (const account_id_type& account_id : match_obj.players) @@ -797,15 +914,22 @@ public: { ilog("Match ${match} is now in progress for player ${account}", ("match", match_obj.id)("account", get_account(account_id).name)); + for (const game_id_type& game_id : match_obj.games) + { + game_object game_obj = get_object(game_id); + auto insert_result = game_cache.insert(game_obj); + if (insert_result.second) + game_in_new_state(game_obj); + } } } } - } + } FC_RETHROW_EXCEPTIONS(warn, "") } // Cache all matches in the tournament, which will also register us for // updates on those matches void monitor_matches_in_tournament(const tournament_object& tournament_obj) - { + { try { tournament_details_object tournament_details = get_object(tournament_obj.tournament_details_id); for (const match_id_type& match_id : tournament_details.matches) { @@ -814,6 +938,42 @@ public: if (insert_result.second) match_in_new_state(match_obj); } + } FC_RETHROW_EXCEPTIONS(warn, "") } + + void resync_active_tournaments() + { + // check to see if any of our accounts are registered for tournaments + // the real purpose of this is to ensure that we are subscribed for callbacks on these tournaments + ilog("Checking my accounts for active tournaments",); + tournament_cache.clear(); + match_cache.clear(); + game_cache.clear(); + for (const account_object& my_account : _wallet.my_accounts) + { + std::vector tournaments = _remote_db->get_active_tournaments(my_account.id, 100); + std::vector tournament_ids; + for (const tournament_object& tournament : tournaments) + { + try + { + auto insert_result = tournament_cache.insert(tournament); + if (insert_result.second) + { + // then this is the first time we've seen this tournament + monitor_matches_in_tournament(tournament); + } + tournament_ids.push_back(tournament.id); + } + catch (const fc::exception& e) + { + edump((e)(tournament)); + } + } + if (!tournaments.empty()) + ilog("Account ${my_account} is registered for tournaments: ${tournaments}", ("my_account", my_account.name)("tournaments", tournament_ids)); + else + ilog("Account ${my_account} is not registered for any tournaments", ("my_account", my_account.name)); + } } bool load_wallet_file(string wallet_filename = "") @@ -876,28 +1036,7 @@ public: } } - // check to see if any of our accounts are registered for tournaments - // the real purpose of this is to ensure that we are subscribed for callbacks on these tournaments - ilog("Checking my accounts for active tournaments",); - for (const account_object& my_account : _wallet.my_accounts) - { - std::vector tournaments = _remote_db->get_active_tournaments(my_account.id, 100); - std::vector tournament_ids; - for (const tournament_object& tournament : tournaments) - { - auto insert_result = tournament_cache.insert(tournament); - if (insert_result.second) - { - // then this is the first time we've seen this tournament - monitor_matches_in_tournament(tournament); - } - tournament_ids.push_back(tournament.id); - } - if (!tournaments.empty()) - ilog("Account ${my_account} is registered for tournaments: ${tournaments}", ("my_account", my_account.name)("tournaments", tournament_ids)); - else - ilog("Account ${my_account} is not registered for any tournaments", ("my_account", my_account.name)); - } + resync_active_tournaments(); return true; } @@ -2723,6 +2862,12 @@ public: ordered_unique< tag, member< object, object_id_type, &object::id > > > > match_index_type; match_index_type match_cache; + typedef multi_index_container< + game_object, + indexed_by< + ordered_unique< tag, member< object, object_id_type, &object::id > > > > game_index_type; + game_index_type game_cache; + #ifdef __unix__ mode_t _old_umask; #endif @@ -3666,6 +3811,7 @@ void wallet_api::unlock(string password) my->_keys = std::move(pk.keys); my->_checksum = pk.checksum; my->self.lock_changed(false); + my->resync_active_tournaments(); } FC_CAPTURE_AND_RETHROW() } void wallet_api::set_password( string password ) @@ -4419,6 +4565,55 @@ tournament_object wallet_api::get_tournament(tournament_id_type id) return my->_remote_db->get_objects({id})[0].as(); } +signed_transaction wallet_api::rps_throw(game_id_type game_id, + string player_account, + rock_paper_scissors_gesture gesture, + bool broadcast) +{ + FC_ASSERT( !is_locked() ); + + // check whether the gesture is appropriate for the game we're playing + graphene::chain::game_object game_obj = my->get_object(game_id); + graphene::chain::match_object match_obj = my->get_object(game_obj.match_id); + graphene::chain::tournament_object tournament_obj = my->get_object(match_obj.tournament_id); + graphene::chain::rock_paper_scissors_game_options game_options = + tournament_obj.options.game_options.get(); + if ((int)gesture >= game_options.number_of_gestures) + FC_THROW("Gesture ${gesture} not supported in this game", ("gesture", gesture)); + + account_object player_account_obj = get_account(player_account); + + // construct the complete throw, the commit, and reveal + rock_paper_scissors_throw full_throw; + fc::rand_bytes((char*)&full_throw.nonce1, sizeof(full_throw.nonce1)); + fc::rand_bytes((char*)&full_throw.nonce2, sizeof(full_throw.nonce2)); + full_throw.gesture = gesture; + + rock_paper_scissors_throw_commit commit_throw; + commit_throw.nonce1 = full_throw.nonce1; + std::vector full_throw_packed(fc::raw::pack(full_throw)); + commit_throw.throw_hash = fc::sha256::hash(full_throw_packed.data(), full_throw_packed.size()); + + rock_paper_scissors_throw_reveal reveal_throw; + reveal_throw.nonce2 = full_throw.nonce2; + reveal_throw.gesture = full_throw.gesture; + + // store off the reveal for transmitting after both players commit + my->_wallet.committed_game_moves[commit_throw] = reveal_throw; + + // broadcast the commit + signed_transaction tx; + game_move_operation move_operation; + move_operation.game_id = game_id; + move_operation.player_account_id = player_account_obj.id; + move_operation.move = commit_throw; + tx.operations = {move_operation}; + my->set_operation_fees( tx, my->_remote_db->get_global_properties().parameters.current_fees ); + tx.validate(); + + return my->sign_transaction( tx, broadcast ); +} + // default ctor necessary for FC_REFLECT signed_block_with_info::signed_block_with_info() {