12A猫で学んだこと-Memoir-

...What are you learning now?

初日占い文を作ってみる。 -位置情報の自動生成-

はじめに

12A猫の初日の占い文は、参加者の方の個性があって面白いですよね。
12A猫とは違って、狐入りの村の初日の占い文は、真偽判定に使われることが多いですね。EXCELプログラミング言語で占い文の自動生成をされている方は結構いらっしゃるみたいです。 最近、面白い自動生成を見せていただきました。

作成された方に聞いた所、JAVAで作成されたようです。 このエントリでは、るる鯖のゲームシステムに適合するように、 初日の占い文の生成をpythonを使ってやってみたいと思います。

目的

初日占い文を python + lxml で作成する。

運用手法

  1. .html ファイルを保存
  2. .html ファイルを読み込み、占い文を .txt に保存する
  3. .txt の中から、Copy & Pasteで対象の人の占い文を貼付ける

道具

機能

  • 自分の名前を自動抽出
  • 他プレイヤーの名前を自動抽出
  • 位置情報自動出力

ソースコード

とりあえず、最低限のものを作ろうとしたら以下のようなソースコードでしょうか。

import os
import sys
import re
import math  
import codecs
from operator import sub, add, mul

import lxml.html

def get_myname(root):
    """ Return my name.
    """
    nodes = root.xpath('//td[@class="occupation"]//span[@class="name"][position()=1]')
    assert len(nodes) == 1
    node = nodes[0]
    return node.text

def get_position_map(root):
    """ Return the position map: position -> name.
    "       position: 2D tuple.
    "       name: participant's name.
    "
    " Left upper is (0, 0), and the format is (vertical_index, horizontal_index).   
    """
    table_nodes = root.xpath('//table[@class="iconsmall"]')
    assert len(table_nodes) == 1
    table_node = table_nodes[0]
    name_nodes = table_node.xpath('//td[@class="name"]')
    position_map = dict()
    for index, name_node in enumerate(name_nodes):
        horizontal_index = index // 2
        vertical_index  = index % 2
        position_map[(horizontal_index, vertical_index)] = name_node.text_content()
    return position_map

def create_document(myname, target_name, position_map):
    """ Create the sentence of foreseener.
    " @param str myname: your name.
    " @param str target_name :  the target of foreseeing.
    " @param dict position_map :  position -> name.
    """
    def create_header(target_name):
        return "占いCO {name} ○".format(name=target_name)

    def create_greeting(myname):
        return "占い師の{name}です。".format(name=myname)

    def explain_act(target_name):
        return "初日なので適当に{name}を占います。".format(name=target_name)

    def explain_result(target_name):
        return "結果は○。{name}は人狼ではありませんでした。".format(name=target_name)

    def explain_closing(my_name, target_name):
        return "{target_name}...溶けましたね。".format(target_name=target_name)

    def create_position_information(myname, target_name, name_to_pos):
        _basis_map = {(1, 0): "下", (-1, 0): "上", (0, 1):"右", (0, -1):"左"}

        def _calc_dot(vec1, vec2):
            assert len(vec1) == len(vec2)
            dot = sum(map(mul, vec1, vec2))
            return dot

        def _dist(vector1, vector2):
            diff_vec = list(map(sub, vector1, vector2))
            norm1 = sum(map(abs, diff_vec))
            angle = math.atan2(diff_vec[1], diff_vec[0])
            return (norm1, angle)

        def _convert_to_string(target_name, diff_vector, basis_map):

            def get_vector_array(vector, ref_vectors):
                square_norm2 = sum(map(mul, vector, vector))
                if square_norm2 < 1:
                    return []
                result = list()
                best_basis = max(ref_vectors, key=lambda ref_vec: _calc_dot(ref_vec, vector))
                square_norm_best_basis = sum(map(mul, best_basis, best_basis))
                best_coef = _calc_dot(vector, best_basis)  / square_norm_best_basis
                best_pair = (best_basis, best_coef)
                best_vector = list(map(lambda elem: best_coef * elem, best_basis))
                residual = list(map(sub, vector, best_vector))
                return [best_pair] + get_vector_array(residual, ref_vectors)

            def to_position_string(vector_arrays, vector_map):
                position_str = ""
                pair_list = sorted(vector_arrays, key=lambda pair: pair[1])
                string_list = ["{char}に{number}つ".format(char=vector_map[pair[0]],number=int(pair[1])) for pair in pair_list]
                position_str = "、".join(string_list)
                position_str += "移動した位置"
                return position_str


            ref_basis = list(basis_map.keys())
            pairs = get_vector_array(diff_vector, ref_basis)
            position_str = to_position_string(pairs, basis_map)
            return "{name}から、{p_str}。".format(name=target_name , p_str=position_str)

        text_lines = list()
        text_lines.append("○結果なので位置情報を書きます。\n")
        text_lines.append("{target_name}の位置は、".format(target_name=target_name))
        name_to_pos = {value:key for key, value in position_map.items()}
        target_pos = name_to_pos[target_name]
        position_list = sorted(position_map.keys(), key=lambda pos: _dist(pos, target_pos))
        for position in position_list:
            name = position_map[position]
            diff_vector = list(map(sub, target_pos, position))
            if name != target_name and name != "第一犠牲者":
                text_lines.append(_convert_to_string(name, diff_vector, _basis_map))
        return "\n".join(text_lines)

    lines = list()
    name_to_pos = {value:key for key, value in position_map.items()}
    target_pos = name_to_pos[target_name]
    lines.append(create_header(target_name))
    lines.append(create_greeting(myname))
    lines.append(explain_act(target_name))
    lines.append(explain_result(target_name))
    lines.append(create_position_information(myname, target_name, name_to_pos))
    lines.append(explain_closing(myname, target_name))
    lines = [line + '\n' for line in lines]

    return lines

def execute(input_file_name, output_file_name):
    """ Created documents from input_file_name and output.
    "@param str input_file_name: the path to .html file.
    "@param str output_file_name: the path to output_file.
    """
    # Acquire the information by interpreting .html file.
    html  = codecs.open(input_file_name, 'r','utf-8').read()
    root = lxml.html.fromstring(html)

    myname = get_myname(root)
    position_map = get_position_map(root)

    # Create documents for each player other than myself and the first victim.
    output_lines = list()
    pos_list = sorted(position_map.keys())
    for pos in pos_list:
        name = position_map[pos]
        if name == myname or name == "第一犠牲者":
            continue
        output_lines += create_document(myname, name, position_map)
        output_lines += ["\n\n"]

    with open(output_file_name, "wt") as fp:
        fp.writelines(output_lines)


if __name__ == "__main__":
    input_file_name = "./test.html" # This is the path for saving html.
    output_file_name = "./output.txt"
    execute(input_file_name, output_file_name)



使用例

f:id:StudentS:20170423200950p:plain

上記のような.htmlファイルを保存して、 input_file_name に指定して、 プログラムを実行します。

占いCO アイドルアトリ ○
占い師の境界人レヴァティです。
初日なので適当にアイドルアトリを占います。
結果は○。アイドルアトリは人狼ではありませんでした。
○結果なので位置情報を書きます。


アイドルアトリの位置は、
東天青騎士オルグから、上に1つ移動した位置。
人形アケミから、左に1つ移動した位置。
エニサーモンから、上に2つ移動した位置。
ジュリエッタから、左に1つ、上に1つ移動した位置。
新米ワカバから、上に3つ移動した位置。
境界人レヴァティから、左に1つ、上に2つ移動した位置。
留守番シィから、上に4つ移動した位置。
夢遊病フェネから、上に5つ移動した位置。
漫画家ミモザから、左に1つ、上に4つ移動した位置。 魔女マーヤから、左に1つ、上に5つ移動した位置。
アイドルアトリ…溶けましたね。


占いCO 人形アケミ ○
占い師の境界人レヴァティです。
初日なので適当に人形アケミを占います。
結果は○。人形アケミは人狼ではありませんでした。
○結果なので位置情報を書きます。

人形アケミの位置は、
アイドルアトリから、右に1つ移動した位置。
ジュリエッタから、上に1つ移動した位置。
東天青騎士オルグから、右に1つ、上に1つ移動した位置。
境界人レヴァティから、上に2つ移動した位置。
エニサーモンから、右に1つ、上に2つ移動した位置。
新米ワカバから、右に1つ、上に3つ移動した位置。
漫画家ミモザから、上に4つ移動した位置。
留守番シィから、右に1つ、上に4つ移動した位置。
魔女マーヤから、上に5つ移動した位置。
夢遊病フェネから、右に1つ、上に5つ移動した位置。
人形アケミ…溶けましたね。

注意

  • ブラウザによって、保存内容 / 形式が異なります。

ブラウザによって、 参加者の情報や自分の情報を.htmlファイルを保存することができるか が異なるみたいです。 自分はGoogle Chromeを使って、保存を行いました。

  • ソースコードは utf8形式での保存が必要だと思います。

  • lxml をインストールしないと上記のスクリプトは作動しません。

    • 基本的には pip install lxml ただ環境によっては駄目かも…
  • いいソースコードとは思えないけれど、許してください。

さて、今回の生成文はいかにも「自動生成しました!」という感じが 出ていますね。これを改造すると、それなりに人間が書いたような文章に近づけることができると思っています。
ちょっと考えてみたいなー と思っています。