// This file is part of Timmi. // // Timmi is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. #include #include #include #include #include #include #include #include #include TimmiServer::TimmiServer(): gs_(), ss_(SERVER_PREGAME), status_changed_(false) { gs_.time_to_go_over = 0; gs_.time_game_started = 0; gs_.c_cards_in_hand = 5; gs_.c_over_delay = 30; gs_.c_deck_count = 1; gs_.c_allow_jokers = 0; } void TimmiServer::mainloop() { while (true) { if (gs_.time_to_go_over != 0) { wait_for_event((gs_.time_to_go_over - time(NULL)) * 1000); } else { wait_for_event(); } do_events(); handle_goover(); if (ss_ == SERVER_INGAME) { handle_winners(); handle_gameend(); } if (status_changed_) { send_status(); } } } void TimmiServer::open_log(std::string filename) { logfile_.open(filename.c_str(), std::ios::app); } void TimmiServer::handle_connection(TCPSocket &socket) { TCPServer::handle_connection(socket); Player &player = gs_.players[socket.getfd()]; player.id = socket.getfd(); player.name = ""; player.hand.clear(); // Made active when name is set player.state = OBSERVER; player.let_go_over = false; std::ostringstream msg(""); msg << "hello " << player.id; send_line(socket, msg.str()); // We don't care to retransmit status for unnamed clients } void TimmiServer::handle_line(TCPSocket &socket, std::string line) { TCPServer::handle_line(socket, line); process_command(socket.getfd(), line); } void TimmiServer::handle_disconnect(TCPSocket &socket) { if (gs_.turn == socket.getfd()) { gs_.turn = next_turn(gs_); } std::ostringstream msg(""); msg << "player " << socket.getfd() << " disconnected"; send_to_all(msg.str()); if (ss_ == SERVER_PREGAME) { gs_.players.erase(socket.getfd()); } else { gs_.players[socket.getfd()].state = DISCONNECTED; } TCPServer::handle_disconnect(socket); status_changed_ = true; } // Private/protected stuff void TimmiServer::log_game() { if (!logfile_.good()) { return; } // Construct to buffer first to avoid partial records on crashes std::ostringstream msg(""); msg << "" << std::endl; msg << " " << datetostring("%Y-%m-%dT%H:%M:%S", gs_.time_game_started) << "" << std::endl; msg << " " << time(NULL) - gs_.time_game_started << "" << std::endl; int i; for (i = 0; i < initial_players_.size(); i++) { int j, position = 0; for (j = 0; j < gs_.winners.size(); j++) { if (gs_.winners.at(j) == initial_players_.at(i).id) { position = j + 1; break; } } msg << " " << initial_players_.at(i).name << "" << std::endl; } msg << " " << gs_.c_cards_in_hand << "" << std::endl; msg << " " << gs_.c_over_delay << "" << std::endl; msg << " " << gs_.c_deck_count << "" << std::endl; msg << " " << gs_.c_allow_jokers << "" << std::endl; msg << "" << std::endl; logfile_ << msg.str(); logfile_.flush(); } void TimmiServer::send_status() { std::ostringstream msg(""); send_to_all(""); // More readable with a blank line send_to_all("status_begin"); std::map::const_iterator iter; for (iter = gs_.players.begin(); iter != gs_.players.end(); iter++) { if (iter->second.name == "") { // Don't transmit unnamed players continue; } msg.str(""); msg << "player " << iter->second.id << " "; switch (iter->second.state) { case ACTIVE: msg << "active"; break; case OBSERVER: msg << "observer"; break; case DISCONNECTED: msg << "disconnected"; break; } msg << " " << iter->second.hand.size() << " "; msg << (iter->second.let_go_over ? "over" : "notover") << " "; msg << iter->second.name; send_to_all(msg.str()); } switch (ss_) { case SERVER_PREGAME: send_to_all("gamestate pregame"); break; case SERVER_INGAME: send_to_all("gamestate ingame"); break; } if (ss_ == SERVER_PREGAME) { msg.str(""); msg << "setting CARDS_IN_HAND " << gs_.c_cards_in_hand; send_to_all(msg.str()); msg.str(""); msg << "setting OVER_DELAY " << gs_.c_over_delay; send_to_all(msg.str()); msg.str(""); msg << "setting DECK_COUNT " << gs_.c_deck_count; send_to_all(msg.str()); msg.str(""); msg << "setting ALLOW_JOKERS " << gs_.c_allow_jokers; send_to_all(msg.str()); } if (ss_ == SERVER_INGAME) { std::vector table(gs_.table); std::sort(table.begin(), table.end(), CMPTable(gs_.trump.suit)); send_to_all(std::string("table ") + table_to_string(table)); msg.str(""); msg << "deck " << gs_.deck.size() << " "; msg << card_to_string(gs_.trump); send_to_all(msg.str()); for (iter = gs_.players.begin(); iter != gs_.players.end(); iter++) { if (iter->second.state == ACTIVE) { std::vector hand(iter->second.hand); std::sort(hand.begin(), hand.end(), CMPSuit(gs_.trump.suit)); send_line(iter->first, std::string("hand ") + hand_to_string(hand)); } } msg.str(""); msg << "turn " << gs_.turn; send_to_all(msg.str()); } send_to_all("status_end"); status_changed_ = false; } void TimmiServer::send_move(const Move &move) { std::ostringstream msg(""); msg << "move " << move.player << " "; switch (move.movetype) { case PICKUP: msg << "pickup"; break; case BEAT: msg << "beat " << card_to_string(move.othercard); msg << " " << card_to_string(move.owncard); break; case HIT: msg << "hit " << card_to_string(move.owncard); break; case MOVE: msg << "move " << card_to_string(move.owncard); break; case NOMOVE: debug(CRITICAL, "Invalid move"); break; } send_to_all(msg.str()); } bool name_in_use(const std::map players, std::string name) { std::map::const_iterator iter; for (iter = players.begin(); iter != players.end(); iter++) { if (iter->second.name == name) { return true; } } return false; } void TimmiServer::process_command(int playerid, std::string line) { std::istringstream stream(line); std::string command(""); stream >> command; if (gs_.players[playerid].name == "") { if (command == "name") { stream.get(); // Remove space std::string basename(""); std::getline(stream, basename); if (!is_valid_utf8(basename)) { send_line(playerid, "error INVALID_UTF8"); return; } std::string name(basename); int append = 2; while (name_in_use(gs_.players, name)) { std::ostringstream namestream(""); namestream << basename << append; name = namestream.str(); append += 1; } gs_.players[playerid].name = name; // We need an immediate status update before sending join message send_status(); std::ostringstream msg(""); msg << "player " << playerid << " join"; send_to_all(msg.str()); if (ss_ == SERVER_PREGAME) { gs_.players[playerid].state = ACTIVE; } } else { send_line(playerid, "error NAME_NOT_SET"); } return; } if (command == "chat") { std::string message(""); std::getline(stream, message); if (is_valid_utf8(message)) { std::ostringstream msg(""); msg << "chat " << playerid << message; send_to_all(msg.str()); } else { send_line(playerid, "error INVALID_UTF8"); } return; } if (command == "status") { send_status(); return; } if (command == "observer" && gs_.players[playerid].state == ACTIVE) { gs_.players[playerid].state = OBSERVER; gs_.players[playerid].hand.clear(); if (gs_.turn == playerid) { gs_.turn = next_turn(gs_); } std::ostringstream msg(""); msg << "player " << playerid << " observer"; send_to_all(msg.str()); return; } if (ss_ == SERVER_PREGAME) { // FIXME: Count of active players, not all players if (command == "start" && gs_.players.size() > 1) { ss_ = SERVER_INGAME; store_initial_players(); start_game(gs_); status_changed_ = true; return; } else if (command == "active" && gs_.players[playerid].state == OBSERVER) { gs_.players[playerid].state = ACTIVE; std::ostringstream msg(""); msg << "player " << playerid << " active"; send_to_all(msg.str()); return; } else if (command == "setting") { std::string setting(""); int value; stream >> setting >> value; if (setting == "CARDS_IN_HAND" && value >= 1 && value <= 1000) { gs_.c_cards_in_hand = value; status_changed_ = true; return; } else if (setting == "OVER_DELAY" && value >= 10 && value <= 1000) { gs_.c_cards_in_hand = value; status_changed_ = true; return; } else if (setting == "DECK_COUNT" && value >= 1 && value <= 1000) { gs_.c_deck_count = value; status_changed_ = true; return; } else if (setting == "ALLOW_JOKERS") { gs_.c_allow_jokers = value ? 1 : 0; status_changed_ = true; return; } else { send_line(playerid, "error INVALID_SETTING"); return; } } } if (ss_ == SERVER_INGAME) { if (command == "over" && all_beaten(gs_.table)) { gs_.players[playerid].let_go_over = true; status_changed_ = true; return; } Move move = {playerid, NOMOVE, {NOJOKER, NOSUIT, NORANK}, {NOJOKER, NOSUIT, NORANK}}; if (command == "pickup") { move.movetype = PICKUP; } else if (command == "hit") { std::string card(""); stream >> card; move.movetype = HIT; move.owncard = string_to_card(card); if (!is_card(move.owncard) && is_joker(move.owncard)) { move.owncard = assign_joker_hit(gs_.table, move.owncard, gs_.trump.suit); } } else if (command == "beat") { std::string tobeat(""); std::string beatwith(""); move.movetype = BEAT; stream >> tobeat >> beatwith; move.owncard = string_to_card(beatwith); move.othercard = string_to_card(tobeat); if (!is_card(move.owncard) && is_joker(move.owncard)) { move.owncard = assign_joker_beat(gs_.table, move.owncard, move.othercard, gs_.trump.suit); } } else if (command == "move") { std::string card(""); stream >> card; move.movetype = MOVE; move.owncard = string_to_card(card); if (!is_card(move.owncard) && is_joker(move.owncard)) { move.owncard = assign_joker_hit(gs_.table, move.owncard, gs_.trump.suit); } } if (move.movetype != NOMOVE) { if (legal_move(gs_, move)) { apply_move(gs_, move); send_move(move); status_changed_ = true; } else { send_line(playerid, "error INVALID_MOVE"); } return; } } send_line(playerid, "error INVALID_COMMAND"); } void TimmiServer::handle_goover() { if (ss_ != SERVER_INGAME) { return; } if (all_beaten(gs_.table)) { OverType otype = can_go_over(gs_); if (otype == NOT_OVER) { if (gs_.time_to_go_over == 0) { gs_.time_to_go_over = time(NULL) + gs_.c_over_delay; } } else { switch (otype) { case OVER_CANTADD: send_to_all("over cantadd"); break; case OVER_LETGOOVER: send_to_all("over letgoover"); break; case OVER_TIMEOUT: send_to_all("over timeout"); break; default: debug(CRITICAL, "Invalid OverType"); } put_over(gs_); status_changed_ = true; } } else if (gs_.time_to_go_over != 0) { gs_.time_to_go_over = 0; } } void TimmiServer::handle_gameend() { if (ss_ == SERVER_INGAME && game_ended(gs_)) { std::ostringstream msg(""); int i; for (i = 0; i < initial_players_.size(); i++) { int j; bool found = false; for (j = 0; j < gs_.winners.size(); j++) { if (gs_.winners.at(j) == initial_players_.at(i).id) { found = true; break; } } if (!found) { gs_.winners.push_back(initial_players_.at(i).id); } } msg << "winners"; for (i = 0; i < gs_.winners.size(); i++) { msg << " " << gs_.winners.at(i); } send_to_all(msg.str()); log_game(); // Back to pregame gs_.winners.clear(); gs_.table.clear(); gs_.time_to_go_over = 0; gs_.deck.clear(); // Make observers active players and remove disconnected std::map::iterator iter, nextiter; for (iter = gs_.players.begin(); iter != gs_.players.end(); iter = nextiter) { if (iter->second.state == OBSERVER && iter->second.name != "") { iter->second.state = ACTIVE; std::ostringstream msg(""); msg << "player " << iter->first << " active"; send_to_all(msg.str()); } iter->second.hand.clear(); nextiter = iter; // Take next iterator before removing current nextiter++; if (iter->second.state == DISCONNECTED) { gs_.players.erase(iter); } } ss_ = SERVER_PREGAME; status_changed_ = true; } } void TimmiServer::handle_winners() { // This substitutes timmilogic's check_winners to add logging if (gs_.deck.size() != 0) { return; } std::map::iterator iter; for (iter = gs_.players.begin(); iter != gs_.players.end(); iter++) { if (iter->second.hand.size() == 0 && iter->second.state == ACTIVE) { gs_.winners.push_back(iter->first); iter->second.state = OBSERVER; if (gs_.turn == iter->first) { gs_.turn = next_turn(gs_); } std::ostringstream msg(""); msg << "player " << iter->first << " won"; send_to_all(msg.str()); } } } void TimmiServer::store_initial_players() { initial_players_.clear(); LogPlayer player; std::map::const_iterator iter; for (iter = gs_.players.begin(); iter != gs_.players.end(); iter++) { if (iter->second.state != ACTIVE) { continue; } player.id = iter->first; player.name = iter->second.name; player.address = find_socket(iter->first)->getaddr(); initial_players_.push_back(player); } }