塊2022

We :love: Katamari

Stray in Any%

STRAY

猫かわいいね

www.twitch.tv/videos/1541465800

走りましょう

上の動画で走った様子のチャートを書き出しました

drive.google.com

本当は帽子取得も OoB 使ってスキップできるんだけど, そこはいくら練習しても再現できないので諦めた

OoB(壁抜け)

適切なピクセルの2,3歩手前から走り, まっすぐ垂直に壁に飛び込む. 飛び込めなかったらピクセルが正しくない. 外から内(Seamus宅)に入る場合には頭を突っ込んで走り続けるといつか入れる(ねじ込める)方法もあるが, RTA では確実じゃないし時間が掛かるのでこれは練習しなくていいと思った.

Slum Skip

OoB を使って、Slum でのノート集めとその次のルーフトップをスキップできる. チャートにも書いたが, 流れは次の通り

  1. (初回)スラムに到着
  2. Seamus 宅に OoB で侵入, 壊れた Tracker を手に入れて修理するまでのミッションをこなす
    • Gradma からポンチョを手に入れた直後に「直前のチェックポイントからやり直す」を実行する
    • これによって Seamus の状態が修正される
  3. Seamus 宅に OoB で侵入, 修理された Tracker を見せると, スラム2のクリアに向かう, が, Seamus 宅のドアが閉じたままなので詰んだ状態になる
  4. ゲームをリセットすると「スラム」及び「ルーフトップ」がクリア状態になっているので「スラム2」を選択してやり直す(Tracker の修理から)

これで単純に10分程度の短縮になる

おまけ:壁抜け練習の様子と, Slum Skip のシーンだけのダイジェスト

www.twitch.tv/videos/1541491054

たこ焼きのレシピ

どこかにメモしたと思ったんだけど見当たらないので改めて.

次を用意する(たこ焼き 20 個用)

  • スーパー炎たこ
    • Iwatani から出てるやつ
    • いくつかバリエーションも値段も違うのがあるが, 縦横に溝が掘られたタイプならなんでもよい
    • 缶のカセットガスが必要なのでなければ買う
  • たこ焼きを回す用の串
    • 2本あると安心
  • サラダ油
  • キッチンペーパー
  • 具材
    • タコやウインナーなど
      • 好きなものを入れればよし
    • ネギ
      • 九条ネギがよし
    • 紅生姜
      • 特に嫌いでなければあったほうが良い
    • 天かす
      • 味の面でもきれいに焼く見た目の面でも必須
    • チーズ
      • オプショナルだが, 部分的に使ってもよし
  • 生地
    • 卵 K 個
      • K = 1 or 2
        • 私は K=2 が好き
        • どちらにするかで他の分量を少し変える
    • 薄力粉 90g if K=1 else 85g くらい
    • 水 350ml
      • K=2 なら少し減らす
    • 生地への味付け要素
      • 醤油, 顆粒だし, ごま油, 胡麻などなど
        • 最初の2つはかなりおすすめ
        • 後ろの2つはオプショナル(あってもなくてもそれはそれで旨い)
  • ソースなど
    • それは自由に

工程

  1. 具材の準備
    • 全部適度に刻んでおく
    • 紅生姜は液に浸かってるなら水切りし, 細かく刻む
    • お皿に盛っておいて, すぐに使えるように置いておく
  2. 生地
    • ボウルに薄力粉と水を併せてよく溶く
    • 卵を K 個入れてよく溶く
    • 味付け要素を全部入れてよく溶く
  3. スーパー炎たこに油を塗りたくる
    • 丸めたキッチンペーパーを刷毛のように使うと便利
    • 穴の内側だけじゃなくて平らな部分も使って焼くので忘れないこと
  4. 最大火力で鉄板を熱する
    • 温まったら試しにいくつかの穴にタコを入れて熱され具合を眺める
      • 生地を少し入れるのでもいいが
    • 流派によるが, タコだけを最初に焼きたいならここで焼いてしまう
  5. すべての穴の 1/2 ~ 3/4 くらいが満たされる具合に生地を流し込む
    • 雑でいい
  6. 次の工程を手早くやる自信がない場合はここで中火に弱める
  7. 生地が固まらないうちにさっさと具材を入れる
    1. メイン具材(タコやウインナーなど)は漏らさず1つずつ穴に落としてく
      • 最初にタコを入れた穴はスキップする
    2. ネギ, 紅生姜, 天かす, チーズ等を入れる
      • 穴が 4x5 に配置されてるので, その真ん中の 2x3 の上に一旦全部盛る
      • そこから串など使って散らせば良い
  8. 残りの生地をすべて入れる
    • 上手く行っていれば, 鉄板の縁ギリギリまで生地が注がれているはず
      • これを生地に混ぜる水の量で調整できればプロ
  9. 移行ずっと中火
  10. 鉄板上に掘られた溝に沿って串を走らせる
    • 生地の焼かれ具合をこれでテストしている
    • 串で生地を切ることが出来るギリギリの硬さになったら, ひっくり返せる
  11. 火の強い穴から順にひっくり返す
    • ひっくり返そうとしてる穴の周辺にある溝(4辺)に串を走らせて, 生地を切っておくこと
      • 今切った矩形箇所の生地すべてが, その穴の上で作るたこ焼きの材料になる
      • 回転させるときに下の面に周辺の生地を押し込む
    • 無理そうだったら早めに諦めてその穴は放置する
      • 極力触らないのが一番良いとされている
  12. 焼きの仕上げ
    • 食べてもいいかなくらいに焼き上がったら隙間にサラダ油を追加, 塩を振って, 留めを刺す

photos.app.goo.gl

; easy-scraper が便利なので Python に移植した(してる)

Tl;dr

pypi.org

これはなにか

とにかく簡単に使えることに注力した web スクレイピングライブラリであるところの

github.com

Pythonで再実装した。 まだ差異があるが、その内寄せる。

既存の何が駄目で easy-scraper の何が良いか

web スクレイピングとは次のような操作だ

  1. ターゲットにする web ページを調べる
  2. 欲しい情報が在る場所をルートからトラバースして見つける
  3. 欲しい情報が構造としてあるなら, 情報たちの関係も特定する
  4. 2, 3, で人手でやったことをシミュレーションするプログラムをコーディングする

2,3 は容易な作業ではないが、それをまたシミュレーションする必要があるのも一苦労だ。ライブラリはたいてい jQuery のような API を提供するからドキュメントと睨みっこしながらプログラミングすることになる。easy-scraper は、このシミュレーション部分を省略できる。

どう使うか

欲しい情報はたいてい、すぐ周りの構造を見れば位置を特定できる。 最も簡単なパターンはある名前のタグ、あるいはある名前の class のついたタグに囲まれているといった具合だ。例えば、WEBページのタイトルは <title> に囲まれている。これはほとんどいつでもそう決まってる。

よくあるライブラリであれば, 1. title という名前のタグを取得する 2. 取得したDOMの中の innerText を得る(innerText という用語はラフに使ってます) ということを自分でする必要がある.

easy-scraper では代わりにHTMLパターンを書く。title タグの中の innerText は次のパターンで書く。ここで {{ x }} がマッチしたテキストを補足するためのプレースホルダーで、x というのは変数名である(自由な名前を使える)。

<title>{{ x }}</title>

easy-scraper はこれをパターンマッチさせることで、欲しいテキストを取得する。

import easy_scraper

body = '<html><head><... 検索対象HTML...'
pattern = '<title>{{ x }}</title>'

result = easy_scraper.match(body, pattern)
# [ {"x": "..."}, ... ]

一つのマッチ結果は辞書で表現される。ここではパターン中の変数が x だから、x をキーとして、値にマッチしたテキストを格納した辞書 dict[str, str] になる。そしてマッチ結果は一つとは限らない。<title> タグが何度も使われてるならその個数だけマッチする。というわけで match の返り値は辞書のリスト list[dict[str, str]] になる。

今のは最も単純なパターン表現だ。 パターンでは入れ子関係を表現できる。 例えばさっきの例では body 以下の title タグを(もしあれば)拾ってしまう。 head 以下の title であることを表現するパターンは次の通り。

<head><title>{{ x }}</title></head>

注意点としてはこの入れ子はあくまで子孫関係を表していて、直接の親子関係に限ってはいない。

パターンは innerText だけでなく属性にマッチさせることもできる。例えば a タグの href 属性を innerText と一緒に拾うには次のパターンを使う。

<a href="{{ linkUrl }}">{{ innerText }}</a>

再実装に過ぎないライブラリの説明にしては丁寧すぎたと自分で思うので、オリジナルライブラリの作者による解説

tanakh.hatenablog.com

も併せて読んでほしい。

オリジナルとの差異

次の2つの差異がある。あることは認識しているが、こういうのは不便さを自分で実感しないと埋める意欲が沸かなくて…。

  1. 兄弟関係に連続性を仮定していない
  2. 部分文字列パターンマッチが無い

パターン <div><A /><B /></div> とあるときにオリジナルのライブラリでは <A /> があって、そのすぐ後ろに <B /> が来ることを表現している。その間に 0 個以上の余分な要素があることを許すには ... を使って <div><A />...<B /></div> と書くことになっている。この再実装版では ... という文法をそもそも用意しておらず、後者の意味でしか解釈していない。つまり、

<div>
  <A />
  <B />
</div>

の意味は、div タグの内部であって、A タグがあって、0個以上の要素を飛ばして、B タグが出現すること、を意味する。A と B は直接隣り合う必要はなく、順序だけが保証されている。

部分文字列のパターンマッチがない。 本家では <span>{{ num }} usres</span> というパターンが書けて、これは <span>123 users</span> にマッチして {"num": "123"} を返す。便利だ。今回の再実装版ではタグの innerText はプレーンなテキストか、{{ ... }} 一個が置かれてるかしか許していない。 これはあったら絶対便利だろうな… パターンに関する字句解析をもうちょっと真面目にする必要があるから、モチベーションがもう2つくらい上がったら手をつける。

走れ!塊魂アンコール RTA

私は走ってるけど皆は?

RTA のための環境

2022年の密かな趣味としてRTAをやってる.

  • Steam
  • Twitch
    • ゲームに特化した配信サービスだが, 私はせいぜい配信しておけば映像を勝手に保存してくれるサービスとして使っている
      • その点で YouTube でもなんでもいいわけだが, とにかく配信ソフトの OBS と相性がよく出来てるし, 配信の安定性もよいので, Twitch じゃない理由がないので Twitch を選んでる
    • 配信した映像は課金状態にもよるが一定期間で消える
    • 残したい記録が出来たらダイジェストとして切り出しておけばずっと残る
      • speedrun への申請はこれを提出している
  • OBS
    • 配信ツール
    • Twitch との相性が良い, 各設定が JSON ファイルでエクスポートしておける
  • LiveSplit
    • ゲームの split タイム(スタートから各フェーズまでに掛かった時間, ゴールまでの時間)を管理・記録するツール
    • PC 外のゲームならホットキーを指定して, 頑張って記録する
    • PC のゲームなら, Auto Splitters という機能で自動的に(!)記録することができる
  • Katamari Damacy Reroll - speedrun.com
    • 記録のリーダーボードだけじゃなく, 先程の asl ファイルのような役に立つ資源や初心者へのガイドがあったりする
    • フォーラムはあんまり活用されてなくて, 後述する Discord が使われている

Twitch の VOD 設定

f:id:cympfh:20220206162732p:plain

読め

塊魂RTAに関することは始めたての私が無理に解説を書かなくても先行研究がいくらでもある.

note.com

docs.google.com

コミュニティ

塊魂RTA走者のための Discord サーバがあります. 招待リンクへの間接的なリンクですが置いておきます.

www.speedrun.com

Autosplitter ファイル

基本的に配布されているものでいいんだけど, 私の環境だと確率的に上手く動いたり動かなかったりするので困っていた. print 関数を使うと print デバッグができる(DebugView でログを見る)のでやってみると, newGameDialogValue が上手く補足されておらず, 中身が常に 0 になっていた. これは「新しい塊を転がしますか?」に対して「はい」を押したか「いいえ」を押したかが入る値のようだ. RTA において「いいえ」を押すことはないから, この値はチェックする必要が実は無い. 仮に「いいえ」を押して split が始まったとしても, また次に「はい」か「いいえ」を押すときに reset させればいい.

ライセンスのないスクリプトファイルなので改変版を勝手に公開してよいか怪しいが, ここにある.

変更点は start の条件文が違う(newGameDialogValue に関する等式を削ってる)のと, reset ブロックを追加した.

その他 Tips

塊魂アンコールは PC (Steam) / PS4 / Switch で遊べる. speedrun では全て一緒くたにされているが, ローディング時間の短さから, PC (Steam) が断然に有利である. そこで, 良質な SSD を手に入れてそこにインストールすることがとても重要である. 私の手元では HDD から SSD にしただけで1ステージあたり 7 秒程度縮んだ.

Windows/WSL2 で自宅サーバを建てる

TL;DR

やれば出来る子

構成

  • Windows10
  • WSL2 (Ubuntu 20.04)

Intro

2019年あたりからWSLが実用的になりつつあり, それまで基本的には Linux (特にUbuntu), 仕事用は Mac で生きていた私にも Windows を見直そうという潮流がやってきた. Windows/WSL(2) での生き方はこう.

  • GUIGUI したもの(webブラウジング, Slack, Discord 等)は Windows ネイティブ
  • ターミナルしたものは, Windows Terminal + WSL
    • いわゆる開発は全てここで完結している
  • VcXsrv で Windows 上に X Window サーバを立てておく
    • これでクリップボードWindows と WSL とで両立される
    • WSL からちょっとした GUI は使える(たまに画像を表示するくらいにしか使わない)

特に WSL2 からは Docker for Desktop を Windows に入れておいて WSL2 Engine にチェックを入れておけば WSL2 から docker コマンドがシームレスに触れるようになる (WSL 上で docker は入れない).

さて自宅サーバは相変わらず Ubuntu だった. これは主には samba を用いたファイルサーバとして使っていて最近はカメラが趣味になったので写真を大量に保存している. その他にも自分用のお粗末なwebサービススクリプトを自宅LAN内で動かす用に使っている. この度このマシンが無事ぶっ壊れた(WiFiモジュールが怪しいと踏んでる)ので, 強めのデスクトップマシンを Windows/WSL2 で自宅サーバとして持っておくことにした.

やること

  • マシンを買う
  • Windows10 のセットアップ
  • WSL2 を導入する
  • Docker for Desktop を導入する
  • samba のセットアップ
  • sshd のセットアップ
  • ポート開放 & フォワディング

マシンを買った

マウスコンピュータで買った. 大好きな 名取さなが広告塔に起用されている から, というのも大きなきっかけだけど, ここでBTOでカスタマイズしてみたものを他で再現してみて値段を比較してみると確かにここが安かったので, 良かったね. メモリとストレージはあればあるだけ使うので(最近の Rust のコンパイルは重くなった)贅沢に積んだ. グラボは今まで気にしたことがなかったけれど GeForce RTX 3060 というのが勝手についてきた. 調べると RTX 3000 シリーズの中の廉価版らしいが, 大抵のゲームが快適に遊べるらしい. FPSみたいなゲームには興味がないのでここはどうでもいいところだが, ついてくるんだからしょうがない. せっかく手に入れたら遊びたくなるもので, 最近は Euro Truck Simulator2 を毎日プレイしている. サーバだと言いながらモニタにはしっかり繋いで遊んでる.

あ、OS は Windows 10 Pro です.

WSL2 を導入する

頑張ってググってやる.

  1. BIOS の仮想化の設定をONにする
  2. "Windows の機能の有効化" で仮想化をON, Hyper-V をOFF, WSLをONにする
  3. 頑張ってググってWSLを入れる
  4. Ubuntu イメージ(或いは他イメージ)を入れる
  5. イメージをWSL2に変換する

WSL 周りは今でも進化を続けてるのでこのあたりは変わるだろうが, 1 は Windows の外のことなので必要だと思う. 私のマシンは届いた状態で 1, 2 は設定済みだった.

そして最近の Windows10, 及び Windows11 からは 2 以降の設定が

# PowerShell
$ wsl --install

で済むらしい!! docs.microsoft.com

ここらへんの操作は「管理者権限」で実行した PowerShell で全て行う.

私の環境では上記のオプションが入ってなかったので, docs.microsoft.com これに従った. 後半の手順はすっ飛ばしても使えるようになってしまったので仕方ない.

Ubuntu イメージは私は Microsoft ストアから入れてる. これで入れたイメージはデフォルトでは WSL1 なものなので,

wsl --set-version Ubuntu 2 # "Ubuntu" はイメージ名に適宜変更する

で WSL2 用にイメージを変換する必要がある. この変換作業は数分かかるが, 作りたてのイメージなら早く済む.

Docker for Desktop を入れる

最近, 有償になった と話題の Docker for Desktop を導入する. 個人で使うならあくまで無料なので気にしないで入れる. ただしそのリリースに併せて発表されたバージョン 4.0 以上を入れようとすると私の環境ではインストール実行時にエラーを吐いてインストールが出来なかったので, バージョン 3.6.0 のインストーラーを引っ張ってきてインストールした. 動かしてると無限に Upgrade しろって言ってる.

インストールが完了したら, 設定で WSL2 だけはチェック入れておく. f:id:cympfh:20211022154908p:plain

こうすると WSL イメージとして docker-desktop, docker-desktop-data という2つが追加され, また他の WSL イメージから何故か docker というコマンドが使えるようになる.

# PowerShell
$ wsl --list  
Linux 用 Windows サブシステム ディストリビューション:  
Ubuntu (既定)  
docker-desktop  
docker-desktop-data  

ところで, WSL は最初に入れたイメージをデフォルト(規定)のイメージとして扱う. 想定していないイメージ(例えば docker-desktop)がデフォルトになってると今後の操作が面倒なので, PowerShell から wsl コマンドでデフォルトを変更しておく.

samba セットアップ

私は主にはファイルサーバとして使いたいので samba サーバを建てる. WSL2 の上に samba サーバを建てるのもその気になれば出来るが, WSLを介するとファイルアクセスが重たくなるので素直に Windows 上に直接建てる.

ヒント

samba サーバの建て方は普通にググるLinux サーバの上に建てる方法ばかりがヒットすると思う. Windows Server に建てる方法をググると良いです. 別にこれは Windows Server じゃなくて Windows10 Pro なんだけど, 関係ないです.

Windows 内において SMB とか 共有 って言葉があったらそれは samba のこと.

例によって "Windows の機能の有効化" で SMB と名の付くものは全てチェックを入れる(サーバだけで十分かもだけど全部入れておいて悪くない). f:id:cympfh:20211022155948p:plain

これでOS再起動すれば samba サーバは立ち上がっている. 次に共有したいフォルダを指定する必要がある. これで初めて実際にファイルにアクセスできるようになる.

Windows 標準のファイラで共有したいフォルダを右クリックしてくと 共有 の文字が見つかるのでこれを有効にする. f:id:cympfh:20211022160222p:plain

参考リンク

sshd のセットアップ

サーバである以上他のマシンからSSH接続くらいさせて欲しい. それも Windows にじゃなくて WSL2 にログインさせて欲しい. これは WSL2/Ubuntu で普通に sshd セットアップすればよい.

# WSL2
$ sudo apt install openssh-server
$ sudo service ssh status
$ curl -sL https://github.com/cympfh.keys > ~/.ssh/authorized_keys  # 各自鍵の設定
$ sudo service ssh restart

ちなみに sytemctl コマンドで触ろうとしたら怒られた. また, 後述するようにまだポートフォワディングされてないので, まだ他のマシンからのアクセスはできない. 一旦同じ Windows 上の PowerShell からのアクセスを試みる. なぜか localhost としてアクセスできるはずなので, これでログインできればOK.

# PowerShell
$ ssh ${USER}@localhost  # もちろん WSL2 でのユーザー名

これでアクセスできなかったら sshd セットアップが出来てないので駄目です.

ポート解放 & フォワディング

同一マシン上で Windows から WSL へは謎の力のおかげでか localhost として簡単にアクセスできてしまうのだが, 他のマシンからは WSL は Windows によって包まれていて直接アクセスはできない.

  1. ファイヤーウォールによってポートが防がれていて, 他マシンから Windows にアクセスできない
  2. Windows にアクセスできても WSL2 は見れない

1 については Windows の設定を素直に頑張るしかない. WindowsFirewallWindows Defender とかいう名前だったりする. 同じものなのかな. GUI の設定で頑張ってポートを開ける設定を追加する. 一回設定したらずっと残るので最初の一回だけ頑張る.

f:id:cympfh:20211022163255p:plain f:id:cympfh:20211022163237p:plain

そういえばこの中に ping の設定が初めからあって, デフォルトでは無効化されている. ping が出来ないのは不便なので有効化しておくとよい. ping コマンドでとりあえず他マシンから Windows 機に疎通できるか確認するとよい. Windows機自体のIPは ipconfig で見れるし, アクセス元がまた Windows (或いはWSL)ならマシン名ででもアクセスできる(!).

ここまでで Windows は見れるようになった. しかし, それでも WSL2 を直接見ることはできない. f:id:cympfh:20211022162024p:plain

というわけで, Windows 上でポートフォワディングの設定をしておく. これによって, Windows 自身のポートとWSL2のポートを同一視させることができるので間接的に WSL2 にアクセスすることができる. これについては tkyonezu.com これをそのまま試すと上手くいった.

スクリプトbash で書きたい都合上, WSLの中で実行する bash スクリプトとして書いておいて, これを PowerShell から wsl --exec で実行させれば良い.

SSH用の22番ポート以外のポートもあらかたフォワディングしておく.

#!/bin/bash

IP=$(ifconfig eth0 | grep 'inet ' | awk '{ print $2 }')

ports() {
    echo 22
    echo 80
    seq 8000 9500
}

echo Cleaning
for PORT in $(ports); do
    netsh.exe interface portproxy delete v4tov4 listenport=${PORT}
done

for PORT in $(ports); do
    echo Fowarding ${IP}:${PORT}
    netsh.exe interface portproxy add v4tov4 listenport=${PORT} connectport=${PORT} connectaddress=${IP}
done

exit 0

で、これを Windows が起動した直後のタイミングで実行する. というのも Windows から見た WSL の IP は起動のタイミングで初めて決定するから.

おわり

WSL2 側の設定は普通の Linux と思ってやれば良いので簡単. Windows 側の設定は目を凝らしてGUIを頑張る. さすが世界で一番普及してるOSなだけあって必要な設定項目は探せばどこかにはある. 探すのが大変なだけで.

2021年逆にPrologでAtCoderをやる

先駆者様たち

qiita.com

qiita.com

frfrfrfr.hatenablog.com

経緯

いやほんと, 知らんうちに Prolog(SWI-Prolog)が AtCoder で使えるようになっており. Prolog はその言語の性質上, 全探索をしてしまうようなアルゴリズムと相性が良い.また, 単純なパターンマッチをするだけといったものも書きやすい. 残念ながら計算速度の観点から, 全ての問題を Prolog で解くのは無理だと思うのだが, ABC の A問題や B問題くらいなら大抵解けるし, Prolog の恩恵を受ける問題もあると思った.

テンプレート, 入出力など

競技プログラミングの文脈でテンプレートというのは, あまりにもよく使うので問題を解く前から予め関数をいくつか定義しておいたりした状態のボイラープレートとしてのソースコードのことを言う. 特に競技プログラミングは入出力に標準入力・標準出力を使い, その形式も多少癖がある.そして Prolog は必ずしても適していないため, 自分で便利に使える入出力用の述語を定義しておく.

暫定版として, 次が私のテンプレートである.

main :-
    % ここにコードを書く
    writeln(Ans).

% stdio
get_string(String) :-
    read_string(current_input, ' \n', '', _, String).

get_chars(Chars) :-
    get_string(String),
    string_chars(String, Chars).

get_atom(Atom) :-
    get_string(String),
    atom_string(Atom, String).

get_number(Number) :-
    get_string(String),
    number_string(Number, String).

get_numbers(0, []).
get_numbers(N, [X|Xs]) :-
    get_number(X),
    N1 is N - 1,
    get_numbers(N1, Xs).

% math
even(X) :- X mod 2 =:= 0.
odd(X) :- X mod 2 =:= 1.

% list
head([X|_], X).
tail([_|Xs], Xs).
last([X], X).
last([_|Xs], Z) :- last(Xs, Z).
take(0, _, []).
take(_, [], []).
take(Length, [X|Xs], Prefix) :-
    M is Length - 1,
    take(M, Xs, Ps),
    Prefix = [X|Ps].
minimum([X], X).
minimum([X|Xs], Z) :- minimum(Xs, U), Z is min(X, U).
maximum([X], X).
maximum([X|Xs], Z) :- maximum(Xs, U), Z is max(X, U).
sum([], 0).
sum([X|Xs], S) :- sum(Xs, T), S is X + T.
contain([X|_], X).
contain([_|Xs], X) :- contain(Xs, X).
nth([X|_], 1, X).
nth([_|Xs], K, Ans) :- K1 is K - 1, nth(Xs, K1, Ans).
count([], _, 0).
count([X|Rest], X, N) :- !, count(Rest, X, M), N is M + 1.
count([_|Rest], X, N) :- count(Rest, X, N).

頭の方にある % ここにコードを書く の部分を埋めて提出する. % stdio に続くブロックに入力周りを書いてある.この定義は先駆者様たちのコードを多いに参考にしている.出力する述語は組み込みの write または writeln で事足りているので自分で定義はしていない. それより後ろには汎用的と思われる述語をいくつか自分で定義している.のだが, SWI-Prolog に詳しくないので, 実は組み込みの述語で十分だったりして, いくつか不要な可能性もある.逆に今後増えてく可能性は多いにある.

精選 10 問

qiita.com

先駆者様のN番煎じになるので恐縮だがさっと解いてこう. コードを載せてくが, テンプレートで定義した述語はその定義を省略し, main と追加で定義した述語の定義だけ書く.

ABC086A - Product

main :-
    get_number(A),
    get_number(B),
    get_number(C),
    get_string(S),
    Sum is A + B + C,
    writeln(Sum),
    writeln(S).

自前で定義した入力を受け取る get_* はスペースと改行を区切り文字としてあるので, 勝手にトークナイズされて気にする必要はない.

PracticeA - Welcome to AtCoder

main :-
    get_number(X),
    get_number(Y),
    (even(X * Y) -> write("Even"); write("Odd")),
    nl.

(A -> B; C)A という述語が成り立つなら B を評価し, さもなくば C を評価する. いわゆる If 文として使うことができる. 外側の () は必ずしも必要ではないが, あったほうが良い. 最後の nl は改行文字を出力するだけの述語.

上では If 文の結果で答えを出力させており, いかにも手続きプログラミングといった味付けだ. 次のように答えの文字列を直接得るための述語 solve を別途用意する方が Prolog の趣があるかもしれない.

main :-
    get_number(X),
    get_number(Y),
    solve(X, Y, Ans),
    writeln(Ans).  % Ans を出力して改行

solve(X, Y, "Even") :- even(X * Y).
solve(_, _, "Odd").

ABC081A - Placing Marbles

与えられるのは数値だが, 文字列として処理したいので初めから文字列として受け取る. get_chars は char のリストを返す. このリストが '1' を含む個数を求める.

main :-
    get_chars(S),
    count_one(S, Ans),
    writeln(Ans).

count_one([], 0).
count_one(['1' | Xs], N) :-
    count_one(Xs, M),
    N is M + 1.
count_one(['0' | Xs], N) :- count_one(Xs, N).

ABC081B - Shift only

main :-
    get_number(N),
    get_numbers(N, As),
    solve(As, Ans),
    writeln(Ans).

solve([A], Ans) :- halven_times(A, Ans).
solve([A|As], Ans) :-
    halven_times(A, X),
    solve(As, Y),
    Ans is min(X, Y).

halven_times(X, 0) :- odd(X).
halven_times(X, M) :-
    even(X),
    X2 is X // 2,
    halven_times(X2, M1),
    M is M1 + 1.

get_numbers は個数を指定して, その長さの列を読み取って返す. solve はこのリストの各値に halven_times を適用してその最小値を返してる. halven_times は一つの整数を最大何回 2 で割り算できるかを求めている.

ABC087B - Coins

ようやく Prolog らしい問題だ. 3つの自然数 A B C について全探索をし, ある性質が成り立つものの通り数を求めたい. これをするには, その成り立って欲しい性質を述語として定義し, findall という述語にそれを渡せば, 成り立つもの全てがリストとして返ってくる. ここではそのリストの長さが答えだ.

main :-
    get_number(A),
    get_number(B),
    get_number(C),
    get_number(X),
    findall(_, solve(A, B, C, X), Rs),
    length(Rs, Ans),
    write(Ans), nl.

solve(A, B, C, X) :-
    between(0, A, I),
    between(0, B, J),
    between(0, C, K),
    500 * I + 100 * J + 50 * K =:= X.

findall の結果がリスト Rs として返ってくる. 第一引数には何に関するリストを求めたいかを指定するのだが, ここでは中身は興味が無いので _ としてある. =:= は両辺を式として評価した結果についての等号.

ABC083B - Some Sums

一つの整数について, 各桁の和を求めるような述語を一つ定義する. この定義自体はつまらなく再帰関数のノリで計算させればよい. これが満たすべき条件を定義したら, やはり findall に入れることで, 成り立つもの全てを集めることができる. さっきの問題では _ とした第一引数にそのことを入れるのである.

main :-
    get_number(N),
    get_number(A),
    get_number(B),
    findall(X, solve(N, A, B, X), Rs),  % solve を満たす全ての X を集めたものがリスト Rs.
    sum_list(Rs, Ans),
    write(Ans), nl.

% X が条件を満たす?
solve(N, A, B, X) :-
    between(1, N, X),
    sum_of_digits(X, S),
    between(A, B, S).

% X の各桁の和が S.
sum_of_digits(X, S) :-
    X < 10,
    S is X.
sum_of_digits(X, S) :-
    X >= 10,
    Head is X // 10,
    Tail is X mod 10,
    sum_of_digits(Head, T),
    S is T + Tail.

ABC088B - Card Game for Two

解放だが, 列を降順にソートしたら先頭から交互に取り合えば良い. というわけでソートをする必要がある. 知っておくべき組み込みのソートとして sortmsort がある. ただの昇順ソートは msort で良い(msort の m は merge sort の m?). reverse でそれをさらにひっくり返せばほしかった降順ソートになるが, sort は比較演算子を直接渡せる. ちなみに @> を渡すと, 重複する値が勝手に消されるので注意.

main :-
    get_number(N),
    get_numbers(N, A),
    sort(0, @>=, A, B), % 比較に使うキー, 比較演算, ソート前, ソート後
    solve(B, Ans),
    writeln(Ans).

solve([], 0).
solve([X], X).
solve([X|Xs], D) :-
    solve(Xs, E),
    D is X - E.

ABC085B - Kagami Mochi

列の内, 異なる値はいくつあるかという問題. sort@<@> を渡すと重複する値を消してくれることを利用すると, これがさくっと実現できる.

main :-
    get_number(N),
    get_numbers(N, A),
    sort(0, @<, A, B),
    length(B, Ans),
    write(Ans), nl.

ABC085C - Otoshidama

これもつまらない全探索. ある条件を満たすものが一つでもあればその解を出力する. (-> ;) を使えばよい.

main :-
    get_number(N),
    get_number(Y),
    solve(N, Y, A, B, C) -> (
        % 満たす A,B,C があるならそれを出力
        writeln(A),
        writeln(B),
        writeln(C)
    );
        writeln("-1 -1 -1").

solve(N, Y, A, B, C) :-
    between(0, N, A),
    NA is N - A,
    between(0, NA, B),
    C is N - A - B,
    10000 * A + 5000 * B + 1000 * C =:= Y.

ABC049C - Daydream

こんなもん完璧に Prolog のためにあるような問題で, 問題文を実直にコードに翻訳すると勝手に処理系が解いてくれる.

main :-
    get_chars(S),
    (ok(S) -> writeln("YES"); writeln("NO")).

ok([]).
ok([d,r,e,a,m | Rest]) :- ok(Rest).
ok([d,r,e,a,m,e,r | Rest]) :- ok(Rest).
ok([e,r,a,s,e | Rest]) :- ok(Rest).
ok([e,r,a,s,e,r | Rest]) :- ok(Rest).

この ok という述語が, 空から始めて, dream dreamer erase eraser の付け足して成立させられるかを全探索してくれる.

ABC086C - Traveling

これはあんまし面白くない. こういうガリガリ実装するだけとなると, Prolog という言語はなんだか Lisp を書いてるような気持ちになる.

main :-
    get_number(N),
    solve(N, 0, 0, 0).

solve(0, _, _, _) :- writeln("Yes").
solve(N, T0, X0, Y0) :-
    get_number(T),
    get_number(X),
    get_number(Y),
    DT is T - T0,
    Dist is abs(X - X0) + abs(Y - Y0),
    ok(DT, Dist) ->
        N1 is N - 1,
        solve(N1, T, X, Y);
        write("No"), nl.

ok(DT, Dist) :-
    DT >= Dist,
    even(DT - Dist).

スーパーカブの道交法違反

今日のインターネット

バイクは免許をとって一年間は二人乗りができないという法律がある。アニメ「スーパーカブ」では主人公が免許を取り立てにも関わらずさらっと友達と二人乗りをしていて、おやっという声が上がっていた。 このアニメでは主人公は素朴で大人しい女の子であり、ルールを破るような素行が全く似つかわしくなく、アニメは全てリアル志向であっただけに、この描写だけが浮いて見えたというのが私の感想だった。 違和感があっただけであって、炎上したなどというわけではない。

というわけで次の指摘は話が違う方向を向いている。

「フィクション」だろうが、「ドキュメンタリー」だろうが、スポンサーがついて監督がいる映像作品である以上、嘘を固めて描きたいものを描いているだけに過ぎない。そんなのは誰も分かっているものだ。

この件でKADOKAWAは謝罪はしていないし、しなくていい。 少し昔の映像作品を見れば高校生が飲酒喫煙をしているし、そのような作品は現在だって禁止されているわけではない。ただ、見ている人に受け入れられないだけだ。

おりもとみまなの、この意見がしっくりきた。 視聴者からすれば、面白いかが全てだ。表現の自由は応援する。是非とも自由を活用してほしいが、自由を、興ざめな作品の理由にしないで欲しいとは思う。

ちなみにおりもとみまなは「ばくおん」というやはりバイクの漫画を描いている作家だが、「スーパーカブ」についてはアニメ化するずっと前から「ばくおん」の中でも皮肉たっぷりに取り上げられているので面白いです。 バイクを知っている人からすれば、「スーパーカブ」という作品は嘘を散りばめまくっているという元々そういう作品に見えるんだろう。 私は全然知らずに楽しんでるライト層なので、全く感想が違う。