Groongaの対話モードをラップするgrnwrap

Groongaには素敵なWebベースの管理画面が付属していますが、ターミナルから対話モードで動作を確かめたいことも(私は)けっこうあります。
がしかし、この対話モードでは履歴呼び出しやカーソル移動が使えません。それと結果のJSONが一行なのでたいへん見づらい。
困ったのでPythonで入出力をラップするスクリプトを書きました。動作環境として、Python 2.6以上(それ以前の場合はsimplejsonをインストール)がreadlineモジュールつきでコンパイルされていることが必要です。

michisu/grnwrap at master - GitHub

機能

  • カーソル移動とか
  • 履歴の保存
  • タブキーでコマンド名、引数名、テーブル名、ファイルパスの補完
  • JSONのインデント表示
  • Gオプションでselectの結果をMySQLの\G風にフォーマット
  • *.grnなファイルパスを入力するとファイルに書かれたGroongaコマンドを実行

引数名(--filterとか)の補完は環境によって動作が変わってしまうようなので、デフォルトでは無効の場合があります。無効になっているけど試したい場合は-cオプションをつけて起動してみてください。

使い方

$ grnwrap 

実行例

$ grnwrap -G /tmp/tutorial.db
> select --table Site --query title:@this
******************************* 1. row *******************************
     _id: 1
    _key: http://example.org/
   title: This is test record 1!
1 records / 1 hits (0.000823 sec)
> select --table Site --query title:@test --output_columns _id,_score,title --sortby _score
******************************* 1. row *******************************
      _id: 1
   _score: 1
    title: This is test record 1!
******************************* 2. row *******************************
      _id: 2
   _score: 1
    title: test record 2.
******************************* 3. row *******************************
      _id: 4
   _score: 1
    title: test record four.
******************************* 4. row *******************************
      _id: 3
   _score: 2
    title: test test record three.
******************************* 5. row *******************************
      _id: 9
   _score: 2
    title: test test record nine.
******************************* 6. row *******************************
      _id: 8
   _score: 2
    title: test test record eight.
******************************* 7. row *******************************
      _id: 7
   _score: 3
    title: test test test record seven.
******************************* 8. row *******************************
      _id: 5
   _score: 3
    title: test test test record five.
******************************* 9. row *******************************
      _id: 6
   _score: 4
    title: test test test test record six.
9 records / 9 hits (0.007133 sec)

TopCoder SRM 502

Div2 Easy: TheProgrammingContestDivTwo

時間内に複数のタスクをこなさなければならない。順序は任意。一つのタスクをこなすと、そのタスクが終わった時点での経過時間がペナルティとして加算される。制限時間Tと各タスクの必要時間requiredTimeが与えられた時、こなせる最大のタスク数と累計のペナルティを答える。

import java.util.Arrays;

public class TheProgrammingContestDivTwo {

	public int[] find(int T, int[] requiredTime) {
		int solved = 0;
		int passed = 0;
		int t = 0;
		Arrays.sort(requiredTime);
		for (int i = 0; i < requiredTime.length; i++) {
			if (T < passed + requiredTime[i]) break;
			solved++;
			passed += requiredTime[i];
			t += passed;
		}
		int[] res = {solved, t};
		return res;
	}
	
}

テスト実行しながら何回か書き直しつつ正解。一発で書きたいなぁ。英語の問題文がぱっと頭に入ってこないのが問題。

Div2 Medium: TheLotteryBothDivs

くじを一枚持っている。くじは"000000000"から"999999999"までの10桁の数字からなる文字列で表されるとする。この時、1桁から10桁までの文字列(suffix)がいくつか与えられ、くじ番号の末尾にそのどれかが一致すれば当たりである。与えられた文字列配列から、当たりの確率を答える。ただし、浮動小数点演算による1e-9以下の誤差は許される。

import java.util.Arrays;
import java.util.Comparator;
import java.util.HashSet;
import java.util.Set;

class StringLengthComparator implements Comparator<String> {
	public int compare(String o1, String o2) {
		if (o1.length() < o2.length()) {
			return -1;
		} else if (o2.length() < o1.length()) {
			return 1;
		} else {
			return 0;
		}
	}
}

public class TheLotteryBothDivs {

	public double find(String[] goodSuffixes) {
		double res = 0.0;
		Set<String> set = new HashSet<String>();
		Arrays.sort(goodSuffixes, new StringLengthComparator());
		for (String suffix : goodSuffixes) {
			boolean ignore = false;
			for (int i = 1; i <= suffix.length(); i++) {
				String check = suffix.substring(suffix.length()-i, suffix.length());
				ignore = set.contains(check);
				if (ignore) break;
			}
			if (!ignore) {
				set.add(suffix);
				res += Math.pow(0.1, suffix.length());
			}
		}
		return res;
	}
	
}

例えば {"1", "2"} が渡されると当たりの確率は0.2。 {"01", "02"} であれば0.02。{"1", "2", "01", "02"} だったら、長いほうの文字列は確率に影響を与えないので0.2のまま。
文字列を短い順に並び替えて、憶えておく。前に共通のサフィックスを使う文字列が現れていたら、確率には加算しない。文字列の種類は最大でも50と少ないから、トライ木のようなデータ構造を使わなくても一文字ずつ削りながら処理していけばOK。
Javaで比較処理のもっと簡単な書き方ってないっけ。。

Div2 Hard: TheCowDivTwo

飼っている牛の数と逃げ出した牛の数が渡される。逃げ出した牛の組み合わせは何通りあるか、という問題。
組み合わせの定義を調べてやってみたがうまく動かず、時間切れ。やっぱり中学数学からやり直しが必要だな。。

結果

EasyとMediumの二問正解で491.79pt。Div2 758人中143位。レーティングは922から979にアップ。次で4桁になれれば初回挑戦以来だ!

Google Code Jam勉強会 第一回

Google Code Jam(GCJ)の勉強会に参加しました。講師はおなじみ id:chokudai 先生。
簡単な説明の後で、二問実習し、問題の後にchokudaiさんのライブコーディング、という流れ。
ちなみに二回目が4/19 18:30〜に予定されていますよ。

説明と方針

以下、chokudaiさんの解説まとめ。

GCJ用の特別な対策は基本的には必要ない。TopCoderとの違いは、時間制限が長いこと、入出力がファイルであること、実行環境がローカルであること、使える言語が自由であること。言語は自由だが処理速度の差が出るので必ず計算量を意識するべし。

各問、SmallとLargeの二種類のデータセットがある。それぞれダウンロードして4分/8分以内に結果を提出しなければならない。Largeは実行に時間がかかるので、ナイーブな実装では間に合わないことがある。
Smallを正解すると10ポイント、Largeで23ポイントが与えられる。Smallは何回も試せるが、Largeは一回しかできないので注意。
33点が予選ラウンド通過スコアなので、Largeを一問解けば(Smallも解けるので)通過できる。逆にSmallが三問解けても点数が足りない。Largeをしっかり一問解けるかどうかが鍵。

そして実習へ。

GCJ Africa and Arabia 2011 Qualification Round A. Closing the Loop

赤と青のロープを交互に結び合わせて輪を作る。結び目の箇所には両方のロープ合わせて1cmを使う。与えられた長さと本数がまちまちのロープから最大何cmの輪を作れるか、という問題。
GCJの問題は初めてだったので入力の書き方をどうするか悩みながらも、問題自体は特に問題なく解けた。赤と青の本数の小さい方を取り、その本数分、長いロープから使っていく。

import sys
def readline():
    return sys.stdin.readline().strip()

num_case = int(readline())
for i in range(num_case):
    red_lopes = []
    blue_lopes = []
    num_lope = int(readline())
    lopes = readline().split()
    for lope in lopes:
        color = lope[-1]
        length = int(lope[:-1])
        if color == 'R':
            red_lopes.append(length)
        else:
            blue_lopes.append(length)
    red_lopes = sorted(red_lopes)
    red_lopes.reverse()
    blue_lopes = sorted(blue_lopes)
    blue_lopes.reverse()
    num_used = min(len(red_lopes), len(blue_lopes))
    len_knots = num_used * 2
    len_lopes = sum(red_lopes[:num_used]) + sum(blue_lopes[:num_used])
    print 'Case #%s: %s' % (i+1, len_lopes - len_knots)

GCJ 2010 Qualification Round C. Theme Park

ローラーコースターがR回運行する。一度に乗せられる人数はk人。乗客はN個のグループが並んでおり、同じグループの人はまとめて乗せなければならない。また、順番を飛ばすこともできない。つまり、k=5の時に、{2, 2, 3, 1}人ずつのグループがあったとすると、最初に乗せられるのは4人。乗り終わったグループはまた列の最後に並ぶことになる。R回運行した後の運賃合計(1人1ユーロ)を答える。

LargeではRが最大10の8乗。まともにR回シミュレーションをしていると、8分以内に終わることはできない。シミュレーション中にループする箇所が出てきて、その分をまとめてスキップしてあげれば最短の時間で処理できるだろう・・・というのはすぐに思いついた。

しかし残念ながら勉強会の時間内には解けなかった。帰宅してからやっと書けたが、一度で全グループを乗せられてしまうケースへの対処に苦労した。プログラム的に難しい問題だと思う。

import sys
def rl(conv=str):
    return conv(sys.stdin.readline().strip())

num_cases = rl(int)
for i in range(num_cases):
    R, k, N = map(int, rl().split())
    groups = map(int, rl().split())
    sum_r = 0
    sum_p = 0
    index = 0
    start = 0
    starts = {}
    skiped = False
    p = 0
    while sum_r < R:
        g = groups[index]
        p += g
        index += 1
        if len(groups) == index:
            index = 0
        if k < p + groups[index] or index == start:
            sum_r += 1
            sum_p += p
            if not skiped and index in starts:
                prev_p, prev_r = starts[index]
                jump_p = sum_p - prev_p
                jump_r = sum_r - prev_r
                num_jump = (R - sum_r) / jump_r
                sum_p += num_jump * jump_p
                sum_r += num_jump * jump_r
                skiped = True
            starts[start] = (sum_p - p, sum_r - 1)
            start = index
            p = 0
    print 'Case #%s: %s' % (i + 1, sum_p)

(4/7)最初に載せたものは不要な変数やimport文が残っていたので、整理した。

ほぼ一発で動くライブコーディングはさすが。

TopCoder SRM 501

Div2 Easy: FoxProgression(Java

与えられた数列に追加すると、その数列が等差数列または等比数列になるような数は何種類あるか、という問題。
先頭に入れるのもありかと思って時間がかかったが、後ろにappendする場合だけを考慮すればいいらしい。

public class FoxProgression {
	public int theCount(int[] seq) {
		if (seq.length == 1) return -1;
		int last = 0;
		int[] diffs = new int[seq.length-1];
		double[] ratios = new double[seq.length-1];
		boolean ari = true;
		boolean geo = true;
		for (int i = 1; i < seq.length; i++) {
			diffs[i-1] = seq[i] - seq[i-1];
			ratios[i-1] = (double)seq[i] / seq[i-1];
			if (seq[i-1] * (int)ratios[i-1] != seq[i]) geo = false;
			last = seq[i];
		}
		for (int i = 1; i < diffs.length; i++) {
			if (diffs[i] != diffs[i-1]) ari = false;
		}
		for (int i = 1; i < ratios.length; i++) {
			if (ratios[i] != ratios[i-1]) geo = false;
		}
		if (!ari && !geo) return 0;
		if (ari && geo) {
			if (last + diffs[0] == last * ratios[0]) {
				return 1;
			} else {
				return 2;
			}
		}
		return 1;
	}
}

Div2 Medium: FoxPlayingGame(Java

スコア0から始めて、任意の順序でparamA / 1000.0をnA回足し、paramB / 1000.0をnB回かけることができる。最大スコアは何点か。

最初の回答。いくつかのパターンを挙げてつぶしていく。。

public class FoxPlayingGame {
	public double theMax(int nA, int nB, int paramA, int paramB) {
		double scoreA = paramA / 1000.0;
		double scoreB = paramB / 1000.0;
		double ret = Math.max(
			0 + (scoreA * nA) * Math.pow(scoreB, nB),
			0 * Math.pow(scoreB, nB) + (scoreA * nA));
		if (scoreB < 0) {
			if (nB % 2 == 0) {
				return Math.max(ret, 
						0 + (scoreA * nA) * Math.pow(scoreB, nB));
			} else {
				return Math.max(ret, 
						0 * scoreB + (scoreA * nA) * Math.pow(scoreB, nB-1));
			}
		}
		return ret;
	}
}

あっさりと他の参加者に撃墜される。こういう方法ではだめですね。

後日の復習で、他の人のコードを見て書き直し。
つまるところ、 (paramA / 1000.0) * nA に、nB回以下で任意の回数 (paramB / 1000.0) をかけて、最大値をとれば良い。

public class FoxPlayingGame {
	public double theMax(int nA, int nB, int paramA, int paramB) {
		double scoreA = paramA / 1000.0;
		double scoreB = paramB / 1000.0;
		double ret = scoreA * nA;
		for (int i = 0; i <= nB; i++) {
			ret = Math.max(ret, (scoreA * nA) * Math.pow(scoreB, i));
		}
		return ret;
	}
}

二つ二次元配列を用意して動的計画法らしく更新していくやり方の人もいるので、そっちも後で確認。

結果

Hard問題には手が出ず。レーティングは912から922に微増。

ここまでできた

地震で動揺していましたが、この週末から開発を再開しました。
手番、アゲハマの記録、コウの判定あたりが実装できました。

まだまだ途中ですが、GAEで動かしています。
つい碁

また、GitHubでソースを公開しています。
michisu/twitigo - GitHub

ぬるぬると、まったりと。

主な残りタスク

  • Twitter連動
    • 対局受付処理
    • 対局中の認証処理
    • 通知処理
  • 終局・勝敗決定処理
  • 全局面を記録
  • セキュリティ周り
  • コメント投稿
  • ハンデ設定
  • 制限時間設定
  • マイページ
  • API

けっこうありますな。

というわけでgoban.py最新版

とりあえずこれで対局を進められる。勝敗判定はできません。。
次はいよいよこれをGAE上で動かす。

# -*- coding: utf-8 -*-
import sys
import codecs

class Untouchable(Exception): pass

class Goban(object):

    empty = 0
    black = 1
    white = 2
    invalid = 3
    around_moves = ((1, 0), (-1, 0), (0, 1), (0, -1))

    def __init__(self, size):
        self.size = size
        self.data = [ [ self.empty for i in range(size) ]
            for j in range(size) ]
    
    def render(self, output=sys.stdout):
        for i, line in enumerate(self.data):
            for j, point in enumerate(line):
                if point == self.empty:
                    output.write(self._empty_str(i, j))
                elif point == self.black:
                    output.write(u'●')
                else:
                    output.write(u'○')
            output.write('\n')
    
    @classmethod
    def get_opponent_color(cls, color):
        return cls.black if color is cls.white else cls.white

    def touch(self, x, y, color):
        if not self.touchable(x, y, color):
            raise Untouchable()
        self.data[y][x] = color
        gottens = set()
        checked = set()
        opponent = self.get_opponent_color(color)
        for move_x, move_y in self.around_moves:
            next_x = x + move_x
            next_y = y + move_y
            if self.get_state(next_x, next_y) is not opponent: continue
            if not self.check_alive(next_x, next_y, opponent, checked):
                self._get_stones(next_x, next_y, gottens)
        for gotten_x, gotten_y in gottens:
            self.data[gotten_y][gotten_x] = self.empty
        return len(gottens)

    def _get_stones(self, x, y, gottens):
        color = self.get_state(x, y)
        gottens.add((x, y))
        targets = []
        target = (x, y)
        while target is not None:
            x, y = target
            for move_x, move_y in self.around_moves:
                next_x = x + move_x
                next_y = y + move_y
                if self.get_state(next_x, next_y) is not color: continue
                if (next_x, next_y) not in gottens:
                    targets.append((next_x, next_y))
                    gottens.add((next_x, next_y))
            target = targets.pop() if targets else None

    def touchable(self, x, y, color):
        if self.get_state(x, y) is not self.empty:
            return False
        elif not self.check_alive(x, y, color):
            return False
        else:
            return True

    def get_state(self, x, y):
        if x < 0 or self.size <= x or y < 0 or self.size <= y:
            return self.invalid
        return self.data[y][x]

    def check_alive(self, x, y, color, checked=None):
        if checked is None: checked = set()
        if (x, y) in checked: return
        checked.add((x, y))
        targets = []
        target = (x, y)
        while target is not None:
            x, y = target
            for move_x, move_y in self.around_moves:
                next_x = x + move_x
                next_y = y + move_y
                state = self.get_state(next_x, next_y)
                if state is self.empty:
                    return True
                elif state is color:
                    if (next_x, next_y) not in checked:
                        targets.append((next_x, next_y))
                        checked.add((next_x, next_y))
            target = targets.pop() if targets else None
        return False

    def _empty_str(self, x, y):
        if x == 0:
            if y == 0:
                return u'┏'
            elif y == self.size - 1:
                return u'┓'
            else:
                return u'┯'
        elif x == self.size - 1:
            if y == 0:
                return u'┗'
            elif y == self.size - 1:
                return u'┛'
            else:
                return u'┷'
        else:
            if y == 0:
                return u'┠'
            elif y == self.size - 1:
                return u'┨'
            else:
                return u'┼'
        
def test():
    sys.stdout = codecs.getwriter('utf-8')(sys.stdout)
    goban_size = 19
    goban = Goban(goban_size)
    goban.touch(3, 3, Goban.black)
    goban.touch(15, 15, Goban.white)
    gotten = goban.touch(3, 4, Goban.black)
    gotten = goban.touch(2, 3, Goban.black)
    gotten = goban.touch(2, 2, Goban.white)
    gotten = goban.touch(3, 2, Goban.white)
    gotten = goban.touch(1, 3, Goban.white)
    gotten = goban.touch(2, 4, Goban.white)
    gotten = goban.touch(4, 3, Goban.white)
    gotten = goban.touch(4, 4, Goban.white)
    goban.render()
    gotten = goban.touch(3, 5, Goban.white)
    goban.render(sys.stdout)
    print 'white got', gotten, 'stones.'

def main():
    sys.stdout = codecs.getwriter('utf-8')(sys.stdout)
    goban_size = 19
    goban = Goban(goban_size)
    turn = Goban.black
    while True:
        goban.render(sys.stdout)
        try:
            input = raw_input()
        except EOFError:
            break
        x, y = [ int(d) for d in input.split(',') ]
        try:
            goban.touch(x, y, turn)
        except Untouchable, e:
            print u'着手できません'
        else:
            turn = Goban.get_opponent_color(turn)

if __name__ == '__main__':
    main()

動機とか仕様

Twitter経由で空き時間にちょっとずつ対局を進められる囲碁サイトをツクリ隊
噂に聞く郵便碁とかのTwitter版、という構想です。題して「つい碁(仮)」。

やりたいこと

  • オープンなプラットフォーム(Twitter)の上で通知を行う
  • ゲーム中の全ての状態(一手ごと)に対して参照用のURLを発行する
  • APIを公開し、クライアントやbotを自由に作成できるようにする

ちなみに私の棋力はヘボヘボです。(15級相当ぐらい)
どちらかと言えばこういった場を構築し、提供したいという技術的関心が先に立っています。

考えている仕様(thanks to [twitter:@kt2d])

  • ゲーム基本部分
    • 大変そうなのであまり深入りしません。。
    • 画面はひとまずアスキーアートみたいな簡素なので。
    • 石を置ける場所と取った石ぐらいは自動判定する。
    • 終局後の地の計算は難しいので、適当にやっておいて、手動で修正してもらう。
    • 盤のサイズ、置き石、手番は、相手募集時に決める。
    • 一手ごとの制限時間は1日がデフォ。(対局が長くなるので九路盤推奨)
  • Twitter連携
    • OAuthログインしてTwitter IDで相手を募集できる。募集のTweetも流す。
    • 対局が始まると一手ごとにコメントを投稿でき、「(あっとまーく相手のID)(コメント) 四の19 (リンク)」みたいなTweetを流す。
    • 観戦者がTwitter経由でコメントをつけられる。
    • 一局にかなり時間がかかる想定なので、一人が複数の対局を並行で進められるようにする。
  • API
    • クライアントアプリやbotを作れるように各種APIを公開。
    • APIで返すフォーマットは、JSONとSGFから選択できる。
  • その他
    • GAEで動かす。
    • マイページにこれまでの対局一覧と現在の対局一覧を表示。
    • 入力待ちの対局一覧は必要だな。

今月は本業でタイトな案件もあるので、のんびりやろうと思ってます。