Mercurial > irccd
changeset 632:e5d0f4289e04
Plugin tictactoe: brand new plugin, closes #393 @6h
author | David Demelier <markand@malikania.fr> |
---|---|
date | Tue, 13 Mar 2018 13:51:17 +0100 |
parents | 1fa9e5222e87 |
children | c07819d1d306 |
files | CHANGES.md doc/html/index.md plugins/CMakeLists.txt plugins/tictactoe/tictactoe.js plugins/tictactoe/tictactoe.md tests/src/plugins/CMakeLists.txt tests/src/plugins/tictactoe/CMakeLists.txt tests/src/plugins/tictactoe/main.cpp |
diffstat | 8 files changed, 738 insertions(+), 1 deletions(-) [+] |
line wrap: on
line diff
--- a/CHANGES.md Wed Mar 07 21:00:56 2018 +0100 +++ b/CHANGES.md Tue Mar 13 13:51:17 2018 +0100 @@ -40,7 +40,8 @@ Plugins: - - Introduce brand new joke plugin (#609). + - Introduce brand new joke plugin (#609), + - Introduce brand new tictactoe plugin (#393). irccd 2.2.0 2017-09-26 ----------------------
--- a/doc/html/index.md Wed Mar 07 21:00:56 2018 +0100 +++ b/doc/html/index.md Tue Mar 13 13:51:17 2018 +0100 @@ -48,6 +48,7 @@ - [logger](plugin/logger.html) - [plugin](plugin/plugin.html) - [roulette](plugin/roulette.html) + - [tictactoe](plugin/tictactoe.html) # Development
--- a/plugins/CMakeLists.txt Wed Mar 07 21:00:56 2018 +0100 +++ b/plugins/CMakeLists.txt Tue Mar 13 13:51:17 2018 +0100 @@ -28,6 +28,7 @@ logger plugin roulette + tictactoe CACHE INTERNAL "" )
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/plugins/tictactoe/tictactoe.js Tue Mar 13 13:51:17 2018 +0100 @@ -0,0 +1,352 @@ +/* + * tictactoe.js -- tictactoe game for IRC + * + * Copyright (c) 2013-2018 David Demelier <markand@malikania.fr> + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +// Plugin information. +info = { + author: "David Demelier <markand@malikania.fr>", + license: "ISC", + summary: "A tictactoe game for IRC", + version: "@IRCCD_VERSION@" +}; + +// Modules. +var Plugin = Irccd.Plugin; +var Util = Irccd.Util; + +// Formats. +Plugin.format = { + "draw": "nobody won", + "invalid": "#{nickname}, please select a valid opponent", + "running": "#{nickname}, the game is already running", + "turn": "#{nickname}, it's your turn", + "used": "#{nickname}, this square is already used", + "win": "#{nickname}, congratulations, you won!" +}; + +/** + * Create a game. + * + * This function creates a game without any checks. + * + * @param server the server object + * @param channel the channel + * @param origin the source origin + * @param target the target nickname + */ +function Game(server, channel, origin, target) +{ + this.server = server; + this.origin = origin; + this.target = target; + this.channel = channel; + this.players = [ Util.splituser(origin), target ]; + this.player = Math.floor(Math.random() * 2); + this.grid = [ + [ '.', '.', '.' ], + [ '.', '.', '.' ], + [ '.', '.', '.' ] + ]; +} + +// Pending games requests checking for names listing. +Game.requests = {}; + +// List of games running. +Game.map = {}; + +/** + * Create a unique id. + * + * @param server the server object + * @param channel the channel + * @return the id + */ +Game.id = function (server, channel) +{ + return channel + "@" + server.toString(); +} + +/** + * Get a running game or undefined. + * + * @param server the server object + * @param channel the channel + * @return the object or undefined if not running + */ +Game.find = function (server, channel) +{ + return Game.map[Game.id(server, channel)]; +} + +/** + * Request a game after the name list gets received. + * + * @param server the server object + * @param channel the channel + * @param origin the originator + * @return the object or undefined if not running + */ +Game.postpone = function (server, channel, origin, target) +{ + /* + * Get list of users on the channel to avoid playing against a non existing + * target. + */ + Game.requests[Game.id(server, channel)] = new Game(server, channel, origin, target); + server.names(channel); +} + +/** + * Populate a set of keywords. + * + * @param server the server object + * @param origin the originator + * @param channel the channel + * @return an object of predefined keywords + */ +Game.keywords = function (server, channel, origin) +{ + var kw = { + channel: channel, + command: server.info().commandChar + Plugin.info().name, + plugin: Plugin.info().name, + server: server.info().name + }; + + if (origin) { + kw.origin = origin; + kw.nickname = Util.splituser(origin); + } + + return kw; +} + +/** + * Tells if a game is pending or running. + * + * @param server the server object + * @param channel the channel + * @return true if any + */ +Game.exists = function (server, channel) +{ + var id = Game.id(server, channel); + + return Game.requests[id] || Game.map[id]; +} + +/** + * Delete a game from the registry. + * + * @param server the server object + * @param channel the channel + */ +Game.remove = function (server, channel) +{ + delete Game.map[Game.id(server, channel)]; +} + +/** + * Erase games when some players leave channels. + * + * @param server the server object + * @param origin the originator + * @param channel the channel + */ +Game.clear = function (server, user, channel) +{ + var nickname = Util.splituser(user); + var game = Game.find(server, channel); + + if (game && (game.players[0] === nickname || game.players[1] === nickname)) + Game.remove(server, channel); +} + +/** + * Show the game grid and the next player line. + */ +Game.prototype.show = function () +{ + var kw = Game.keywords(this.server, this.channel); + + // nickname is the current player. + kw.nickname = this.players[this.player]; + + this.server.message(this.channel, " a b c"); + this.server.message(this.channel, "1 " + this.grid[0].join(" ")); + this.server.message(this.channel, "2 " + this.grid[1].join(" ")); + this.server.message(this.channel, "3 " + this.grid[2].join(" ")); + + if (this.hasWinner()) + this.server.message(this.channel, Util.format(Plugin.format.win, kw)); + else if (this.hasDraw()) + this.server.message(this.channel, Util.format(Plugin.format.draw, kw)); + else + this.server.message(this.channel, Util.format(Plugin.format.turn, kw)); +} + +/** + * Tells if it's the nickname's turn. + * + * @param nickname the nickname to check + * @return true if nickname is allowed to play + */ +Game.prototype.isTurn = function (nickname) +{ + return this.players[this.player] == nickname; +} + +/** + * Place the column and row as the current player. + * + * @param column the column (a, b or c) + * @param row the row (1, 2 or 3) + */ +Game.prototype.place = function (column, row, origin) +{ + var columns = { a: 0, b: 1, c: 2 }; + var rows = { 1: 0, 2: 1, 3: 2 }; + + column = columns[column]; + row = rows[row]; + + var kw = Game.keywords(this.server, this.channel, origin); + + if (this.grid[row][column] !== '.') { + this.server.message(this.channel, Util.format(Plugin.format.used, kw)); + return false; + } + + this.grid[row][column] = this.player === 0 ? 'x' : 'o'; + + // Do not change if game is finished. + if (!this.hasWinner() && !this.hasDraw()) + this.player = this.player === 0 ? 1 : 0; + + return true; +} + +/** + * Check if there is a winner. + * + * @return true if there is a winner + */ +Game.prototype.hasWinner = function () +{ + var lines = [ + [ [ 0, 0 ], [ 0, 1 ], [ 0, 2 ] ], + [ [ 1, 0 ], [ 1, 1 ], [ 1, 2 ] ], + [ [ 2, 0 ], [ 2, 1 ], [ 2, 2 ] ], + [ [ 0, 0 ], [ 1, 0 ], [ 2, 0 ] ], + [ [ 0, 1 ], [ 1, 1 ], [ 2, 1 ] ], + [ [ 0, 2 ], [ 1, 2 ], [ 2, 2 ] ], + [ [ 0, 0 ], [ 1, 1 ], [ 2, 2 ] ], + [ [ 0, 2 ], [ 1, 1 ], [ 2, 0 ] ] + ]; + + for (var i = 0; i < lines.length; ++i) { + var p1 = lines[i][0]; + var p2 = lines[i][1]; + var p3 = lines[i][2]; + + var result = this.grid[p1[0]][p1[1]] === this.grid[p2[0]][p2[1]] && + this.grid[p2[0]][p2[1]] === this.grid[p3[0]][p3[1]] && + this.grid[p3[0]][p3[1]] !== '.'; + + if (result) + return true; + } +} + +/** + * Check if there is draw game. + * + * @return true if game is draw + */ +Game.prototype.hasDraw = function () +{ + for (var r = 0; r < 3; ++r) + for (var c = 0; c < 3; ++c) + if (this.grid[r][c] === '.') + return false; + + return true; +} + +function onNames(server, channel, list) +{ + var id = Game.id(server, channel); + var game = Game.requests[id]; + + // Names can come from any other plugin/event. + if (!game) + return; + + // Not a valid target? destroy the game. + if (list.indexOf(game.target) < 0) + server.message(channel, Util.format(Plugin.format.invalid, + Game.keywords(server, channel, game.origin))); + else { + Game.map[id] = game; + game.show(); + } + + delete Game.requests[id]; +} + +function onCommand(server, origin, channel, message) +{ + var target = message.trim(); + var nickname = Util.splituser(origin); + + if (Game.exists(server, channel)) + server.message(channel, Util.format(Plugin.format.running, Game.keywords(server, channel, origin))); + else if (target === "" || target === nickname || target === server.info().nickname) + server.message(channel, Util.format(Plugin.format.invalid, Game.keywords(server, channel, origin))); + else + Game.postpone(server, channel, origin, message); +} + +function onMessage(server, origin, channel, message) +{ + var nickname = Util.splituser(origin); + var game = Game.find(server, channel); + + if (!game || !game.isTurn(nickname)) + return; + + var match = /([abc]) ([123])/.exec(message); + + if (!match) + return; + + if (game.place(match[1], match[2], origin)) + game.show(); + if (game.hasWinner() || game.hasDraw()) + Game.remove(server, channel); +} + +function onKick(server, origin, channel, target) +{ + Game.clear(server, target, channel); +} + +function onPart(server, origin, channel) +{ + Game.clear(server, origin, channel); +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/plugins/tictactoe/tictactoe.md Tue Mar 13 13:51:17 2018 +0100 @@ -0,0 +1,97 @@ +--- +title: "Tictactoe plugin" +header: "Tictactoe plugin" +guide: yes +--- + +This plugin let you play tictactoe over IRC. + +Warning: this plugin is verbose. + +## Installation + +The plugin **tictactoe** is distributed with irccd. To enable it add the following +to your `plugins` section: + +```ini +[plugins] +tictactoe = "" +``` + +## Usage + +Execute **tictactoe** plugin with the target opponent nickname. Then each player +send a message in the form **x y** where x targets the column and y the row. + +To verify target opponent, this plugins first requests the names on the channel +to ensures a valid player. + +If one of the players leaves the channel (either by kick or part) the game is +aborted. + +```nohighlight +markand: !tictactoe francis +irccd: a b c +irccd: 1 . . . +irccd: 2 . . . +irccd: 3 . . . +irccd: markand, it's your turn +``` + +And then, placing tokens. + +```nohighlight +20:27 < markand> a 1 +20:27 < irccd> a b c +20:27 < irccd> 1 x . . +20:27 < irccd> 2 . . . +20:27 < irccd> 3 . . . +20:27 < irccd> francis, it's your turn +20:27 <@francis> c 1 +20:27 < irccd> a b c +20:27 < irccd> 1 x . o +20:27 < irccd> 2 . . . +20:27 < irccd> 3 . . . +20:27 < irccd> markand, it's your turn +20:27 < markand> a 2 +20:27 < irccd> a b c +20:27 < irccd> 1 x . o +20:27 < irccd> 2 x . . +20:27 < irccd> 3 . . . +20:27 < irccd> francis, it's your turn +20:27 <@francis> c 3 +20:27 < irccd> a b c +20:27 < irccd> 1 x . o +20:27 < irccd> 2 x . . +20:27 < irccd> 3 . . o +20:27 < irccd> markand, it's your turn +20:27 < markand> a 3 +20:27 < irccd> a b c +20:27 < irccd> 1 x . o +20:27 < irccd> 2 x . . +20:27 < irccd> 3 x . o +20:27 < irccd> francis, it's your turn +20:27 < irccd> markand, congratulations, you won! +``` + +## Formats + +The **tictactoe** plugin supports the following formats in `[format.tictactoe]` +section: + + - **draw**: when the game ended with no winner, + - **invalid**: the opponent does not exist or is not valid, + - **running**: the game is already running, + - **turn**: message sent when current player change, + - **used**: the cell requested is already used, + - **win**: game ended with a winner. + +### Keywords supported + +The following keywords are supported: + +| Format | Keywords | Notes | +|---------|--------------------------------------------|-------------| +| (any) | channel, command, nickname, plugin, server | all formats | +| invalid | origin | | +| running | origin | |
--- a/tests/src/plugins/CMakeLists.txt Wed Mar 07 21:00:56 2018 +0100 +++ b/tests/src/plugins/CMakeLists.txt Tue Mar 13 13:51:17 2018 +0100 @@ -23,3 +23,4 @@ add_subdirectory(joke) add_subdirectory(logger) add_subdirectory(plugin) +add_subdirectory(tictactoe)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tests/src/plugins/tictactoe/CMakeLists.txt Tue Mar 13 13:51:17 2018 +0100 @@ -0,0 +1,27 @@ +# +# CMakeLists.txt -- CMake build system for irccd +# +# Copyright (c) 2013-2018 David Demelier <markand@malikania.fr> +# +# Permission to use, copy, modify, and/or distribute this software for any +# purpose with or without fee is hereby granted, provided that the above +# copyright notice and this permission notice appear in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +# + +irccd_define_test( + NAME plugin-tictactoe + SOURCES main.cpp + LIBRARIES libirccd + FLAGS + PLUGIN_NAME="tictactoe" + PLUGIN_PATH="${CMAKE_SOURCE_DIR}/plugins/tictactoe/tictactoe.js" +) +
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tests/src/plugins/tictactoe/main.cpp Tue Mar 13 13:51:17 2018 +0100 @@ -0,0 +1,257 @@ +/* + * main.cpp -- test plugin plugin + * + * Copyright (c) 2013-2018 David Demelier <markand@malikania.fr> + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#define BOOST_TEST_MODULE "Plugin tictactoe" +#include <boost/test/unit_test.hpp> + +#include <irccd/string_util.hpp> + +#include <irccd/daemon/irccd.hpp> +#include <irccd/daemon/server.hpp> +#include <irccd/daemon/service/plugin_service.hpp> + +#include <irccd/test/plugin_test.hpp> + +namespace irccd { + +class test_fixture : public plugin_test { +public: + test_fixture() + : plugin_test(PLUGIN_NAME, PLUGIN_PATH) + { + plugin_->set_formats({ + { "draw", "draw=#{channel}:#{command}:#{nickname}:#{plugin}:#{server}" }, + { "invalid", "invalid=#{channel}:#{command}:#{nickname}:#{origin}:#{plugin}:#{server}" }, + { "running", "running=#{channel}:#{command}:#{nickname}:#{origin}:#{plugin}:#{server}" }, + { "turn", "turn=#{channel}:#{command}:#{nickname}:#{plugin}:#{server}" }, + { "used", "used=#{channel}:#{command}:#{nickname}:#{origin}:#{plugin}:#{server}" }, + { "win", "win=#{channel}:#{command}:#{nickname}:#{plugin}:#{server}" } + }); + } + + auto next_players() const + { + if (server_->cqueue().size() == 0) + throw std::runtime_error("no message"); + + const auto cmd = server_->cqueue().back(); + const auto list = string_util::split(cmd["message"].get<std::string>(), ":"); + + BOOST_TEST(list.size() == 5U); + BOOST_TEST(list[0] == "turn=#tictactoe"); + BOOST_TEST(list[1] == "!tictactoe"); + BOOST_TEST(list[3] == "tictactoe"); + BOOST_TEST(list[4] == "test"); + + return list[2] == "a" ? std::make_pair("a", "b") : std::make_pair("b", "a"); + } + + auto start() + { + plugin_->on_command(irccd_, {server_, "a!a@localhost", "#tictactoe", "b"}); + plugin_->on_names(irccd_, {server_, "#tictactoe", {"a", "b"}}); + + return next_players(); + } + + /** + * Helper to place several tokens on the board and automatically toggling + * players. + * + * This will start the game from "a" with target opponent "b". + * + */ + void run(const std::initializer_list<std::string>& points) + { + auto players = start(); + + for (const auto& p : points) { + server_->cqueue().clear(); + plugin_->on_message(irccd_, {server_, players.first, "#tictactoe", p}); + players = next_players(); + } + } +}; + +BOOST_FIXTURE_TEST_SUITE(test_fixture_suite, test_fixture) + +BOOST_AUTO_TEST_CASE(win) +{ + run({"a 1", "b 1", "a 2", "b 2"}); + + const auto players = next_players(); + + plugin_->on_message(irccd_, {server_, players.first, "#tictactoe", "a 3"}); + + const auto cmd = server_->cqueue().back(); + + BOOST_TEST(cmd.is_object()); + BOOST_TEST(cmd["command"].get<std::string>() == "message"); + BOOST_TEST(cmd["target"].get<std::string>() == "#tictactoe"); + + const auto parts = string_util::split(cmd["message"].get<std::string>(), ":"); + + BOOST_TEST(parts.size() == 5U); + BOOST_TEST(parts[0] == "win=#tictactoe"); + BOOST_TEST(parts[1] == "!tictactoe"); + BOOST_TEST(parts[2] == players.first); + BOOST_TEST(parts[3] == "tictactoe"); + BOOST_TEST(parts[4] == "test"); +} + +BOOST_AUTO_TEST_CASE(draw) +{ + /* + * a b c + * 1 o x o + * 2 o x x + * 3 x o x + */ + run({ "b 2", "c 1", "c 3", "b 3", "c 2", "a 2", "a 3", "a 1" }); + + const auto players = next_players(); + + plugin_->on_message(irccd_, {server_, players.first, "#tictactoe", "b 1"}); + + const auto cmd = server_->cqueue().back(); + + BOOST_TEST(cmd.is_object()); + BOOST_TEST(cmd["command"].get<std::string>() == "message"); + BOOST_TEST(cmd["target"].get<std::string>() == "#tictactoe"); + + const auto parts = string_util::split(cmd["message"].get<std::string>(), ":"); + + BOOST_TEST(parts.size() == 5U); + BOOST_TEST(parts[0] == "draw=#tictactoe"); + BOOST_TEST(parts[1] == "!tictactoe"); + BOOST_TEST(parts[2] == players.first); + BOOST_TEST(parts[3] == "tictactoe"); + BOOST_TEST(parts[4] == "test"); +} + +BOOST_AUTO_TEST_CASE(used) +{ + auto players = start(); + + plugin_->on_message(irccd_, {server_, players.first, "#tictactoe", "a 1"}); + plugin_->on_message(irccd_, {server_, players.second, "#tictactoe", "a 1"}); + + const auto cmd = server_->cqueue().back(); + + BOOST_TEST(cmd.is_object()); + BOOST_TEST(cmd["command"].get<std::string>() == "message"); + BOOST_TEST(cmd["target"].get<std::string>() == "#tictactoe"); + + const auto parts = string_util::split(cmd["message"].get<std::string>(), ":"); + + BOOST_TEST(parts[0] == "used=#tictactoe"); + BOOST_TEST(parts[1] == "!tictactoe"); + BOOST_TEST(parts[2] == players.second); + BOOST_TEST(parts[3] == players.second); + BOOST_TEST(parts[4] == "tictactoe"); + BOOST_TEST(parts[5] == "test"); +} + +BOOST_AUTO_TEST_CASE(invalid) +{ + nlohmann::json cmd; + + // empty name (no names) + plugin_->on_command(irccd_, {server_, "jean", "#tictactoe", ""}); + cmd = server_->cqueue().back(); + + BOOST_TEST(cmd["command"].get<std::string>() == "message"); + BOOST_TEST(cmd["target"].get<std::string>() == "#tictactoe"); + BOOST_TEST(cmd["message"].get<std::string>() == "invalid=#tictactoe:!tictactoe:jean:jean:tictactoe:test"); + + // bot name (no names) + plugin_->on_command(irccd_, {server_, "jean", "#tictactoe", "irccd"}); + cmd = server_->cqueue().back(); + + BOOST_TEST(cmd["command"].get<std::string>() == "message"); + BOOST_TEST(cmd["target"].get<std::string>() == "#tictactoe"); + BOOST_TEST(cmd["message"].get<std::string>() == "invalid=#tictactoe:!tictactoe:jean:jean:tictactoe:test"); + + // target is origin (no names) + plugin_->on_command(irccd_, {server_, server_->nickname(), "#tictactoe", server_->nickname()}); + cmd = server_->cqueue().back(); + + BOOST_TEST(cmd["command"].get<std::string>() == "message"); + BOOST_TEST(cmd["target"].get<std::string>() == "#tictactoe"); + BOOST_TEST(cmd["message"].get<std::string>() == "invalid=#tictactoe:!tictactoe:irccd:irccd:tictactoe:test"); + + // not existing (names) + plugin_->on_command(irccd_, {server_, server_->nickname(), "#tictactoe", server_->nickname()}); + plugin_->on_names(irccd_, {server_, "#tictactoe", {"a", "b", "c"}}); + cmd = server_->cqueue().back(); + + BOOST_TEST(cmd["command"].get<std::string>() == "message"); + BOOST_TEST(cmd["target"].get<std::string>() == "#tictactoe"); + BOOST_TEST(cmd["message"].get<std::string>() == "invalid=#tictactoe:!tictactoe:irccd:irccd:tictactoe:test"); +} + +BOOST_AUTO_TEST_CASE(random) +{ + /* + * Ensure that the first player is not always the originator, start the game + * for at most 1'000'000 times to avoid forever loop. + */ + unsigned count = 0; + bool a = false; + bool b = false; + + // Last player turn is the winner. + while (!a && !b && count++ < 1000000U) { + run({"a 1", "b 1", "a 2", "b 2"}); + + const auto players = next_players(); + + if (players.first == std::string("a")) + a = true; + else + b = true; + + plugin_->on_message(irccd_, {server_, players.first, "#tictactoe", "a 3"}); + } +} + +BOOST_AUTO_TEST_CASE(kick) +{ + auto players = start(); + + server_->cqueue().clear(); + plugin_->on_kick(irccd_, {server_, "kefka", "#tictactoe", players.first, ""}); + plugin_->on_message(irccd_, {server_, players.first, "#tictactoe", "a 1"}); + + BOOST_TEST(server_->cqueue().empty()); +} + +BOOST_AUTO_TEST_CASE(part) +{ + auto players = start(); + + server_->cqueue().clear(); + plugin_->on_part(irccd_, {server_, players.first, "#tictactoe", ""}); + plugin_->on_message(irccd_, {server_, players.first, "#tictactoe", "a 1"}); + + BOOST_TEST(server_->cqueue().empty()); +} + +BOOST_AUTO_TEST_SUITE_END() + +} // !irccd