view plugins/hangman/hangman.js @ 666:c99780476eb7

Misc: rework networking The network_stream and irc classes do not assume that owner is alive anymore by keeping handlers before end of block. Instead, callers postpone deletion of themselves when required to allow handler finishing correctly. Capture all exceptions that can happen in network_stream to make sure handler is called as appropriate in any case. Do the same in irc class. Create a dedicated on_disconnect event in server class which is emitted when the server gets disconnected but is not dead yet.
author David Demelier <markand@malikania.fr>
date Fri, 06 Apr 2018 13:44:20 +0200
parents 27587ff92a64
children 3e816cebed2c
line wrap: on
line source

/*
 * hangman.js -- hangman 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 hangman game for IRC",
    version: "@IRCCD_VERSION@"
};

// Modules.
var Logger = Irccd.Logger;
var File = Irccd.File;
var Plugin = Irccd.Plugin;
var Server = Irccd.Server;
var Unicode = Irccd.Unicode
var Util = Irccd.Util;

// Default options.
Plugin.config["collaborative"] = "true";

// Formats.
Plugin.format = {
    "asked":        "#{nickname}, '#{letter}' was already asked.",
    "dead":         "#{nickname}, fail the word was: #{word}.",
    "found":        "#{nickname}, nice! the word is now #{word}",
    "running":      "#{nickname}, the game is already running and the word is: #{word}",
    "start":        "#{nickname}, the game is started, the word to find is: #{word}",
    "win":          "#{nickname}, congratulations, the word is #{word}.",
    "wrong-word":   "#{nickname}, this is not the word.",
    "wrong-player": "#{nickname}, please wait until someone else proposes.",
    "wrong-letter": "#{nickname}, there is no '#{letter}'."
};

function Hangman(server, channel)
{
    this.server = server;
    this.channel = channel;
    this.tries = 10;
    this.select();
}

/**
 * Map of games.
 */
Hangman.map = {};

/**
 * List of words.
 */
Hangman.words = {
    all: [],        //!< All words,
    registry: {}    //!< Words list per server/channel.
};

/**
 * Search for an existing game.
 *
 * @param server the server object
 * @param channel the channel name
 * @return the hangman instance or undefined if no one exists
 */
Hangman.find = function (server, channel)
{
    return Hangman.map[server.toString() + '@' + channel];
}

/**
 * Create a new game, store it in the map and return it.
 *
 * @param server the server object
 * @param channel the channel name
 * @return the hangman object
 */
Hangman.create = function (server, channel)
{
    return Hangman.map[server.toString() + "@" + channel] = new Hangman(server, channel);
}

/**
 * Remove the specified game from the map.
 *
 * @param game the game to remove
 */
Hangman.remove = function (game)
{
    delete Hangman.map[game.server + "@" + game.channel];
}

/**
 * Check if the text is a valid word.
 *
 * @param word the word to check
 * @return true if a word
 */
Hangman.isWord = function (word)
{
    if (word.length === 0)
        return false;

    for (var i = 0; i < word.length; ++i)
        if (!Unicode.isLetter(word.charCodeAt(i)))
            return false;

    return true;
}

/**
 * Load all words.
 */
Hangman.loadWords = function ()
{
    var path;

    // User specified file?
    if (Plugin.config["file"])
        path = Plugin.config["file"];
    else
        path = Plugin.paths.config + "/words.conf";

    try {
        Logger.info("loading words...");

        var file = new File(path, "r");
        var line;

        while ((line = file.readline()) !== undefined)
            if (Hangman.isWord(line))
                Hangman.words.all.push(line);
    } catch (e) {
        throw new Error("could not open '" + path + "'");
    }

    if (Hangman.words.all.length === 0)
        throw new Error("empty word database");

    Logger.info("number of words in database: " + Hangman.words.all.length);
}

/**
 * Load all formats.
 */
Hangman.loadFormats = function ()
{
    // --- DEPRECATED -------------------------------------------
    //
    // This code will be removed.
    //
    // Since:    2.1.0
    // Until:    3.0.0
    // Reason:    new [format] section replaces it.
    //
    // ----------------------------------------------------------
    for (var key in Plugin.format) {
        var optname = "format-" + key;

        if (typeof (Plugin.config[optname]) !== "string")
            continue;

        if (Plugin.config[optname].length === 0)
            Logger.warning("skipping empty '" + optname + "' format");
        else
            Plugin.format[key] = Plugin.config[optname];
    }
}

/**
 * Select the next word for the game.
 */
Hangman.prototype.select = function ()
{
    var id = this.server.toString() + "@" + this.channel;

    // Reload the words if empty.
    if (!Hangman.words.registry[id] || Hangman.words.registry[id].length === 0)
        Hangman.words.registry[id] = Hangman.words.all.slice(0);

    var i = Math.floor(Math.random() * Hangman.words.registry[id].length);

    this.word = Hangman.words.registry[id][i];

    // Erase words from the registry.
    Hangman.words.registry[id].splice(i, 1);

    // Fill table.
    this.table = {};

    for (var j = 0; j < this.word.length; ++j)
        this.table[this.word.charCodeAt(j)] = false;
}

/**
 * Format the word with underscore and letters.
 *
 * @return the secret
 */
Hangman.prototype.formatWord = function ()
{
    var str = "";

    for (var i = 0; i < this.word.length; ++i) {
        var ch = this.word.charCodeAt(i);

        if (!this.table[ch])
            str += "_";
        else
            str += String.fromCharCode(ch);

        if (i + 1 < this.word.length)
            str += " ";
    }

    return str;
}

/**
 * Propose a word or a letter.
 *
 * @param ch the code point or the unique word
 * @param nickname the user trying
 * @return the status of the game
 */
Hangman.prototype.propose = function (ch, nickname)
{
    var status = "found";

    // Check for collaborative mode.
    if (Plugin.config["collaborative"] === "true" && !this.query) {
        if (this.last !== undefined && this.last === nickname)
            return "wrong-player";

        this.last = nickname;
    }

    if (typeof(ch) == "number") {
        if (this.table[ch] === undefined) {
            this.tries -= 1;
            status = "wrong-letter";
        } else {
            if (this.table[ch]) {
                this.tries -= 1;
                status = "asked";
            } else
                this.table[ch] = true;
        }
    } else {
        if (this.word != ch) {
            this.tries -= 1;
            status = "wrong-word";
        } else
            status = "win";
    }

    // Check if dead.
    if (this.tries <= 0)
        status = "dead";

    // Check if win.
    var win = true;

    for (var i = 0; i < this.word.length; ++i) {
        if (!this.table[this.word.charCodeAt(i)]) {
            win = false;
            break;
        }
    }

    if (win)
        status = "win";

    return status;
}

function onLoad()
{
    Hangman.loadFormats();
    Hangman.loadWords();
}

onReload = onLoad;

function propose(server, channel, origin, game, proposition)
{
    var kw = {
        channel: channel,
        command: server.info().commandChar + Plugin.info().name,
        nickname: Util.splituser(origin),
        origin: origin,
        plugin: Plugin.info().name,
        server: server.toString()
    };

    var st = game.propose(proposition, kw.nickname);

    switch (st) {
    case "found":
        kw.word = game.formatWord();
        server.message(channel, Util.format(Plugin.format["found"], kw));
        break;
    case "wrong-letter":
    case "wrong-player":
    case "wrong-word":
        kw.word = proposition;
    case "asked":
        kw.letter = String.fromCharCode(proposition);
        server.message(channel, Util.format(Plugin.format[st], kw));
        break;
    case "dead":
    case "win":
        kw.word = game.word;
        server.message(channel, Util.format(Plugin.format[st], kw));

        // Remove the game.
        Hangman.remove(game);
        break;
    default:
        break;
    }
}

function onCommand(server, origin, channel, message)
{
    var isquery = server.isSelf(channel);

    if (isquery)
        channel = origin;
    else
        channel = channel.toLowerCase();

    var game = Hangman.find(server, channel);
    var kw = {
        channel: channel,
        command: server.info().commandChar + Plugin.info().name,
        nickname: Util.splituser(origin),
        origin: origin,
        plugin: Plugin.info().name,
        server: server.toString()
    };

    if (game) {
        var list = message.split(" \t");

        if (list.length === 0 || String(list[0]).length === 0) {
            kw.word = game.formatWord();
            server.message(channel, Util.format(Plugin.format["running"], kw));
        } else {
            var word = String(list[0]);

            if (Hangman.isWord(word))
                propose(server, channel, origin, game, word);
        }
    } else {
        game = Hangman.create(server, channel);
        game.query = isquery;
        kw.word = game.formatWord();
        server.message(channel, Util.format(Plugin.format["start"], kw));
    }

    return game;
}

function onMessage(server, origin, channel, message)
{
    if (server.isSelf(channel))
        channel = origin;
    else
        channel = channel.toLowerCase();

    var game = Hangman.find(server, channel);

    if (!game)
        return;

    if (message.length === 1 && Unicode.isLetter(message.charCodeAt(0)))
        propose(server, channel, origin, game, message.charCodeAt(0));
}