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

...What are you learning now?

初日占い文を作ろう -ちょっと工夫してみる-

最初に

ちょっと、力不足だなぁ…と思う所が多いですが、 多分、ためていても駄目なものだと思うし、メモ程度のものとして、 占い文の自動生成のコードをおいておこうと思いました。

初日占い文の自動生成を考えた話の続き。

初日占い文を作ってみる。 -位置情報の自動生成- - 12A猫で学んだこと-Memoir-

考えたこと

初日占い文を上記のエントリで作成しましたが、最低限の機能しかなかったです。 使うことができるようになるには、工夫がまだまだ必要だと思います。 少し考えてみた結果、自分は最低でも以下の点は欲しいと思いました。

  • 第一犠牲者からの位置情報も欲しい。
  • 位置を書く時に自分のCharacterNameをそのまま書かない。 自分自身のCNは「自分」のように置き換えよう。
  • 挨拶があまりにも単調。 バリエーションを持たせたい。
  • 全ての位置を記載するPlayerはかなり不自然。書く位置のPlayerの数を制限しよう。
  • 感嘆詞・Fillerを活用したい。

実装方針

第一犠牲者からの位置情報

これは合った方がいいですね。バグの修正レベルの内容でしたね。

自分自身のCNの書き換え

凄く簡単。1-2行追加するだけ。

挨拶のバリエーション増加

  • 時間帯に合せた挨拶をする
  • 挨拶を入れるかどうかをランダムにする

上記2点ぐらいを入れたら、少しは単調さがなくなるでしょうか、

書く位置のPlayer数の制限 / 感嘆詞・Filler・接続詞の活用

「位置情報をどのPlayerから記述するか」という問題と絡んで、 少し難しいですね。とりあえず、"よさそうな"例を1つ作ってみて、 細かい所は調整ということにしましょう。

上記の考え方に基づいて作ってみたのが下のスクリプトになります。 うーん、エンターテイメント性に欠けているなぁ…

ソースコード

import os
import sys
import re
import datetime
import math  
import random
import codecs
from collections import defaultdict
from operator import sub, add, mul

import lxml.html

def _get_partition_maplist(target_list, function):
    """ Return the partitions of list 
            function: element -> key of maplist
    """
    ## Not sophisticated code...
    maplist = dict()
    for elem in target_list:
        key = function(elem)
        tmp = maplist.get(key, list()); tmp.append(elem); maplist[key] = tmp
    return maplist

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):
        def _select_greeting_with_time():
            greeting = ""
            hour  = datetime.datetime.now().hour
            if 0 <= hour  <= 3:
                greeting =  "深夜の人狼、楽しみたいと思います。\n"
            elif 4 <= hour <= 11:
                greeting =  "おはようございます。"
            elif 12 <= hour <= 16:
                greeting = "こんにちは!"
            elif 17 <= hour <= 23:
                greeting = "こんばんは。"
            return random.choice([greeting, ""])
        greeting = ""
        greeting += _select_greeting_with_time()
        greeting +=  "占い師の{name}です。".format(name=myname)
        return greeting

    def explain_act(target_name):
        return "気になった{name}さんを占います。".format(name=target_name)

    def explain_result(target_name):
        return "○結果。人狼ではないです。".format(name=target_name)

    def explain_closing(my_name, target_name):
        text = "久しぶりの占い師、楽しませていただきたいと思います。\n"
        text += "特に大人数村の占い師はあまりやったことがないので、ドキドキです。\n"
        text += "配役に狐がいると、同じ人外でも狙う対象が変わったりして、考える幅が広くて面白そうです。\n"
        text += "\nあ...{target_name}さん、霊界観戦楽しんでねー".format(target_name=target_name)
        return text

    def create_position_information(myname, target_name, name_to_pos):
        root2 = 2**0.5
        _basis_map = {(1, 0): "下", (-1, 0): "上", (0, 1):"右", (0, -1):"左"}
        _basis_map.update({(1/root2, 1/root2):"右下", (1/root2, -1/root2): "右上", (-1/root2, 1/root2):"左上", (-1/root2, -1/root2): "左下"})

        def _is_display_position(diff_vector):
            max_norm = max(map(abs, diff_vector))
            return  max_norm <= 2
        
        def _calc_dot(vec1, vec2):
            assert len(vec1) == len(vec2)
            dot = sum(map(mul, vec1, vec2))
            return dot

        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):
                def _to_str(char, number):
                    if number == 1:
                        return char
                    return "{number}つ{char}".format(number=number, char=char)
                position_str = ""
                pair_list = sorted(vector_arrays, key=lambda pair: pair[1])
                string_list = [_to_str(char=vector_map[pair[0]],number=int(pair[1])) for pair in pair_list]
                position_str = "の".join(string_list)
                return position_str
            
            def _add_honorific(name):
                if name != "自分" and name != "第一犠牲者":
                    return name + "さん"
                return name

            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=_add_honorific(target_name) , p_str=position_str)

        def _difflist_to_text(target_name, diff_list, diff_to_name, dist):
            text_list = list()
            for index, diff_vector in enumerate(diff_list):
                name = diff_to_name[diff_vector]
                if name == myname:
                    name = "自分"
                if name != target_name:
                    text_list.append(_convert_to_string(name, diff_vector, _basis_map)) 
            return "、".join(text_list) 

        result_string = ""
        text_lines = list()
        result_string += "\n{target_name}さんの位置は、\n".format(target_name=target_name)
        name_to_pos = {value:key for key, value in position_map.items()}
        target_pos = name_to_pos[target_name]

        diff_to_name = {tuple(map(sub, target_pos, pos)):name for pos, name in position_map.items()}
        diff_list = sorted(diff_to_name.keys()) 

        def _square_norm2(diff_vector):
            return sum([elem**2 for elem in diff_vector]) 
        dist_to_difflist = _get_partition_maplist(diff_list, _square_norm2)
        dist_list = sorted(list(dist_to_difflist.keys()))
        dist_list = list(filter(lambda dist: 0 < dist <= 2**2, dist_list))
        

        for index, dist in enumerate(dist_list):
            diff_list = dist_to_difflist[dist]
            result_string += _difflist_to_text(target_name, diff_list, diff_to_name, dist)
            if index != len(dist_list)-1:
                result_string += "。\n"
            else:
                result_string += "ですね。\n"

        return result_string


    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 = "./example.html" # This is the path for saving html.
    output_file_name = "./output.txt"
    execute(input_file_name, output_file_name)

実例

占いCO アイドルアトリ ○
こんばんは。占い師の境界人レヴァティです。
気になったアイドルアトリさんを占います。
○結果。人狼ではないです。

アイドルアトリさんの位置は、
東天青騎士オルグさんの上、人形アケミさんの左。
ジュリエッタさんの左下。
エニサーモンさんの2つ上ですね。

久しぶりの占い師、楽しませていただきたいと思います。
特に大人数村の占い師はあまりやったことがないので、ドキドキです。
配役に狐がいると、同じ人外でも狙う対象が変わったりして、考える幅が広くて面白そうです。

あ…アイドルアトリさん、霊界観戦楽しんでねー



占いCO 人形アケミ ○
占い師の境界人レヴァティです。
気になった人形アケミさんを占います。
○結果。人狼ではないです。

人形アケミさんの位置は、
ジュリエッタさんの上、アイドルアトリさんの右。
東天青騎士オルグさんの左上。
自分の2つ上ですね。

久しぶりの占い師、楽しませていただきたいと思います。
特に大人数村の占い師はあまりやったことがないので、ドキドキです。
配役に狐がいると、同じ人外でも狙う対象が変わったりして、考える幅が広くて面白そうです。
… …

感想

  • 「プログラムの練習に書いてみましたー」 というような感じになってしまっているなぁ…
  • ソースコードを綺麗にできていないなぁ…
  • PEP8とか守れていないんだろうなぁ…
  • コメントの書き方これでいいのかなぁ…