commiting last changes
This commit is contained in:
parent
787019617f
commit
2fd34ef6ab
6 changed files with 111 additions and 94 deletions
|
|
@ -371,12 +371,6 @@ void database::init_genesis(const genesis_state_type& genesis_state)
|
|||
a.options.payout_interval = 7*24*60*60;
|
||||
a.dividend_distribution_account = TOURNAMENT_RAKE_FEE_ACCOUNT_ID;
|
||||
});
|
||||
// const asset_bitasset_data_object& bit_asset =
|
||||
// create<asset_bitasset_data_object>([&](asset_bitasset_data_object& a) {
|
||||
// a.current_feed.maintenance_collateral_ratio = 1750;
|
||||
// a.current_feed.maximum_short_squeeze_ratio = 1500;
|
||||
// a.current_feed_publication_time = genesis_state.initial_timestamp + fc::hours(1);
|
||||
// });
|
||||
|
||||
const asset_object& core_asset =
|
||||
create<asset_object>( [&]( asset_object& a ) {
|
||||
|
|
@ -392,7 +386,6 @@ void database::init_genesis(const genesis_state_type& genesis_state)
|
|||
a.options.core_exchange_rate.quote.asset_id = asset_id_type(0);
|
||||
a.dynamic_asset_data_id = dyn_asset.id;
|
||||
a.dividend_data_id = div_asset.id;
|
||||
// a.bitasset_data_id = bit_asset.id;
|
||||
});
|
||||
assert( asset_id_type(core_asset.id) == asset().asset_id );
|
||||
assert( get_balance(account_id_type(), asset_id_type()) == asset(dyn_asset.current_supply) );
|
||||
|
|
@ -411,12 +404,6 @@ void database::init_genesis(const genesis_state_type& genesis_state)
|
|||
a.options.payout_interval = 7*24*60*60;
|
||||
a.dividend_distribution_account = TOURNAMENT_RAKE_FEE_ACCOUNT_ID;
|
||||
});
|
||||
const asset_bitasset_data_object& bit_asset1 =
|
||||
create<asset_bitasset_data_object>([&](asset_bitasset_data_object& a) {
|
||||
a.current_feed.maintenance_collateral_ratio = 1750;
|
||||
a.current_feed.maximum_short_squeeze_ratio = 1500;
|
||||
a.current_feed_publication_time = genesis_state.initial_timestamp + fc::hours(1);
|
||||
});
|
||||
|
||||
const asset_object& default_asset =
|
||||
create<asset_object>( [&]( asset_object& a ) {
|
||||
|
|
@ -433,7 +420,6 @@ void database::init_genesis(const genesis_state_type& genesis_state)
|
|||
a.options.core_exchange_rate.quote.asset_id = asset_id_type(1);
|
||||
a.dynamic_asset_data_id = dyn_asset1.id;
|
||||
a.dividend_data_id = div_asset1.id;
|
||||
a.bitasset_data_id = bit_asset1.id;
|
||||
});
|
||||
assert( default_asset.id == asset_id_type(1) );
|
||||
#endif
|
||||
|
|
|
|||
|
|
@ -106,7 +106,6 @@ namespace graphene { namespace chain {
|
|||
fc_ilog(fc::logger::get("tournament"),
|
||||
"game ${id} received a commit move, still expecting another commit move",
|
||||
("id", game.id));
|
||||
set_next_timeout(event.db, game);
|
||||
}
|
||||
};
|
||||
struct expecting_reveal_moves : public msm::front::state<>
|
||||
|
|
@ -131,14 +130,16 @@ namespace graphene { namespace chain {
|
|||
game_object& game = *fsm.game_obj;
|
||||
|
||||
if (event.move.move.which() == game_specific_moves::tag<rock_paper_scissors_throw_commit>::value)
|
||||
{
|
||||
fc_ilog(fc::logger::get("tournament"),
|
||||
"game ${id} received a commit move, now expecting reveal moves",
|
||||
("id", game.id));
|
||||
set_next_timeout(event.db, game);
|
||||
}
|
||||
else
|
||||
fc_ilog(fc::logger::get("tournament"),
|
||||
"game ${id} received a reveal move, still expecting reveal moves",
|
||||
("id", game.id));
|
||||
set_next_timeout(event.db, game);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -146,9 +147,9 @@ namespace graphene { namespace chain {
|
|||
{
|
||||
void clear_next_timeout(database& db, game_object& game)
|
||||
{
|
||||
const match_object& match_obj = game.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<rock_paper_scissors_game_options>();
|
||||
//const match_object& match_obj = game.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<rock_paper_scissors_game_options>();
|
||||
game.next_timeout = fc::optional<fc::time_point_sec>();
|
||||
}
|
||||
void on_entry(const timeout& event, game_state_machine_& fsm)
|
||||
|
|
@ -200,7 +201,7 @@ namespace graphene { namespace chain {
|
|||
|
||||
const rock_paper_scissors_game_details& game_details = game_obj->game_details.get<rock_paper_scissors_game_details>();
|
||||
for (unsigned i = 0; i < game_details.commit_moves.size(); ++i)
|
||||
if (!game_details.reveal_moves[i] && i != this_reveal_index)
|
||||
if (game_details.commit_moves[i] && !game_details.reveal_moves[i] && i != this_reveal_index)
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
|
@ -350,7 +351,7 @@ namespace graphene { namespace chain {
|
|||
|
||||
void game_object::evaluate_move_operation(const database& db, const game_move_operation& op) const
|
||||
{
|
||||
const match_object& match_obj = match_id(db);
|
||||
//const match_object& match_obj = match_id(db);
|
||||
|
||||
if (game_details.which() == game_specific_details::tag<rock_paper_scissors_game_details>::value)
|
||||
{
|
||||
|
|
@ -450,18 +451,22 @@ namespace graphene { namespace chain {
|
|||
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<rock_paper_scissors_game_options>();
|
||||
for (unsigned i = 0; i < 2; ++i)
|
||||
|
||||
if (game_options.insurance_enabled)
|
||||
{
|
||||
if (!rps_game_details.commit_moves[i] ||
|
||||
no_player_has_reveal_move)
|
||||
{
|
||||
struct rock_paper_scissors_throw_reveal reveal;
|
||||
reveal.nonce2 = 0;
|
||||
reveal.gesture = (rock_paper_scissors_gesture)db.get_random_bits(game_options.number_of_gestures);
|
||||
rps_game_details.reveal_moves[i] = reveal;
|
||||
ilog("Player ${player} failed to commit a move, generating a random move for them: ${gesture}",
|
||||
("player", i)("gesture", reveal.gesture));
|
||||
}
|
||||
for (unsigned i = 0; i < 2; ++i)
|
||||
{
|
||||
if (!rps_game_details.commit_moves[i] ||
|
||||
no_player_has_reveal_move)
|
||||
{
|
||||
struct rock_paper_scissors_throw_reveal reveal;
|
||||
reveal.nonce2 = 0;
|
||||
reveal.gesture = (rock_paper_scissors_gesture)db.get_random_bits(game_options.number_of_gestures);
|
||||
rps_game_details.reveal_moves[i] = reveal;
|
||||
ilog("Player ${player} failed to commit a move, generating a random move for him: ${gesture}",
|
||||
("player", i)("gesture", reveal.gesture));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -500,8 +505,10 @@ namespace graphene { namespace chain {
|
|||
ilog("Player 0 didn't commit or reveal their move, player 1 wins");
|
||||
winners.insert(players[1]);
|
||||
}
|
||||
else if (rps_game_details.reveal_moves[1])
|
||||
else
|
||||
{
|
||||
ilog("Neither player made a move, both players lose");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -94,22 +94,39 @@ namespace graphene { namespace chain {
|
|||
"Match ${id} is complete",
|
||||
("id", match.id));
|
||||
|
||||
optional<account_id_type> last_game_winner;
|
||||
std::map<account_id_type, unsigned> scores_by_player;
|
||||
for (const flat_set<account_id_type>& game_winners : match.game_winners)
|
||||
for (const account_id_type& account_id : game_winners)
|
||||
{
|
||||
++scores_by_player[account_id];
|
||||
last_game_winner = account_id;
|
||||
}
|
||||
|
||||
bool all_scores_same = true;
|
||||
optional<account_id_type> high_scoring_account;
|
||||
unsigned high_score = 0;
|
||||
for (const auto& value : scores_by_player)
|
||||
if (value.second > high_score)
|
||||
{
|
||||
if (high_scoring_account)
|
||||
all_scores_same = false;
|
||||
high_score = value.second;
|
||||
high_scoring_account = value.first;
|
||||
}
|
||||
|
||||
if (high_scoring_account)
|
||||
match.match_winners.insert(*high_scoring_account);
|
||||
{
|
||||
if (all_scores_same && last_game_winner)
|
||||
match.match_winners.insert(*last_game_winner);
|
||||
else
|
||||
match.match_winners.insert(*high_scoring_account);
|
||||
}
|
||||
else
|
||||
{
|
||||
match.match_winners.insert(match.players[event.db.get_random_bits(match.players.size())]);
|
||||
}
|
||||
|
||||
|
||||
match.end_time = event.db.head_block_time();
|
||||
const tournament_object& tournament_obj = match.tournament_id(event.db);
|
||||
|
|
@ -137,11 +154,16 @@ namespace graphene { namespace chain {
|
|||
|
||||
typedef match_state_machine_ x; // makes transition table cleaner
|
||||
|
||||
// Guards
|
||||
bool was_final_game(const game_complete& event)
|
||||
{
|
||||
const tournament_object& tournament_obj = match_obj->tournament_id(event.db);
|
||||
|
||||
if (match_obj->games.size() >= tournament_obj.options.number_of_wins * 4)
|
||||
{
|
||||
wdump((match_obj->games.size()));
|
||||
return true;
|
||||
}
|
||||
|
||||
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,
|
||||
|
|
|
|||
|
|
@ -199,13 +199,6 @@ namespace graphene { namespace chain {
|
|||
}
|
||||
}
|
||||
|
||||
#ifdef HELPFULL_DUMP_WHEN_SOLVING_BYE_MATCH_PROBLEM
|
||||
wlog("###");
|
||||
wdump((tournament_details_obj.matches[tournament_details_obj.matches.size() - 1]));
|
||||
for( match_id_type mid : tournament_details_obj.matches )
|
||||
wdump((mid(event.db)));
|
||||
#endif
|
||||
|
||||
}
|
||||
void on_entry(const match_completed& event, tournament_state_machine_& fsm)
|
||||
{
|
||||
|
|
@ -241,13 +234,7 @@ namespace graphene { namespace chain {
|
|||
|
||||
event.db.modify(next_round_match, [&](match_object& next_match_obj) {
|
||||
|
||||
#ifdef HELPFULL_DUMP_WHEN_SOLVING_BYE_MATCH_PROBLEM
|
||||
wdump((event.match.get_state()));
|
||||
wdump((event.match));
|
||||
wdump((other_match.get_state()));
|
||||
wdump((other_match));
|
||||
#endif
|
||||
if (!event.match.match_winners.empty()) // if there is a winner
|
||||
if (!event.match.match_winners.empty()) // if there is a winner
|
||||
{
|
||||
if (winner_index_in_next_match == 0)
|
||||
next_match_obj.players.insert(next_match_obj.players.begin(), *event.match.match_winners.begin());
|
||||
|
|
@ -257,10 +244,6 @@ namespace graphene { namespace chain {
|
|||
|
||||
if (other_match.get_state() == match_state::match_complete)
|
||||
{
|
||||
#ifdef HELPFULL_DUMP_WHEN_SOLVING_BYE_MATCH_PROBLEM
|
||||
wdump((next_match_obj.get_state()));
|
||||
wdump((next_match_obj));
|
||||
#endif
|
||||
next_match_obj.on_initiate_match(event.db);
|
||||
}
|
||||
|
||||
|
|
@ -376,21 +359,16 @@ namespace graphene { namespace chain {
|
|||
("value", tournament_obj->registered_players == tournament_obj->options.number_of_players - 1));
|
||||
return tournament_obj->registered_players == tournament_obj->options.number_of_players - 1;
|
||||
}
|
||||
|
||||
|
||||
bool was_final_match(const match_completed& event)
|
||||
{
|
||||
const tournament_details_object& tournament_details_obj = tournament_obj->tournament_details_id(event.db);
|
||||
auto final_match_id = tournament_details_obj.matches[tournament_details_obj.matches.size() - 1];
|
||||
bool was_final = event.match.id == final_match_id;
|
||||
fc_ilog(fc::logger::get("tournament"),
|
||||
"In was_final_match guard, returning ${value}",
|
||||
("value", event.match.id == tournament_details_obj.matches[tournament_details_obj.matches.size()]));
|
||||
#ifdef HELPFULL_DUMP_WHEN_SOLVING_BYE_MATCH_PROBLEM
|
||||
wlog("###");
|
||||
wdump((event.match.id));
|
||||
wdump((tournament_details_obj.matches[tournament_details_obj.matches.size() - 1]));
|
||||
for( match_id_type mid : tournament_details_obj.matches )
|
||||
wdump((mid(event.db)));
|
||||
#endif
|
||||
return event.match.id == tournament_details_obj.matches[tournament_details_obj.matches.size() - 1];
|
||||
("value", was_final));
|
||||
return was_final;
|
||||
}
|
||||
|
||||
void register_player(const player_registered& event)
|
||||
|
|
|
|||
|
|
@ -17,10 +17,6 @@ file(GLOB PERFORMANCE_TESTS "performance/*.cpp")
|
|||
add_executable( performance_test ${PERFORMANCE_TESTS} ${COMMON_SOURCES} )
|
||||
target_link_libraries( performance_test graphene_chain graphene_app graphene_account_history graphene_egenesis_none fc ${PLATFORM_SPECIFIC_LIBS} )
|
||||
|
||||
file(GLOB TOURNAMENT_TESTS "tournament/*.cpp")
|
||||
add_executable( tournament_test ${TOURNAMENT_TESTS} ${COMMON_SOURCES} )
|
||||
target_link_libraries( tournament_test graphene_chain graphene_app graphene_account_history graphene_egenesis_none fc ${PLATFORM_SPECIFIC_LIBS} )
|
||||
|
||||
file(GLOB BENCH_MARKS "benchmarks/*.cpp")
|
||||
add_executable( chain_bench ${BENCH_MARKS} ${COMMON_SOURCES} )
|
||||
target_link_libraries( chain_bench graphene_chain graphene_app graphene_account_history graphene_time graphene_egenesis_none fc ${PLATFORM_SPECIFIC_LIBS} )
|
||||
|
|
@ -33,4 +29,12 @@ file(GLOB INTENSE_SOURCES "intense/*.cpp")
|
|||
add_executable( intense_test ${INTENSE_SOURCES} ${COMMON_SOURCES} )
|
||||
target_link_libraries( intense_test graphene_chain graphene_app graphene_account_history graphene_egenesis_none fc ${PLATFORM_SPECIFIC_LIBS} )
|
||||
|
||||
file(GLOB TOURNAMENT_TESTS "tournament/*.cpp")
|
||||
add_executable( tournament_test ${TOURNAMENT_TESTS} ${COMMON_SOURCES} )
|
||||
target_link_libraries( tournament_test graphene_chain graphene_app graphene_account_history graphene_egenesis_none fc ${PLATFORM_SPECIFIC_LIBS} )
|
||||
|
||||
file(GLOB RANDOM_SOURCES "random/*.cpp")
|
||||
add_executable( random_test ${RANDOM_SOURCES} ${COMMON_SOURCES} )
|
||||
target_link_libraries( random_test graphene_chain graphene_app graphene_egenesis_none fc ${PLATFORM_SPECIFIC_LIBS} )
|
||||
|
||||
add_subdirectory( generate_empty_blocks )
|
||||
|
|
|
|||
|
|
@ -25,7 +25,6 @@
|
|||
#include <fc/crypto/openssl.hpp>
|
||||
#include <openssl/rand.h>
|
||||
|
||||
|
||||
#include <graphene/chain/tournament_object.hpp>
|
||||
#include <graphene/chain/match_object.hpp>
|
||||
#include <graphene/chain/game_object.hpp>
|
||||
|
|
@ -289,13 +288,17 @@ public:
|
|||
tx.validate();
|
||||
tx.set_expiration(db.head_block_time() + fc::seconds( params.block_interval * (params.maintenance_skip_slots + 1) * 3));
|
||||
df.sign(tx, sig_priv_key);
|
||||
PUSH_TX(db, tx);
|
||||
if (game_obj.get_state() == game_state::expecting_commit_moves) // checking again
|
||||
PUSH_TX(db, tx);
|
||||
}
|
||||
|
||||
// spaghetti programming
|
||||
// walking through all tournaments, matches and games and throwing random moves
|
||||
void play_games()
|
||||
// optionaly skip generting randomly selected moves
|
||||
void play_games(unsigned skip_some_commits = 0, unsigned skip_some_reveals = 0)
|
||||
{
|
||||
//try
|
||||
//{
|
||||
graphene::chain::database& db = df.db;
|
||||
const chain_parameters& params = db.get_global_properties().parameters;
|
||||
|
||||
|
|
@ -310,20 +313,25 @@ public:
|
|||
for(const auto& game_id: match.games )
|
||||
{
|
||||
const game_object& game = game_id(db);
|
||||
const rock_paper_scissors_game_details& rps_details = game.game_details.get<rock_paper_scissors_game_details>();
|
||||
if (game.get_state() == game_state::expecting_commit_moves)
|
||||
{
|
||||
for(const auto& player_id: game.players)
|
||||
{
|
||||
if (players_keys.find(player_id) != players_keys.end())
|
||||
if ( players_keys.find(player_id) != players_keys.end())
|
||||
{
|
||||
rps_throw(game_id, player_id, (rock_paper_scissors_gesture) (std::rand() % game_options.number_of_gestures), players_keys[player_id]);
|
||||
if (!skip_some_commits || player_id.instance.value % skip_some_commits != game_id.instance.value % skip_some_commits)
|
||||
{
|
||||
auto iter = std::find(game.players.begin(), game.players.end(), player_id);
|
||||
unsigned player_index = std::distance(game.players.begin(), iter);
|
||||
if (!rps_details.commit_moves.at(player_index))
|
||||
rps_throw(game_id, player_id, (rock_paper_scissors_gesture) (std::rand() % game_options.number_of_gestures), players_keys[player_id]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (game.get_state() == game_state::expecting_reveal_moves)
|
||||
{
|
||||
const rock_paper_scissors_game_details& rps_details = game.game_details.get<rock_paper_scissors_game_details>();
|
||||
|
||||
for (unsigned i = 0; i < 2; ++i)
|
||||
{
|
||||
if (rps_details.commit_moves.at(i) &&
|
||||
|
|
@ -333,27 +341,32 @@ public:
|
|||
if (players_keys.find(player_id) != players_keys.end())
|
||||
{
|
||||
{
|
||||
|
||||
auto iter = committed_game_moves.find(*rps_details.commit_moves.at(i));
|
||||
if (iter != committed_game_moves.end())
|
||||
{
|
||||
const rock_paper_scissors_throw_reveal& reveal = iter->second;
|
||||
|
||||
game_move_operation move_operation;
|
||||
move_operation.game_id = game.id;
|
||||
move_operation.player_account_id = player_id;
|
||||
move_operation.move = reveal;
|
||||
|
||||
signed_transaction tx;
|
||||
tx.operations = {move_operation};
|
||||
for( auto& op : tx.operations )
|
||||
if (!skip_some_reveals || player_id.instance.value % skip_some_reveals != game_id.instance.value % skip_some_reveals)
|
||||
{
|
||||
asset f = db.current_fee_schedule().set_fee(op);
|
||||
players_fees[player_id][f.asset_id] -= f.amount;
|
||||
const rock_paper_scissors_throw_reveal& reveal = iter->second;
|
||||
|
||||
game_move_operation move_operation;
|
||||
move_operation.game_id = game.id;
|
||||
move_operation.player_account_id = player_id;
|
||||
move_operation.move = reveal;
|
||||
|
||||
signed_transaction tx;
|
||||
tx.operations = {move_operation};
|
||||
for( auto& op : tx.operations )
|
||||
{
|
||||
asset f = db.current_fee_schedule().set_fee(op);
|
||||
players_fees[player_id][f.asset_id] -= f.amount;
|
||||
}
|
||||
tx.validate();
|
||||
tx.set_expiration(db.head_block_time() + fc::seconds( params.block_interval * (params.maintenance_skip_slots + 1) * 3));
|
||||
df.sign(tx, players_keys[player_id]);
|
||||
if (game.get_state() == game_state::expecting_reveal_moves) // check again
|
||||
PUSH_TX(db, tx);
|
||||
}
|
||||
tx.validate();
|
||||
tx.set_expiration(db.head_block_time() + fc::seconds( params.block_interval * (params.maintenance_skip_slots + 1) * 3));
|
||||
df.sign(tx, players_keys[player_id]);
|
||||
PUSH_TX(db, tx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -363,6 +376,12 @@ public:
|
|||
}
|
||||
}
|
||||
}
|
||||
//}
|
||||
//catch (fc::exception& e)
|
||||
//{
|
||||
// edump((e.to_detail_string()));
|
||||
// throw;
|
||||
//}
|
||||
}
|
||||
|
||||
private:
|
||||
|
|
@ -447,7 +466,7 @@ BOOST_FIXTURE_TEST_CASE( simple, database_fixture )
|
|||
asset buy_in = asset(12000);
|
||||
tournament_id_type tournament_id;
|
||||
|
||||
BOOST_TEST_MESSAGE( "Preparing a tournament" );
|
||||
BOOST_TEST_MESSAGE( "Preparing a tournament, insurance disabled" );
|
||||
tournament_id = tournament_helper.create_tournament (nathan_id, nathan_priv_key, buy_in, TEST1_NR_OF_PLAYERS_NUMBER);
|
||||
BOOST_REQUIRE(tournament_id == tournament_id_type());
|
||||
|
||||
|
|
@ -458,9 +477,10 @@ BOOST_FIXTURE_TEST_CASE( simple, database_fixture )
|
|||
#endif
|
||||
++tournaments_to_complete;
|
||||
|
||||
BOOST_TEST_MESSAGE( "Preparing another one" );
|
||||
BOOST_TEST_MESSAGE( "Preparing another one, insurance enabled" );
|
||||
buy_in = asset(13000);
|
||||
tournament_id = tournament_helper.create_tournament (nathan_id, nathan_priv_key, buy_in, TEST2_NR_OF_PLAYERS_NUMBER);
|
||||
tournament_id = tournament_helper.create_tournament (nathan_id, nathan_priv_key, buy_in, TEST2_NR_OF_PLAYERS_NUMBER,
|
||||
3, 1, 3, 3600, 3, 3, true);
|
||||
BOOST_REQUIRE(tournament_id == tournament_id_type(1));
|
||||
tournament_helper.join_tournament(tournament_id, alice_id, alice_id, fc::ecc::private_key::regenerate(fc::sha256::hash(string("alice"))), buy_in);
|
||||
tournament_helper.join_tournament(tournament_id, bob_id, bob_id, fc::ecc::private_key::regenerate(fc::sha256::hash(string("bob"))), buy_in);
|
||||
|
|
@ -745,7 +765,7 @@ BOOST_FIXTURE_TEST_CASE( assets, database_fixture )
|
|||
while(tournaments_to_complete > 0)
|
||||
{
|
||||
generate_block();
|
||||
tournament_helper.play_games();
|
||||
tournament_helper.play_games(3, 4);
|
||||
for(const auto& tournament_id: tournaments)
|
||||
{
|
||||
const tournament_object& tournament = tournament_id(db);
|
||||
|
|
|
|||
Loading…
Reference in a new issue