gpn-maze-bot/src/main.cc

293 lines
8.8 KiB
C++

#include <boost/asio/any_io_executor.hpp>
#include <boost/asio/awaitable.hpp>
#include <boost/asio/co_spawn.hpp>
#include <boost/asio/connect.hpp>
#include <boost/asio/detached.hpp>
#include <boost/asio/io_context.hpp>
#include <boost/asio/ip/tcp.hpp>
#include <boost/asio/read_until.hpp>
#include <boost/asio/steady_timer.hpp>
#include <boost/asio/streambuf.hpp>
#include <boost/asio/use_awaitable.hpp>
#include <boost/asio/write.hpp>
#include <cmath>
#include <iostream>
#include <map>
#include <random>
#include <set>
#include <sstream>
#include <string>
#include <string_view>
#include <utility>
namespace asio = boost::asio;
enum class Direction : unsigned {
UP = 1,
RIGHT = 2,
DOWN = 4,
LEFT = 8,
};
std::string_view DirectionToString(Direction direction) {
switch (direction) {
case Direction::UP:
return "up";
case Direction::RIGHT:
return "right";
case Direction::DOWN:
return "down";
case Direction::LEFT:
return "left";
}
return "(invalid)";
}
class Bot {
asio::any_io_executor executor;
asio::steady_timer timer;
asio::ip::tcp::socket socket;
std::string server;
std::string name;
std::string pass;
std::random_device generator;
int goal_x, goal_y;
int x, y;
bool up, right, down, left;
std::map<std::pair<int, int>, unsigned> known_map;
int field_width, field_height;
asio::awaitable<void> Protocol();
asio::awaitable<void> Join();
asio::awaitable<bool> Move(Direction dir);
unsigned AStarHeuristic(std::pair<int, int> position);
unsigned ShortestPath(int x, int y);
asio::awaitable<void> ChooseMove();
public:
Bot(asio::any_io_executor executor, std::string_view name,
std::string_view pass, std::string_view server);
};
Bot::Bot(asio::any_io_executor executor, std::string_view name,
std::string_view pass, std::string_view server)
: executor{executor},
timer{executor},
socket{executor},
server{server},
name{name},
pass{pass} {
asio::co_spawn(executor, std::bind(&Bot::Protocol, this), asio::detached);
}
asio::awaitable<void> Bot::Protocol() {
asio::ip::tcp::resolver resolver{executor};
asio::ip::tcp::resolver::query query{server, "4000"};
while (true) {
while (true) {
try {
auto host = co_await resolver.async_resolve(query, asio::use_awaitable);
co_await asio::async_connect(socket, host, asio::use_awaitable);
break;
} catch (std::exception &e) {
std::cout << e.what() << std::endl;
}
timer.expires_after(std::chrono::seconds(1));
co_await timer.async_wait(asio::use_awaitable);
}
try {
while (true) {
asio::streambuf buf;
co_await asio::async_read_until(socket, buf, '\n', asio::use_awaitable);
std::istream is{&buf};
std::string line;
while (std::getline(is, line)) {
std::istringstream iss{line};
std::string field;
char dummy;
std::getline(iss, field, '|');
if (field == "motd") {
std::getline(iss, field);
std::cout << "-> MotD: " << field << std::endl;
co_await Join();
} else if (field == "error") {
std::getline(iss, field);
std::cout << "-> Error: " << field << std::endl;
} else if (field == "game") {
iss >> field_width >> dummy >> field_height >> dummy >> goal_x >>
dummy >> goal_y;
std::cout << "-> Goal is at (" << goal_x << ", " << goal_y << ")"
<< std::endl;
known_map.clear();
} else if (field == "pos") {
iss >> x >> dummy >> y >> dummy >> up >> dummy >> right >> dummy >>
down >> dummy >> left;
std::cout << "-> Position at (" << x << ", " << y << ")"
<< std::endl;
#define X(b) (b ? #b ", " : "")
std::cout << " Walls are (" << X(up) << X(right) << X(down)
<< X(left) << ")" << std::endl;
#undef X
co_await ChooseMove();
} else if (field == "win" || field == "lose") {
int win, lose;
iss >> win >> dummy >> lose;
std::cout << "-> You " << field << '!' << std::endl;
std::cout << " Score: " << win << '/' << lose << std::endl;
} else {
std::cout << "unhandled message: " << field << '|';
std::getline(iss, field);
std::cout << field << std::endl;
}
}
}
} catch (std::exception &e) {
std::cout << e.what() << std::endl;
}
}
co_return;
}
asio::awaitable<void> Bot::Join() {
asio::streambuf buf;
std::ostream os{&buf};
os << "join|" << name << '|' << pass << std::endl;
std::cout << "<- Join as " << name << std::endl;
co_await async_write(socket, buf.data(), asio::use_awaitable);
co_return;
}
asio::awaitable<bool> Bot::Move(Direction direction) {
asio::streambuf buf;
std::ostream os{&buf};
if (direction == Direction::LEFT && left) {
co_return false;
}
if (direction == Direction::UP && up) {
co_return false;
}
if (direction == Direction::RIGHT && right) {
co_return false;
}
if (direction == Direction::DOWN && down) {
co_return false;
}
os << "move|" << DirectionToString(direction) << std::endl;
std::cout << "<- Go " << DirectionToString(direction) << std::endl;
co_await async_write(socket, buf.data(), asio::use_awaitable);
co_return true;
}
unsigned Bot::AStarHeuristic(std::pair<int, int> position) {
return std::abs(position.first - goal_x) + std::abs(position.second - goal_y);
}
unsigned Bot::ShortestPath(int x, int y) {
// A*
std::set<std::pair<int, int>> visited;
std::multimap<unsigned, std::pair<int, int>> queue;
auto goal = std::make_pair(goal_x, goal_y);
queue.emplace(0, std::make_pair(x, y));
while (queue.begin() != queue.end()) {
auto it = queue.begin();
auto distance = it->first;
auto position = it->second;
queue.erase(it);
if (position == goal) {
return distance;
}
if (visited.count(position)) {
continue;
}
if (!(known_map[position] & static_cast<unsigned>(Direction::LEFT)) &&
position.first > 0) {
auto pos = std::make_pair(position.first - 1, position.second);
queue.emplace(distance + 1 + AStarHeuristic(pos), pos);
}
if (!(known_map[position] & static_cast<unsigned>(Direction::RIGHT)) &&
position.first < field_width) {
auto pos = std::make_pair(position.first + 1, position.second);
queue.emplace(distance + 1 + AStarHeuristic(pos), pos);
}
if (!(known_map[position] & static_cast<unsigned>(Direction::UP)) &&
position.second > 0) {
auto pos = std::make_pair(position.first, position.second - 1);
queue.emplace(distance + 1 + AStarHeuristic(pos), pos);
}
if (!(known_map[position] & static_cast<unsigned>(Direction::DOWN)) &&
position.second < field_height) {
auto pos = std::make_pair(position.first, position.second + 1);
queue.emplace(distance + 1 + AStarHeuristic(pos), pos);
}
visited.emplace(position);
}
// should never happen
return -1;
}
asio::awaitable<void> Bot::ChooseMove() {
// update map
known_map[std::make_pair(x, y)] =
(up ? static_cast<unsigned>(Direction::UP) : 0) |
(right ? static_cast<unsigned>(Direction::RIGHT) : 0) |
(down ? static_cast<unsigned>(Direction::DOWN) : 0) |
(left ? static_cast<unsigned>(Direction::LEFT) : 0);
known_map[std::make_pair(x - 1, y)] |=
(left ? static_cast<unsigned>(Direction::RIGHT) : 0);
known_map[std::make_pair(x + 1, y)] |=
(right ? static_cast<unsigned>(Direction::LEFT) : 0);
known_map[std::make_pair(x, y - 1)] |=
(up ? static_cast<unsigned>(Direction::DOWN) : 0);
known_map[std::make_pair(x, y + 1)] |=
(down ? static_cast<unsigned>(Direction::UP) : 0);
// find best move
Direction best;
unsigned best_estimate = -1;
if (!left) {
unsigned estimate = ShortestPath(x - 1, y);
if (estimate < best_estimate) {
best = Direction::LEFT;
best_estimate = estimate;
}
}
if (!right) {
unsigned estimate = ShortestPath(x + 1, y);
if (estimate < best_estimate) {
best = Direction::RIGHT;
best_estimate = estimate;
}
}
if (!up) {
unsigned estimate = ShortestPath(x, y - 1);
if (estimate < best_estimate) {
best = Direction::UP;
best_estimate = estimate;
}
}
if (!down) {
unsigned estimate = ShortestPath(x, y + 1);
if (estimate < best_estimate) {
best = Direction::DOWN;
best_estimate = estimate;
}
}
co_await Move(best);
co_return;
}
int main(int argc, char **argv) {
if (argc < 4) {
std::cerr << "usage: " << argv[0] << " <name> <pass> <server>" << std::endl;
return 1;
}
asio::io_context context;
Bot bot{context.get_executor(), argv[1], argv[2], argv[3]};
context.run();
return 0;
}