ゲームを作りました

エンジンの勉強として1週間でゲームを作成する Jam に参加してみました。

1週間じゃ想定していた工程の3割しか終わらないことを体感し、打ちひしがれた

今は完全に燃え尽きています、楽しかった~

あとりにゃあ

こんにちは、ねぼこです。

adventar.org

ととりにゃあ Advent Calendar 2021、8日目です。

毎日進捗

アトリのトトリエ

〜アーランドの錬金術士2〜

あとりにゃあ

アトリ科 Fringillidae

アトリ科(アトリか、学名 Fringillidae)は、鳥類スズメ目の科である。分類によってはアトリ亜科 Fringillinae ともなる。 アトリ(花鶏・臘子鳥・獦子鳥)と総称されるが、狭義にはその1種をアトリと呼ぶ。 南極以外の世界中に生息する。

「アトリ科」『フリー百科事典 ウィキペディア日本語版』より。
2021/06/15 20:51 UTC
URL: https://ja.wikipedia.org/wiki/%E3%82%A2%E3%83%88%E3%83%AA%E7%A7%91

日本だとそこまでメジャーな鳥ではないかも。

系統樹はどこまでをひとまとめにするかで揉めたりしていてちょっと分かりづらいけど、現在では3つの亜科に分類することが多いっぽい。

スミレフウキンチョウ亜科 Euphoniinae

昔はフウキンチョウ科だったけどアトリ科に再分類された。

スミレフウキンチョウ属とミドリフウキンチョウ属の2属からなる。

ちなみにフウキン(風琴)はオルガンのことらしい。

スミレフウキンチョウ Euphonia violacea

属名は eu-(善の) + phōnē(音) 。この由来はユーフォニウム(でっかい金管楽器)と一緒だね

鳴き声がと~っても可愛いんでしょうね 聞いたことはありません

中南米に生息している。

f:id:nebocco:20211208171453j:plain

By Dario Sanches - Flickr: GATURAMO-VERDADEIRO (Euphonia violacea), CC BY-SA 2.0, Link

スミレフウキンチョウ

かわいい~

ヒワ亜科 Carduelinae

40属からなるでっかい系統。

カナリア Serinus canaria

お前アトリ科だったんか

たぶん日本ではアトリ科の中で一番有名。丸くて黄色くて小さい。愛玩動物として世界中で飼われているけど、野生種もヨーロッパ西部の離島に生息している。

鳴きまくるので毒ガスの検知に使われたりしていた。

f:id:nebocco:20211208171736j:plain

4028mdk09 - 投稿者自身による作品, CC 表示-継承 3.0, リンクによる

カナリア

かわいいねぇ

マヒワ Spinus spinus

ユーラシア大陸ほぼ全域に生息。日本にもいる。

冬に大陸から渡ってくるらしい。ちっちゃいのにすごいね

f:id:nebocco:20211208171759j:plain

Tokumi - オリジナル Tokumiが撮影, パブリック・ドメイン, リンクによる

マヒワ

かわい~

ベニヒワ Acanthis flammea

こちらも大陸にからやってくる渡り鳥。顔がツルみたいね

こういう見た目が派手な鳥は大概がオスで、メスはシンプルな茶色っぽい見た目のことが多いイメージがあるけど、ベニヒワはメスも頭が赤くなっている。

f:id:nebocco:20211208171821j:plain

Jyrki Salmi from Finland - Common redpoll, CC 表示-継承 2.0, リンクによる

ベニヒワ

かわいいね

オオマシコ Carpodacus roseus

あkkkkkkkkkkkkkっか

シベリアを中心とする亜寒帯に生息。

モフモフしててりっぱだね

f:id:nebocco:20211208171849j:plain

Tokumi Ohsaka - 投稿者自身による作品, CC0, リンクによる

オオマシコ

かわいい

ウソ Pyrrhula pyrrhula

ウソは嘘じゃなくて口笛のこと。狂言に「嘯(うそふき)」という名前のひょっとこみたいなお面がある。

鳴き声が口笛みたいらしい。もしかして学名もオノマトペなのか!?と思って調べたけど、どうやら pyrrha(炎) が由来らしい。

f:id:nebocco:20211208171924j:plain

Alpsdake - 投稿者自身による作品, CC 表示-継承 3.0, リンクによる

かわい

アトリ亜科 Fringillinae

アトリ属のみからなる。

アトリ Fringilla montifringilla

あとりにゃあ

冬になるとシベリアから東北地方にやってくる。

漢字で書くと「花鶏」。読めるかい

特に書くことはないです

f:id:nebocco:20211208171943j:plain

Marek Szczepanek, CC 表示-継承 3.0, リンクによる

アトリ

かわいいね~

おわり

整数のまま行う偏角ソート

浮動小数点数に直して $\arg$ 求めるの嫌いなので整数のままソートしましょう。 偏角の取りうる範囲は $[0, 2 \pi )$ とします。

追記

投稿直後にもっと賢い方法が投稿され、膝から崩れ落ちました

ngtkana.hatenablog.com

ソートする時には、二点 $p = (p _ x, p _ y), q = (q _ x, q _ y)$ が与えられたときにどちらの偏角が大きいのか判定できればOKです。 なので二点の比較を行う方法だけ考えます。

ざっくりと比較

まずは座標平面を三つに切り分けて大まかな位置を特定します。

  1. 偏角 $[0, \pi / 2]$
  2. 偏角 $(\pi / 2, \pi]$
  3. 偏角 $(\pi, 2 \pi)$

これ以外の切り分け方でも大丈夫ですが、座標の正負だけ見ればいいのでこれが一番楽だと思います。

この段階で二点の属する領域が異なれば、それだけで大小関係を決定できます。

Python
def area(p: tuple[int, int]):
    x, y = p
    if y < 0:
        return 3
    elif x < 0:
        return 2
    else:
        return 1

def arg_cmp(p: tuple[int, int], q: tuple[int, int]):
    ap = area(p)
    aq = area(q)
    if ap < aq:
        return -1
    elif ap > aq:
        return 1
    else:
        return 0 # もっと詳しく比較する必要がある
Rust
use std::cmp::Ordering;

fn area(p: (i64, i64)) -> u8 {
    let (x, y) = p;
    if y < 0 { 3 } else if x < 0 { 2 } else { 1 }
}

fn arg_cmp(p: &(i64, i64), q: &(i64, i64)) -> Ordering {
    let ap = area(*p);
    let aq = area(*q);
    ap.cmp(&aq) // もっと詳しく比較する必要がある
}

細かな比較

座標平面上での三角形の面積の求め方 の考え方を利用すれば、面積の符号によって二点の位置関係を特定できます。 二点の偏角の差が $\pi$ 以上だと符号が反転したり変な感じになってしまうので、そうならないようにあらかじめ三つの領域に分割しました。(かしこい)

Python
def arg_cmp(p: tuple[int, int], q: tuple[int, int]):
    ap = area(p)
    aq = area(q)
    if ap < aq:
        return -1
    elif ap > aq:
        return 1
    else:
        px, py = p
        qx, qy = q
        z = px * qy - py * qx
        if z > 0:
            return -1
        elif z < 0:
            return 1
        else:
            return 0
Rust
use std::cmp::Ordering;

fn area(p: (i64, i64)) -> u8 {
    let (x, y) = p;
    if y < 0 { 3 } else if x < 0 { 2 } else { 1 }
}

fn arg_cmp(p: &(i64, i64), q: &(i64, i64)) -> Ordering {
    let ap = area(*p);
    let aq = area(*q);
    ap.cmp(&aq).then((q.0 * p.1).cmp(&(p.0 * q.1)))
}

あとはこれを sort に渡せば OK です。Python では比較関数を直接渡すことはできないので、 functoolscmp_to_key を使いましょう。

Python
from functools import cmp_to_key

points = [(0, 1), (2, -3), (-4, -5), (-6, 7)]
points.sort(key=cmp_to_key(arg_cmp))
Rust
fn main() {
    let mut points: Vec<(i64,i64)> = vec![(0, 1), (2, -3), (-4, -5), (-6, 7)];
    points.sort_by(arg_cmp);
}
補足

area() を書き換えるだけで偏角の範囲を $[- \pi / 2, \pi / 2)$ に変更したり、点 $(0, 0)$ を必ず先頭に持ってきたりできるのが嬉しいです。

使用例

ABC225E - フ

問題ページ
提出コード (Pypy3)
提出コード (Rust)

Python3 だと全然間に合いませんでした。cmp_to_key の呼び出しが重いっぽい?

Sort Points by Argument (Library Checker)

問題ページ
提出コード (Python)

$\arctan$ って書くとはてなキーワードリンクのせいで $\KaTeX$ 効かないのやめてくれ~~~~

ABC224-G Roll or Increment 雑解法

みんなはちゃんと証明しましょう

問題

リンクはこちら

解法

現在の出目が $i$ の時、出目を $T$ に変える最適な戦略でかかる合計コストを $C(i)$ と書くことにします。 戦略はゴールまで増やし続けるか一旦振り直すかです。

振り直した後にかかるコストの期待値を $r$ と置きます。すると、

$$ C(i) = \begin{cases} \min(A(T - i), B + r) & (i \le T) \\ B + r & (i > T) \end{cases} $$

です。さらに、$r = \sum C(i) / N$ です。

ここで、 $r$ をある値 $r_{a}$ だと仮定します。すると $C(i)$ が定まり、上の式を用いて $r$ が計算できます。これを $r_b$ とします。 $r_a$ が $r_b$ より大きいとき、真の $r$ は $r_a$ より小さく、 小さいときは $r_a$ より大きいはずです。 このように $r$ を二分探索することで真の $r$ および $C(i)$ が計算できます。求める答えは $C(S)$ です。

from math import ceil

N, S, T, A, B = map(float, input().split())

hi = N * max(A, B)
lo = 0
for _ in range(64):
    ra = (hi + lo) / 2.

    # A * (T - i) <= B + r である i の個数
    count = min(T, ceil((ra + B) / A))
    border = T - count

    # C(i) の総和
    total = (ra + B) * (border + (N - T)) + A * (count - 1) * count / 2
    rb = total / N

    if ra > rb:
        hi = ra
    else:
        lo = rb

r = (hi + lo) / 2
if S > T:
    ans = r + B
else:
    ans = min(A * (T - S), r + B)
print(f"{ans:.16f}")

提出コード

提出コード(Python)

雑すぎる

ABC215-E Chain Contestant 解説

文字列に含まれる文字の種類数を $σ$ とおきます。 $O(N2^{σ})$ で解きます。

問題

リンクはこちら

解法

$σ = 1$

まずは $σ = 1$ の場合を考えてみましょう。このとき答えは明らかに $2^{N} - 1$ です。これをDPで解いてみることにします。配列 $g[i] \coloneqq \text{$s[0, i)$ の範囲についての選び方}$ を考えます。例えば $s = \text{\textquotedblleft AAAAAA\textquotedblright}$ の時、 $g = [0, 1, 3, 7, 15, 31, 63]$ です。

ここで、$g[i]$ の値を求める計算を、以下のように場合分けして考えてみましょう。

最初に選ぶのが $s[0]$ のとき

$s[1], s[2], \ldots, s[i-1]$ の $i-1$ 個については、選んでも選ばなくてもよい。
$\Rightarrow 2^{i-1}$ 通り

最初に選ぶのが $s[1]$ のとき

$s[2], s[3], \ldots, s[i-1]$ の $i-2$ 個については、選んでも選ばなくてもよい。
$\Rightarrow 2^{i-2}$ 通り

$\vdots$
最初に選ぶのが $s[i-1]$ のとき

$1$ 通り

これらを足し合わせることで、確かに $\displaystyle\sum _ {j=0}^{i-1}2^{j} = 2^{i} - 1$ となり、 $g[i]$ の値と一致します。

$σ = 2$

続いて、 $σ = 2$ の場合を考えます。問題の文字列 $s$ が $\text{\textquotedblleft ABAABAB\textquotedblright}$ であるとします。 同じ種類のコンテストは必ず一かたまりで選ばなければなりませんが、この順番が $\text{A} \rightarrow \text{B}$ の場合を考えます。

まず、先ほど考えた $σ = 1$ のパターン同様、 $\text{A}$ だけを選ぶ場合を表す配列 $g _ {A}$ が存在すると考えます。これは以下のようになります。

f:id:nebocco:20210825165356p:plain
A のみを選ぶ場合 B は無視される

では、改めて $g _ {\text{AB}}[i] \coloneqq \text{$s[0, i)$ から条件を満たすように A, B の順で選ぶ方法の個数}$ を埋めていきます。 $g _ {\text{AB}}[7]$ を計算するときの様子を考えてみます。

最初に選ぶ $\text{B}$ が $s[1]$ のとき

$s[0, 1)$ からいくつか $\text{A}$ を選び、$s[1]$ を選ぶ。$s[4], s[6]$ は選んでも選ばなくてもよい。
$\Rightarrow g _ {\text{A}}[2] \times 2^{2}$ 通り

最初に選ぶ $\text{B}$ が $s[4]$ のとき

$s[0, 4)$ からいくつか $\text{A}$ を選び、$s[4]$ を選ぶ。$s[6]$ は選んでも選ばなくてもよい。
$\Rightarrow g _ {\text{A}}[4] \times 2^{1}$ 通り

最初に選ぶ $\text{B}$ が $s[6]$ のとき

$s[0, 6)$ からいくつか $\text{A}$ を選び、$s[6]$ を選ぶ。
$\Rightarrow g _ {\text{A}}[6] \times 2^{0}$ 通り

これらの合計が $g _ {\text{AB}}[7]$ になります。足し合わせるべき $g _ {\text{A}}[i]$ を以下のように累積的に計算すれば、前から見ていくことで $g _ {\text{AB}}$ を計算することができます。

acc = 0
for i in range(N):
    if s[i] == 'B':
        acc = acc * 2 + gA[i]
        gAB[i+1] = acc
    else:
        gAB[i+1] = gAB[i]

同様に $g _ {\text{B}}$ から $g _ {\text{BA}}$ も計算できます。

ここで、 $g _ {\text{\{A,B\}}}[i] \coloneqq \text{$s[0, i)$ から条件を満たすように A, B を選ぶ方法の個数}$ という配列を考えると、 $g _ {\text{\{A,B\}}}[i] = g _ {\text{AB}}[i] + g _ {\text{BA}}[i]$ が成り立ちます。また、文字列中に出現する文字全体の集合を $U$ とおくと、本問題で求めるべき答えは

$$ \sum _ {\substack{S \subset U \\ S \neq \varnothing}} g _ {S}[n] $$

と表せます。

$σ \ge 3$ のとき

これまでとおおむね同様にして $g _ {*}$ を求めていくことができます。 例えば $g _ {\text{ABCD}}$ であれば以下のようなコードになります。

acc = 0
for i in range(N):
    if s[i] == 'D':
        acc = acc * 2 + gABC[i] 
        gABCD[i+1] = acc
    else:
        gABCD[i+1] = gABCD[i]

特に $σ$ が大きくなると、ありうるすべての順列について $g _ {*}$ を足し合わせるのが困難となるので、直接 $g _ {S}$ を求める必要があります。この方法について、 $σ = 2$ の場合に戻って考えます。

$g _ {S}$ を直接求める

$s = \text{\textquotedblleft ABAABAB\textquotedblright}$ とします。ここで、以下の二種類の配列を考えます。

$$ \begin{aligned} f _ {\text{AB}}[i] &\coloneqq \text{$s[i-1]$ を必ず選ぶとき、 $s[0, i)$ から条件を満たすように A, B の順で選ぶ方法の個数} \\ f _ {\text{\{A,B\}}}[i] &\coloneqq \text{$s[i-1]$ を必ず選ぶとき、 $s[0, i)$ から条件を満たすように A, B を選ぶ方法の個数} \end{aligned} $$

この時、 $f _ {\text{\{A,B\}}}[i] = f _ {\text{AB}}[i] + f _ {\text{BA}}[i]$ です。また、条件を満たす選び方それぞれについて、最後の文字の位置がどこであるかを考えると、$g$ が $f$ の累積和となっていることがわかります。

f:id:nebocco:20210825170916p:plain
g は f の累積和

さらに、 $f _ {\text{AB}}$ と $f _ {\text{BA}}$ を見比べると、一方が $0$ でないときもう一方は必ず $0$ となっています。これは、 $s[i-1]$ を必ず選ぶという条件により、$s[i-1] = \text{A}$ のときは $f _ {\text{AB}}[i] = 0$ となるためです。

f:id:nebocco:20210825171627p:plain
片方だけ

$f _ {\text{AB}}$ は以下のように計算可能ですが、

fAB = [0] * (N+1)
acc = 0
for i in range(N):
    if s[i] == 'B':
        # s[i-1] を必ず使うという条件により、2の指数が1減る
        # それに伴い更新式が若干変化した
        fAB[i+1] = acc + g_A_[i]
        acc = acc * 2 + g_A_[i]

$f _ {\text{AB}}$ と $f _ {\text{BA}}$ の更新のタイミングはかぶらないため、if, else によって以下のようにまとめることができます。

fAB = [0] * (N+1)
fBA = [0] * (N+1)
accA = 0
accB = 0
for i in range(N):
    if s[i] == 'A':
        fBA[i+1] = accA + g_B_[i]
        accA = accA * 2 + g_B_[i]
    else:
        fAB[i+1] = accB + g_A_[i]
        accB = accB * 2 + g_A_[i]

さらに配列もひとつにまとめてしまうことで、直接 $f _ {\text{\{A,B\}}}$ を計算することができます。

f_AB_= [0] * (N+1)
accA = 0
accB = 0
for i in range(N):
    if s[i] == 'A':
        f_AB_[i+1] = accA + g_B_[i]
        accA = accA * 2 + g_B_[i]
    else:
        f_AB_[i+1] = accB + g_A_[i]
        accB = accB * 2 + g_A_[i]

以上の内容と、 $f$ の累積和を取ると $g$ になることから、$g _ {\text{\{A,B\}}}$ は結局以下のように直接計算することができます。

g_AB_= [0] * (N+1)
accA = 0
accB = 0
for i in range(N):
    if s[i] == 'A':
        g_AB_[i+1] = accA + g_B_[i]
        accA = accA * 2 + g_B_[i]
    else:
        g_AB_[i+1] = accB + g_A_[i]
        accB = accB * 2 + g_A_[i]
    g_AB_[i+1] += g_AB_[i]

一般の $σ$ について

より多くの文字が含まれる場合についても、最後に選ぶ文字がどれであるかによって分類すると、同様の計算が可能です。 $\mathrm{dp}[S][i] \coloneqq \text{$s[0, i)$ から条件を満たすように、 $S$ 中の文字を選ぶ方法の個数}$ を埋めていきます。 更新式は以下のように表されます。

# 更新のイメージ
for i in range(N):
    c = s[i]
    if c in S:
        dp[S][i+1] = acc[c] + dp[S\{c}][i]
        acc[c] = acc[c] * 2 + dp[S\{c}][i]
    dp[S][i+1] += dp[S][i]

ここで、 $\mathrm{dp}[S]$ を計算する際には $\mathrm{dp}[S \setminus \{c\}]$ が既に計算されている必要があることに注意してください。

実際に実装する際には $S$ はビット表現として持つことになるので、以下のようなコードになります。

# 各文字は適当な整数に変換
for i in range(N):
    c = s[i]
    if bit>>c & 1:
        dp[bit][i+1] = acc[c] + dp[bit ^ 1<<c][i]
        acc[c] = acc[c] * 2 + dp[bit ^ 1<<c][i]
    dp[bit][i+1] += dp[bit][i]

bit について昇順に更新していけば、$\mathrm{dp}[S]$ の更新に必要な $\mathrm{dp}[T] ~ (T \subset S)$ は全て計算済みとなります。

余談

逆に言えば自分の上位集合はまだ計算されていないことになるので、if を省略することができます。

for i in range(N):
    # bit & 1<<s[i] == 0 なら dp[bit | 1<<s[i]][i] == 0
    # acc[s[i]] もずっと 0 のまま
    dp[bit][i+1] = acc[s[i]] + dp[bit ^ 1<<s[i]][i] + dp[bit][i]
    acc[s[i]] = acc[s[i]] * 2 + dp[bit ^ 1<<s[i]][i]

実装

全体の実装は以下の通りです。計算量は $O(N2^σ)$ です。

K = 10
MOD = 998_244_353

N = int(input())
s = [ord(x) - ord('A') for x in input()]
dp = [[0] * (N+1) for _ in range(1<<K)]
dp[0][0] = 1

for bit in range(1<<K):
    acc = [0] * K
    for i in range(N):
        dp[bit][i+1] = (acc[s[i]] + dp[bit ^ 1<<s[i]][i] + dp[bit][i]) % MOD
        acc[s[i]] = (acc[s[i]] * 2 + dp[bit ^ 1<<s[i]][i]) % MOD

ans = sum(dp[bit][N] for bit in range(1, 1<<K)) % MOD
print(ans)

提出コード(Python)

ツイステのダメージ計算Webアプリを作りました

初めてのWebアプリを作りました。

twst-simulator.herokuapp.com

プログラミングをやっているからにはいつかWebの技術も触りたいなあ、と思いながらも特に作るアイデアもなくのうのうと過ごしていましたが、ふとこれを思いついたので作ってみました。

これのためにHTML、CSSJavaScript (Vue.js) を一から勉強しました。本当に大変だった

URLがいつまでたってもGoogleに登録されないので検索に引っかからず、だれにも使ってもらえません ウケるね いつか登録されるといいですね

作る過程で Vue.js に慣れていったので、次はもう少しうまく書ける気がします。 次は何つくろっかな~ 何かの投稿サイトとか作りたいね

Rime on Windows10

競技プログラミング作問支援ツール Rime を Windows10 で使えるように改造しましょう!

Mac なんかに……負けないっ……!

おことわり
  • 作業にあたって Rime 非公式ドキュメント を大いに参考にさせていただきました。基本的にはここのチュートリアルに従い、windowsで正しく動かなかったら適宜書き換えていく、という方針で作業を行います。
  • 自己責任でお願いします。そんなにパソコン全体に影響のあるような作業はしないので大丈夫だとは思いますが……
  • 動かない原因をコードを読みながら延々と試行錯誤した結果なので、書き換えなくていい場所や、もっと効率のいい方法があるかもしれません。
  • 「同じように作業をしたのにここが違う!」みたいなことを言われても何もわからないので自分でコードと公式リファレンス読んでください。
  • Powershell を使用します。

インストール

PS C:\home> pip install git+https://github.com/icpc-jag/rime
Collecting git+https://github.com/icpc-jag/rime
...(略)...

インストールが完了したことを確かめましょう。

PS C:\home> rime --help

f:id:nebocco:20210501171635p:plain
???

f:id:nebocco:20210501171653p:plain
?????

コマンドが走らず、スクリプトファイル自体を開こうとしてしまいます。どうやらMacではファイル名を入力するだけでPythonを起動できるようなのですが、Windowsではご存じのように

python file.py

のように入力しなければなりません。ではそのようにしてみましょう。

PS C:\home> python rime --help
C:\Users\Personal\AppData\Local\Programs\Python\Python39\python.exe: can't open file 'C:\home\rime': [Errno 2] No such file or directory

どうやら二番目以降の引数はパスが通っていても認識してくれないようです。まったくもう

これをフルパスで入力するとちゃんと動きます。

PS C:\home> python 'C:\Users\Personal\AppData\Local\Programs\Python\Python39\Scripts\rime' --help
rime.py <command> [<options>...] [<args>...]

    Rime is a tool for programming contest organizers to automate usual, boring
    and error-prone process of problem set preparation. It supports various
    programming contest styles like ACM-ICPC, TopCoder, etc. by plugins.

    To see a brief description and available options of a command, try:

    rime.py help <command>

Commands:
...(略)...

ただ毎回こんなことはやっていられないので、起動しやすい場所に持ってくることにします。

Rimeで作問をする専用のディレクトリを作成し、その中にさっきの場所にあるrimeとrime_initをコピーしてきましょう。エクスプローラを使ってマウスでやっても大丈夫です。(そのほうが速いかも)

PS C:\home> mkdir Rime


    ディレクトリ: C:\home


Mode                 LastWriteTime         Length Name
----                 -------------         ------ ----
d-----        2021/05/01     17:25                Rime


PS C:\home> cd Rime
PS C:\home\Rime> cp 'C:\Users\Personal\AppData\Local\Programs\Python\Python39\Scripts\rime' .
PS C:\home\Rime> cp 'C:\Users\Personal\AppData\Local\Programs\Python\Python39\Scripts\rime_init' .
PS C:\home\Rime> ls


    ディレクトリ: C:\home\Rime


Mode                 LastWriteTime         Length Name
----                 -------------         ------ ----
-a----        2021/05/01     17:11            172 rime
-a----        2021/05/01     17:11           2421 rime_init

こうするとほぼ本家と同じように書くことができます。

PS C:\home\Rime> py rime --help
rime.py <command> [<options>...] [<args>...]

    Rime is a tool for programming contest organizers to automate usual, boring
    and error-prone process of problem set preparation. It supports various
    programming contest styles like ACM-ICPC, TopCoder, etc. by plugins.

    To see a brief description and available options of a command, try:

    rime.py help <command>

Commands:
...(略)...

以上のように、コマンドすべての頭に py を付ければOKです。 py を登録していない人は python に置き換えてください。

これでインストールは完了です。

チュートリアル

ディレクトリの初期化

PS C:\home\Rime> py rime_init --git
Initialized empty Git repository in C:/home/Rime/.git/
[master (root-commit) fe093c2] Initial commit
 3 files changed, 5049 insertions(+)
 create mode 100644 .gitignore
 create mode 100644 PROJECT
 create mode 100644 common/testlib.h

新規問題の作成

subprocess
PS C:\home\Rime> py rime add . problem aplusb
Note: Running Rime under Windows will be unstable.
Traceback (most recent call last):
  File "c:\users\personal\appdata\local\programs\python\python39\lib\site-packages\rime\core\main.py", line 144, in Main

...(中略)...

FileNotFoundError: [WinError 2] 指定されたファイルが見つかりません。

出力ログを見ると、途中で call という関数を呼んでいることが分かります。これは subprocess というモジュールの関数なのですが、このモジュールでシェルコマンドを呼び出すためには shell=True という引数を与える必要があります。Rime では subprocess から call() のほかに check_output()Popen() を使っています。これらについても shell=True を与える必要があります。

一度、Rime の中身のコードが置かれているディレクトリに移動します。

PS C:\home\Rime> cd 'C:\users\personal\appdata\local\programs\python\python39\lib\site-packages\rime'
PS C:\..\..\rime> 

以降、Rime 本体のコードに言及する時は基本的にこの中にあるものを指していると思ってください。スクリプト内で call() を使っている箇所をすべて見つけるため、以下のコマンドで検索します。

PS C:\..\..\rime> findstr /s /n 'call\(' *.py
plugins\plus\commands.py:63:    call([EDITOR, filename])

これで 'call(' が書かれている箇所をすべて探すことができました。call() は一度しか使われていないようです。 plugins\plus\commands.py を見に行って、以下のように書き換えます。

def EditFile(filename, initial):
    EDITOR = os.environ.get('EDITOR', 'vi')
    files.WriteFile(initial, filename)
    call([EDITOR, filename], shell=True)

check_output(), Popen() についても同じように探して書き換えます。

comple(None, ...)

あたらめて実行しましょう。最初の実行時に作成できてしまったディレクトリは削除しておきます。

PS C:\home\Rime> py rime add . problem aplusb
Note: Running Rime under Windows will be unstable.
ERROR: aplusb: compile() arg 1 must be a string, bytes or AST object

これは core/targets.py 内の TargetBase.Load() 内部で、compile()None を渡してしまっていることが原因です。そのため、以下のように条件分岐を付け加えておきます。

# Evaluate config.
try:
    script = files.ReadFile(self.config_file)
except IOError:
    raise ConfigurationError('cannot read file: %s' % self.config_file)

if script is None:
    return

try:
    code = compile(script, self.config_file, 'exec')
    self.PreLoad(ui)
    exec(code, self.exports, self.configs)
    self.PostLoad(ui)
except ReloadConfiguration:
    raise  # Passthru
except Exception as e:
    # TODO(nya): print pretty file/lineno for debug
    raise ConfigurationError(e)
vim
PS C:\home\Rime> py rime add . problem aplusb
Note: Running Rime under Windows will be unstable.
'vi' は、内部コマンドまたは外部コマンド、
操作可能なプログラムまたはバッチ ファイルとして認識されていません。
[   ADD    ] C:\home\Rime\aplusb/PROBLEM

Error Summary:
Total 0 errors, 0 warnings

問題ディレクトリの作成に成功すると自動で vim が起動するのですが、私は vim を入れていないので起動に失敗します。代わりに Visual Studio Code を指定します。

plugins/plus/commands.pyEditFile() を以下のように書き換えます。

def EditFile(filename, initial):
    EDITOR = os.environ.get('EDITOR', 'code')
    files.WriteFile(initial, filename)
    call([EDITOR, filename], shell=True)

お好きなエディタを指定してください。

ここまで済ませると無事に Rime/aplusb が作成され、Rime/aplusb/PROBLEM が自動で開きます!お疲れさまでした。まだまだ続きます。

PS C:\home\Rime> ls


    ディレクトリ: C:\home\Rime


Mode                 LastWriteTime         Length Name
----                 -------------         ------ ----
d-----        2021/05/01     18:21                aplusb
d-----        2021/05/01     17:36                common
-a----        2021/05/01     17:36             72 .gitignore
-a----        2021/05/01     17:36            671 PROJECT
-a----        2021/05/01     17:11            172 rime
-a----        2021/05/01     17:11           2421 rime_init


PS C:\home\Rime> ls aplusb


    ディレクトリ: C:\home\Rime\aplusb


Mode                 LastWriteTime         Length Name
----                 -------------         ------ ----
-a----        2021/05/01     18:21            408 PROBLEM

解答プログラムの作成

先ほど作成した aplusb/ ディレクトリ内で、以下のコマンドを実行してみましょう。

と書かれていますが、Rime/ でもパスを指定する引数を書き換えれば同じことができるのでそうします。私は Rust が好きなので Rust で書きます。

PS C:\home\Rime> py rime add aplusb solution rust_correct
Note: Running Rime under Windows will be unstable.
[   ADD    ] C:\home\Rime\aplusb\rust_correct/SOLUTION

Error Summary:
Total 0 errors, 0 warnings

Rime/aplusb/rust_correct/SOLUTION が自動で開きます。Rust 用の行をコメントアウトして、ans.rs を書きましょう。

fn main() {
    let mut io = IO::new();
    input!{ from io,
        a: i32,
        b: i32
    }
    println!("{}", a + b);
}

SOLUTION を見れば分かるように、コンパイルは rustc を通して行われるので、cargo を使ってほかのクレートを持ってくることができません。入出力関数なども自分で用意しましょう。

テスト用プログラムの作成

全部自分で書かなければならないので、generator と validator を Rust で作るのはかなり絶望的です。あきらめて C++ で書くことにします。

ここまで来たら

PS C:\home\Rime> py rime add aplusb testset tests
Note: Running Rime under Windows will be unstable.
[   ADD    ] aplusb: C:\home\Rime\aplusb\tests/TESTSET

Error Summary:
Total 0 errors, 0 warnings

も問題なく実行できるはずなので、その後は本家に則って作業してください。私はコピペしました。

テストの実行

SIGXCPU
PS C:\home\Rime> py rime test aplusb
Note: Running Rime under Windows will be unstable.
[ COMPILE  ] aplusb/tests: generator.cc
ERROR: aplusb/tests: generator.cc: Compile Error (On compiling: module 'signal' has no attribute 'SIGXCPU')
[ COMPILE  ] aplusb/tests: validator.cc
ERROR: aplusb/tests: validator.cc: Compile Error (On compiling: module 'signal' has no attribute 'SIGXCPU')

Build Summary:
aplusb ... in: 0B, diff: 0B, md5: d41d8cd98f00b204e9800998ecf8427e
  rust_correct RUST 236 lines, 5.1kB

Test Summary:
aplusb ... 1 solutions, 0 tests
  rust_correct FAIL Failed to build tests

Error Summary:
ERROR: aplusb/tests: generator.cc: Compile Error (On compiling: module 'signal' has no attribute 'SIGXCPU')
ERROR: aplusb/tests: validator.cc: Compile Error (On compiling: module 'signal' has no attribute 'SIGXCPU')
Total 2 errors, 0 warnings

pythonsignal というモジュールを使っているらしいのですが、SIGXCPU という変数は存在しません。どうやら Python3.5 で SIG_DFL という変数に統合されてしまったようです。先ほどと同様、 findstr を使って検索してすべて書き換えましょう。

ここでは実行に時間が掛かりすぎている場合に処理を切り替えるような作業を行っていますが、コンパイルを行う際は必ず引っかかっているようです。これが正しい挙動なのかはわかりません。動けばよかろうなのです。だれか助けて~~~

seek
PS C:\home\Rime> py rime test aplusb
Note: Running Rime under Windows will be unstable.
[ COMPILE  ] aplusb/tests: generator.cc
ERROR: aplusb/tests: generator.cc: Compile Error (On compiling: 'int' object has no attribute 'seek')
[ COMPILE  ] aplusb/tests: validator.cc
ERROR: aplusb/tests: validator.cc: Compile Error (On compiling: 'int' object has no attribute 'seek')

Build Summary:
aplusb ... in: 0B, diff: 0B, md5: d41d8cd98f00b204e9800998ecf8427e
  rust_correct RUST 236 lines, 5.1kB

Test Summary:
aplusb ... 1 solutions, 0 tests
  rust_correct FAIL Failed to build tests

Error Summary:
ERROR: aplusb/tests: generator.cc: Compile Error (On compiling: 'int' object has no attribute 'seek')
ERROR: aplusb/tests: validator.cc: Compile Error (On compiling: 'int' object has no attribute 'seek')
Total 2 errors, 0 warnings

rime/basic/codes.pyCodeBase._ResetIO() 内で args (stdin, stdout, stderr) に対して seek() を呼びますが、stderr として渡される subprocess.STDOUT はオブジェクトではなく、特殊な名前を持つただの整数です。そこで、以下のように書き換えます。

def _ResetIO(self, *args):
    for f in args:
        if f is None or f is subprocess.STDOUT:
            continue
        try:
            f.seek(0)
            f.truncate()
        except IOError:
            pass
UTF-8
PS C:\home\Rime> py rime test aplusb
Note: Running Rime under Windows will be unstable.
[ COMPILE  ] aplusb/tests: generator.cc
[ COMPILE  ] aplusb/tests: validator.cc
[ GENERATE ] aplusb/tests: generator.cc
[ VALIDATE ] aplusb/tests: 02_random_01.in: PASSED
[ VALIDATE ] aplusb/tests: 02_random_02.in: PASSED
[ VALIDATE ] aplusb/tests: 02_random_03.in: PASSED
[ VALIDATE ] aplusb/tests: 02_random_04.in: PASSED
[ VALIDATE ] aplusb/tests: 02_random_05.in: PASSED
[ VALIDATE ] aplusb/tests: 02_random_06.in: PASSED
[ VALIDATE ] aplusb/tests: 02_random_07.in: PASSED
[ VALIDATE ] aplusb/tests: 02_random_08.in: PASSED
[ VALIDATE ] aplusb/tests: 02_random_09.in: PASSED
[ VALIDATE ] aplusb/tests: 02_random_10.in: PASSED
[ VALIDATE ] aplusb/tests: OK
[ COMPILE  ] aplusb/rust_correct
[  REFRUN  ] aplusb/rust_correct: 02_random_01.in: DONE
[  REFRUN  ] aplusb/rust_correct: 02_random_02.in: DONE
[  REFRUN  ] aplusb/rust_correct: 02_random_03.in: DONE
[  REFRUN  ] aplusb/rust_correct: 02_random_04.in: DONE
[  REFRUN  ] aplusb/rust_correct: 02_random_05.in: DONE
[  REFRUN  ] aplusb/rust_correct: 02_random_06.in: DONE
[  REFRUN  ] aplusb/rust_correct: 02_random_07.in: DONE
[  REFRUN  ] aplusb/rust_correct: 02_random_08.in: DONE
[  REFRUN  ] aplusb/rust_correct: 02_random_09.in: DONE
[  REFRUN  ] aplusb/rust_correct: 02_random_10.in: DONE
[  REFRUN  ] aplusb/rust_correct
[   TEST   ] aplusb/rust_correct
ERROR: aplusb/rust_correct: 02_random_01.in: Wrong Answer
  judge log: C:\home\Rime\aplusb\rime-out\rust_correct\02_random_01.judge
[   TEST   ] aplusb/rust_correct: 02_random_01.in: Wrong Answer

Build Summary:
aplusb ... in: 40B, diff: 25B, md5: -
  rust_correct RUST 236 lines, 5.1kB

Test Summary:
aplusb ... 1 solutions, 10 tests
  rust_correct FAIL 02_random_01.in: Wrong Answer

Error Summary:
ERROR: aplusb/rust_correct: 02_random_01.in: Wrong Answer
  judge log: C:\home\Rime\aplusb\rime-out\rust_correct\02_random_01.judge
Total 1 errors, 0 warnings

コンパイルはできましたが、どうやらジャッジが合わないようです。output を作成するコードと解答コードが同一なのに……どういうことでしょうか?ログに書かれている judge log を確認してみます。

'diff' �́A�����R�}���h�܂��͊O���R�}���h�A
����\�ȃv���O�����܂��̓o�b�` �t�@�C���Ƃ��ĔF������Ă��܂���B

ポンコツ太郎がよ……

Pythonopen() はちゃんと文字コードを指定してあげないとこういうことが起こりがちなので、ちゃんと UTF-8 を指定してあげましょう。結構たくさんあるので頑張ってください。

def _ExecForCompile(self, args):
    with open(os.path.join(self.out_dir, self.log_name), 'w', encoding='utf-8') as outfile:
        yield (yield self._ExecInternal(
            args=args, cwd=self.src_dir,
            stdin=files.OpenNull(), stdout=outfile,
            stderr=subprocess.STDOUT))

こんな感じです。

diff

これでもう一度実行して judge log を見ます。

'diff' �́A�����R�}���h�܂��͊O���R�}���h�A
����\�ȃv���O�����܂��̓o�b�` �t�@�C���Ƃ��ĔF������Ă��܂���B

どうして……

私の力では judge log を出力しているのがどこなのか分からず、文字化けを治すことができませんでした。仕方がないのでここを直すのはあきらめて別エンコードで開くことにします。VSCode であれば下部の「UTF-8」と書かれた箇所をクリックすることで自分でエンコードを指定して開くことができます。

f:id:nebocco:20210501193656p:plain
ここ

'diff' は、内部コマンドまたは外部コマンド、
操作可能なプログラムまたはバッチ ファイルとして認識されていません。

subprocess でコマンドを実行する際は Powershell ではなく cmd.exe を呼び出すのですが、残念ながら cmd.exe には diff というコマンドはありません。代わりに fc というコマンドがあるので、これを使えばよいです。rime/basic/codes.pyInternalDiffCode.Run() を書き換えます。

def Run(self, args, cwd, input, output, timeout, precise,
        redirect_error=False):
    parser = optparse.OptionParser()
    parser.add_option('-i', '--infile', dest='infile')
    parser.add_option('-d', '--difffile', dest='difffile')
    parser.add_option('-o', '--outfile', dest='outfile')
    (options, pos_args) = parser.parse_args([''] + list(args))
    run_args = ('fc', options.difffile, options.outfile) # ここ
    with open(input, 'r', encoding='utf-8') as infile:

UTF-8 として比較するようなオプションがあり、それがないと非ASCII文字を出力する問題でバグりそうな予感がしますが、そのオプションを付けると改行が \n\n\r かが区別され、正しい出力でも WA と判定されてしまうので付けないことにしました。

これで実行できるはずです。

PS C:\home\Rime> py rime test aplusb
Note: Running Rime under Windows will be unstable.
[ COMPILE  ] aplusb/rust_correct: up-to-date
[   TEST   ] aplusb/rust_correct
[   TEST   ] aplusb/rust_correct: 02_random_01.in: PASSED
[   TEST   ] aplusb/rust_correct: 02_random_02.in: PASSED
[   TEST   ] aplusb/rust_correct: 02_random_03.in: PASSED
[   TEST   ] aplusb/rust_correct: 02_random_04.in: PASSED
[   TEST   ] aplusb/rust_correct: 02_random_05.in: PASSED
[   TEST   ] aplusb/rust_correct: 02_random_06.in: PASSED
[   TEST   ] aplusb/rust_correct: 02_random_07.in: PASSED
[   TEST   ] aplusb/rust_correct: 02_random_08.in: PASSED
[   TEST   ] aplusb/rust_correct: 02_random_09.in: PASSED
[   TEST   ] aplusb/rust_correct: 02_random_10.in: PASSED
[   TEST   ] aplusb/rust_correct: max 0.03s, acc 0.29s

Build Summary:
aplusb ... in: 40B, diff: 25B, md5: -
  rust_correct RUST 236 lines, 5.1kB

Test Summary:
aplusb ... 1 solutions, 10 tests
  rust_correct  OK  max 0.03s, acc 0.29s

Error Summary:
Total 0 errors, 0 warnings

誤答が落ちることをテストする

せっかくなのでここも試してみます。今度はpythonで書いてみることにします。

PS C:\home\Rime> py rime add aplusb solution py_wa
Note: Running Rime under Windows will be unstable.
[   ADD    ] C:\home\Rime\aplusb\py_wa/SOLUTION

SOLUTION にも書かれているとおり、 shebang を忘れないようにしてください。フルパスで書かなければならず結構面倒です。ここもうまく書き換えれば shebang を省略できる気がするんですが、あんまり試してないので良く分かりません。

#!C:\Users\Personal\AppData\Local\Programs\Python\Python39\python.exe

a, b = map(int, input().split())
if a % 5 == 0:
    a -= 1
print(a + b)
PS C:\home\Rime> py rime test aplusb
Note: Running Rime under Windows will be unstable.
[   TEST   ] aplusb/py_wa
[   TEST   ] aplusb/py_wa: 02_random_01.in: PASSED
[   TEST   ] aplusb/py_wa: 02_random_02.in: PASSED
[   TEST   ] aplusb/py_wa: 02_random_03.in: PASSED
[   TEST   ] aplusb/py_wa: 02_random_04.in: PASSED
[   TEST   ] aplusb/py_wa: 02_random_05.in: PASSED
[   TEST   ] aplusb/py_wa: 02_random_06.in: Wrong Answer
[ COMPILE  ] aplusb/rust_correct: up-to-date
[   TEST   ] aplusb/rust_correct
[   TEST   ] aplusb/rust_correct: 02_random_01.in: PASSED
[   TEST   ] aplusb/rust_correct: 02_random_02.in: PASSED
[   TEST   ] aplusb/rust_correct: 02_random_03.in: PASSED
[   TEST   ] aplusb/rust_correct: 02_random_04.in: PASSED
[   TEST   ] aplusb/rust_correct: 02_random_05.in: PASSED
[   TEST   ] aplusb/rust_correct: 02_random_06.in: PASSED
[   TEST   ] aplusb/rust_correct: 02_random_07.in: PASSED
[   TEST   ] aplusb/rust_correct: 02_random_08.in: PASSED
[   TEST   ] aplusb/rust_correct: 02_random_09.in: PASSED
[   TEST   ] aplusb/rust_correct: 02_random_10.in: PASSED
[   TEST   ] aplusb/rust_correct: max 0.02s, acc 0.20s

Build Summary:
aplusb ... in: 40B, diff: 25B, md5: -
  rust_correct RUST   236 lines, 5.1kB
  py_wa        SCRIPT   6 lines,  142B

Test Summary:
aplusb ... 2 solutions, 10 tests
  rust_correct  OK  max 0.02s, acc 0.20s
  py_wa         OK  02_random_06.in: Wrong Answer

Error Summary:
Total 0 errors, 0 warnings

しっかり WA も判定できました

やった~~~~~~~!!!!!!!!!!!!!!!!!!!!!!

おわりに

とてもがんばりました。

よき作問ライフを!