スコフィごいたの同期変数の実装

はじめに

スコフィごいた Scofi-Goita は、石川県能登地方の伝統ボードゲーム「ごいた」をVRChat上で遊べるようにしたワールドです。駒はスコフィ(1058mart制作アバター)。 ごいたは4人・2チームで遊ぶゲームで、将棋の駒と同じ形をした32枚を使い、「攻め」「受け」「なし」を繰り返していち早く手駒をなくすことを目指します。

この記事ではソースコードの中でも同期変数の圧縮・展開まわりに絞って、技術的な話をしようと思います。

少し裏話になりますが、このワールドはもともと「ちゃんと作ろう」という気持ちで始まったわけではなくて、「暇だしごいたのギミックでも書いてみるか」くらいの軽いノリで書き始めた実験コードでした。ゲームを完成させることよりも「同期変数をどこまで削れるか」という課題の方が面白くなってしまって、必要以上に凝った圧縮処理を書き込んでいました。

そんな中で、

  • 「駒がスコフィだったら絶対かわいい」という声が上がり
  • 試しにやってみたら本当にハマって
  • その様子がスコフィの作者であるinugoyaさんに見つかり
  • これはちゃんとしたワールドにしないと……となり

という、まったく予期しない方向転換が起きました。

結果として、暇つぶしで作った過剰に圧縮された同期ロジックが、そのまま本番ワールドへ引き継がれることになりました。

「せっかくこの圧縮ロジックがあるなら、LateJoiner にも完全同期できるし帯域も節約できる、このまま使えばいいじゃないか」という判断でそのまま採用した結果、実験的な実装が設計の根幹になってしまいました。

こうして、同期変数にゲーム状態を極限まで詰め込み、FieldChangeCallback で受信したら展開して状態を復元するという構造が生まれました。ただ当然ながら、本来そこまで複雑にする必要のない部分まで圧縮してしまったので、バグが紛れ込みやすい作りになってしまいました。ワールド公開後や大会のときにいくつか不具合を起こしてしまい、プレイヤーの皆さんにご迷惑をおかけしました。本当に申し訳なかったです。

技術的には我ながら面白いと思っているので、ここで紹介します。

なぜ「圧縮」が必要なのか

VRChatでゲームの状態(誰がどの駒を持っているか、盤面の様子、スコアなど)を参加者全員に届けるには、その情報をネットワーク越しに送信する必要があります。 UdonSharpで BehaviourSyncMode.Manual を使う場合、RequestSerialization() を呼ぶことで同期対象のフィールドが送信されます。送信データが大きいと送信レートが制限されるため(参考: Networking Specs & Tricks | VRChat Creation)、なるべく少ない同期変数に多くの情報を詰め込むのが重要です。

スコフィごいたでは、

  • 複数卓を並べる構成でも同期負荷を抑えたい
  • LateJoiner にも完全なゲーム状態を渡したい

という要件があって、手駒・盤面・スコア・ターン情報といったゲーム状態をすべて同期変数に持たせる必要がありました。

そこで「1つの変数にどれだけ情報を詰め込めるか」という方向で、ビット演算や進数変換を使った圧縮方式を採用しています。

同期変数は以下の16個です(参加プレイヤー名の文字列を除く)。

[UdonSynced, FieldChangeCallback(nameof(P1))] private uint _p1;
[UdonSynced, FieldChangeCallback(nameof(P2))] private uint _p2;
[UdonSynced, FieldChangeCallback(nameof(P3))] private uint _p3;
[UdonSynced, FieldChangeCallback(nameof(P4))] private uint _p4;
[UdonSynced, FieldChangeCallback(nameof(B1))] private uint _b1 = 1212696648u;
[UdonSynced, FieldChangeCallback(nameof(B2))] private uint _b2 = 1212696648u;
[UdonSynced, FieldChangeCallback(nameof(B3))] private uint _b3 = 1212696648u;
[UdonSynced, FieldChangeCallback(nameof(B4))] private uint _b4 = 1212696648u;
[UdonSynced, FieldChangeCallback(nameof(NextGameBits))] private byte _nextGameBits = 0;
[UdonSynced, FieldChangeCallback(nameof(ContinueBits))] private byte _continueBits = 0;
[UdonSynced, FieldChangeCallback(nameof(Turn))] private byte _turn = 0;
[UdonSynced, FieldChangeCallback(nameof(Both5Si))] private bool _both5Si = false;
[UdonSynced, FieldChangeCallback(nameof(Score))] private int _score = 0;
[UdonSynced, FieldChangeCallback(nameof(PickupMode))] private bool _pickupMode = false;
[UdonSynced, FieldChangeCallback(nameof(TabletFixedStates))] private byte _tabletFixedStates = 0;

これらの変数でごいた全体のゲーム状態を表現しています。それぞれどういう圧縮をしているか、順番に見ていきます。

圧縮① P1~P4(uint × 4)— ビットマップで駒の配布を表現

各プレイヤーの手駒は uint(32ビット整数)1つで表します。 ごいたのコマは全部で32枚(「王」×1、「玉」×1、「飛」×2、「角」×2、「金」×4、「銀」×4、「馬」×4、「香」×4、「し」×10)で、各コマに0~31のインデックスが振ってあります。

「ビット」は0か1の2値しか取れない情報の最小単位で、uint は32個のビットを横に並べた整数型です。駒が32枚でビットも32個、ちょうど同じ数なので「第i番ビットが1 ⟺ i番の駒を持っている」というON/OFFとして使えます。

エンコード側はこうなっています。

// OwnerShuffle内(Owner側が駒をシャッフルして配布)
for (int i = 0; i < 8; i++)
{
    p1 |= 1u << shufflePieces[i << 2];     // 0,4,8,...番目の駒 → P1
    p2 |= 1u << shufflePieces[i << 2 | 1]; // 1,5,9,...番目の駒 → P2
    p3 |= 1u << shufflePieces[i << 2 | 2]; // 2,6,10,...番目の駒 → P3
    p4 |= 1u << shufflePieces[i << 2 | 3]; // 3,7,11,...番目の駒 → P4
}

i << 2 は i * 4、i << 2 | 1 は i * 4 + 1 と同じです。

デコード側は各ビットを走査して、1 が立っている位置から駒インデックスを復元します。

// SyncPieces内
for (int i = 0; i < 32; i++)
{
    if ((P1 >> i & 1) == 1) playerPieces[0][index1++] = i;
    if ((P2 >> i & 1) == 1) playerPieces[1][index2++] = i;
    if ((P3 >> i & 1) == 1) playerPieces[2][index3++] = i;
    if ((P4 >> i & 1) == 1) playerPieces[3][index4++] = i;
}

配布完了の判定は、4人分の OR が 0xFFFFFFFF(全駒が誰かの手に)かつ AND が 0(同じ駒を2人が持っていない)であることを確認します。

if ((P1 & P2 & P3 & P4) == 0u && (P1 | P2 | P3 | P4) == 0xFF_FF_FF_FFu)

32枚の駒と32ビットを1対1対応させた、シンプルなビットマップ設計です。

圧縮② B1~B4(uint × 4)— 乗算パックで盤面を表現

盤面は32マス(4プレイヤー × 8手)あり、4つの uint(B1~B4)で管理しています。1つの uint に1プレイヤー分8マス(= 4ペア)を収める作りです。

各マスの内容は「受け駒スロット番号(-8~+7、マイナスは伏せ駒)」と「攻め駒スロット番号(0~8)」のペアです。ごいたのルールで補足すると、受け駒はそのターンで受けた駒のことです。全員が「なし」を宣言した場合、親は手駒から1枚を裏向きに出して受けとするのですが、この伏せ駒がマイナス値として記録されます。攻め駒は常に表向きなので非負値です。

エンコード式はこうなっています。

1バイト = (受け駒スロット + 8) × 9 + 攻め駒スロット

受け駒スロットは16通り(-8~+7)、攻め駒スロットは9通り(0~8)なので 16 × 9 = 144 で1バイト(256)に収まります。4ペア分を uint に詰めます。

10進数の「23 = 2×10 + 3」と同じ発想ですが、上位は0〜15の16通り、下位は0〜8の9通りと、桁ごとの基数が揃っていません。全桁が同じ基数で揃う純粋な進数表現ではなく、「16通りと9通りの組み合わせを1バイトに収める」というそのままの設計です。

デコード側はこうです。

// SyncBoard1内(SyncBoard2〜4も同じ構造)
uint b = B1;
for (int i = 0; i < 4; i++)
{
    int t = (int)(b & 0xFF);          // 下位1バイトを取り出す
    board[0][i << 1]     = t / 9 - 8; // 受け駒スロット(-8〜+7、マイナスは伏せ駒)
    board[0][i << 1 | 1] = t % 9;     // 攻め駒スロット(0〜8)
    b >>= 8;                           // 次の1バイトへ
}

i << 1 は i * 2、i << 1 | 1 は i * 2 + 1 と同じです。

初期値 boardInitValue = 1212696648u はゲーム開始前の盤面が空の状態を表しています。空マスは「受け: 0(伏せ駒なし), 攻め: 0」なので (0+8)×9+0 = 72 になります。

// (((8*9<<8)+8*9<<8)+8*9<<8)+8*9 = 1212696648u

各バイトが72(= 8×9)で初期化されているわけです。この定数を使って B1 == boardInitValue で盤面が初期状態かどうか判定できます。

圧縮③ Turn(byte)— 1バイトに4つの意味

Turn は1バイト(8ビット)ですが、複数の状態を同時に持っています。8個のビットを「2+2+2+2」の4グループに分けて、それぞれ別の情報として使う設計です。1枚のメモを4つの欄に区切るイメージですね。

// ビット構成
// [7][6][5][4][3][2][1][0]
//  ↑  ↑  ↑  ↑  └──┘ └──┘
//  |  |  |  |  parent  turn
//  |  |  └──┴── 次ゲーム準備フラグ(チーム1・2)
//  └──┴──────── ゲーム終了フラグ(チーム1・2)
  • bits 0-1: 現在のターンプレイヤー (0〜3)
  • bits 2-3: 攻め側(親)のプレイヤー (0〜3)
  • bits 4-5: 「次のゲーム準備中」フラグ(ビット4=チーム1、ビット5=チーム2)
  • bits 6-7: 「ゲーム終了」フラグ(ビット6=チーム1、ビット7=チーム2)

デコードはこうなっています。

// DelayChangeTurn内
int parent = Turn >> 2 & 0b00000011; // bits 2-3: 攻め側プレイヤー
int turn   = Turn & 0b00000011;      // bits 0-1: 現在のターン
if ((Turn & 0b11000000) != 0) FinishGame();       // bits 6-7 のどちらかが1 → ゲーム終了
if ((Turn & 0b00110000) != 0) PrepareNextGame();  // bits 4-5 のどちらかが1 → 次ゲームへ

エンコード例(ゲーム終了時):

// roundWinner = 上がったプレイヤーの番号(0〜3)
// 上がった人が次の親かつ次のターン先頭になるので bits 2-3 と bits 0-1 に同じ値
// チーム1勝利(チーム1=ビット6)
Turn = (byte)((score1 >= 150 ? 0b01010000 : 0b00010000) | roundWinner << 2 | roundWinner);
// チーム2勝利(チーム2=ビット7)
Turn = (byte)((score2 >= 150 ? 0b10100000 : 0b00100000) | roundWinner << 2 | roundWinner);

ターン・攻め側・準備・終了の4種類を1バイトに詰め込んでいます。

圧縮④ Score(int)— 2チームのスコアを1変数に

ごいたは150点を先取したチームが勝ちです。2チームのスコアを1変数に収めるなら16ビットで足りそうに見えますが、ごいたには「5し」という特殊ルールがあります。同チームの2人がそれぞれ「5し」を達成すると150点が一気に加算されます。140点保有時にこれが起きると 140 + 150 = 290点になるので、1チームのスコアは最大290点まで表現できる必要があります。

290を表すには9ビット必要です(29 = 512 > 290)。2チーム分で18ビット以上必要なので16ビット整数では足りません。そこで int(32ビット)を使います。

実際には290より大きくて2のべき乗にきりがよい 512(= 29)を基数にして詰め込んでいます。

// エンコード
Score = (score1 << 9) | score2;
// デコード
score1 = Score >> 9;
score2 = Score & 0x1FF; // 0x1FF = 511 = (1<<9)-1

score1 を上位9ビット、score2 を下位9ビットに詰め込む形です。たとえば score1=10・score2=30 なら (10 << 9) | 30 = 5150。取り出すときは 5150 >> 9 = 10、5150 & 0x1FF = 30 で戻ります。

圧縮⑤ ContinueBits(byte)— 2種類の準備フラグを1バイトに

ContinueBits は1バイトで「全員の続行OK」と「5し宣言」の2種類のフラグを管理しています。

  • 下位4ビット(bits 0-3): 各プレイヤーの「ゲーム続行OK」フラグ
  • 上位4ビット(bits 4-7): 各プレイヤーの「5し宣言」フラグ
// 全員続行OKか判定
if ((ContinueBits & 0b00001111) == 0b00001111) { /* 全員続行 → ゲーム開始 */ }

// プレイヤーiの5し宣言を参照
if ((ContinueBits >> i + 4 & 1) == 1) { /* プレイヤーiが5し宣言 */ }

圧縮⑥ NextGameBits(byte)— 次ゲームへの準備完了フラグ

NextGameBits の下位4ビット(bits 0-3)はそれぞれ4プレイヤーの「次のゲームへ進む」ボタン押下フラグです。

NextGameBits = (byte)(NextGameBits | (1 << player - 1)); // プレイヤーが押した
if ((NextGameBits & 0b00001111) == 0b1111) { /* 全員準備完了 */ }

FieldChangeCallback — 圧縮変数をリアクティブに展開するパターン

すべての同期変数に [FieldChangeCallback] 属性が付いています。

[UdonSynced, FieldChangeCallback(nameof(Turn))]
private byte _turn = 0;

public byte Turn
{
    get => _turn;
    set
    {
        _turn = value;
        ChangeTurn(); // ← ネットワーク同期時に自動で呼ばれる
    }
}

この属性を使うと、ネットワーク越しに _turn が更新されたとき、setter が自動で呼び出されます。setter の中で ChangeTurn() を呼んでいるので、圧縮された byte が届いた瞬間に展開してUI更新まで走ります。

[FieldChangeCallback] は「値が書き換えられたら指定した処理を自動呼び出し」する仕組みなので、受信側は自分で処理を呼び出す必要がなく、VRChatのSDKが面倒を見てくれます。

  • Turn が変わる → ChangeTurn() → ビット分解 → ターン表示・ゲーム状態遷移
  • B1 が変わる → SyncBoard1() → 乗算パックデコード → 盤面メッシュ更新
  • Score が変わる → ChangeScore() → Score >> 9 Score & 0x1FF → スコア表示更新
  • P1〜P4 が変わる → SyncPieces() → ビット走査 → 手駒ボタン更新

圧縮したまま送って、受信したら自動展開する。これが実装の中心となるパターンです。

まとめ

同期変数設計をまとめると次の通りです。

変数 型 圧縮方式 格納情報
P1〜P4 uint × 4 ビットマップ(1bit = 1駒) 4人の手駒配布(32枚)
B1〜B4 uint × 4 乗算パック(1byte = 受け+攻めペア) 盤面32マスの状態
Turn byte ビット分割(2+2+2+2bits) ターン・攻め側・準備・終了フラグ
Score int ビットシフトパック(×512) 2チームのスコア
ContinueBits byte ビット分割(4+4bits) 続行OK + 5し宣言フラグ
NextGameBits byte ビットフラグ(4bits) 次ゲーム準備完了フラグ

「同期変数をどこまで削れるか」という実験が起点とはいえ、複数卓を並べながらLateJoinerにも完全な状態を渡すという要件に対しては、圧縮は単なるこだわりではなく妥当な選択になっていました。FieldChangeCallback で「受信したら自動展開する」パターンも、今見るとなかなかきれいにまとまっていると思います。

ただ、「自分が面白くて書いたコード」と「人が遊ぶゲームのコード」は別物で、その区別を最初からつけられていなかったのは素直に反省しています。圧縮率より読みやすさを取るべき場所はあったし、実際そのツケは後から返ってきました。

それでも、このようなご縁がなければ、このコードは誰にも見られずにどこかに消えていたはずです。過剰な圧縮も、不具合の記憶も、全部ひっくるめてスコフィごいたというゲームになっています。もうちょっとちゃんと書けとは今も思いますが。

ICPC国内予選2020参加記

チームshop_oneで参加しました。

メンバーはshop_one(RenFukatsu)さんとshop_one(GunseiKPaseri)さんとshop_one(僕)です。

チーム名の由来はshop_oneさんです。

 

前日まで

shop_one(僕)は国内予選の直後に国際会議を控えており、この時期は忙しいだろうから出場を諦めようと思っていました。

しかし何か知らんけど忙しく無かったので(マジで数日後国際会議のはずなんだけどどうなってんのかよくわからん)、直前になって、他にチームを組んでなかった同大学のよく知らない水色コーダー(他学科の人&三学年下の人)を誘って急遽出ることにしました。

幣大学にはチームshop_one以外に、既にshop_oneさんが所属するチーム(highest青2、水1のチーム)があったので、予選突破は厳しいだろうというつもりでいました。

しかし、一回だけやったチーム戦(2017国内)でうっかり4完してしまい、ワンチャンあるかもしれないねという感じのチームになりました。

まぁ十中八九shop_oneさんのチームに負けるだろうなーっと思ってたので、shop_oneさんに「勝った方が焼肉奢りでどうですか」と持ち掛ける。

 これで他人の金で焼肉よ。

 

当日、コンテスト前

shop_one(GunseiKPaseri)さん、体調不良で欠席(リモートで参加はしてくれる)、ウケる。

リハーサルで全完。

後で聞いたんですけど、4問目550点らしいですね、よくACしたなshop_one(僕)。

ここで運を使い切った気がする。

 

コンテスト中

Aをshop_one(RenFukatsu)さん、Bをshop_one(GunseiKPaseri)さんに任せてshop_one(僕)はCを見る。

よくわからん、3乗根取って適当に探索したら通らんか~?って言ってたらAを通したshop_one(RenFukatsu)さんに「絶対に無理です。」と強めに否定される。∩(´;ヮ;`)∩ンヒィ~~~~~~~~~~~~~~~~~~~~

shop_one(RenFukatsu)さんがまず素因数分解するというヒントをくれたので、雑にw,d,hにそれぞれの素数を何個ずつ割り当てるかをdfsしたら通る。これ計算量大丈夫なのか……。

3完時点でshop_oneさんのチームに勝っていたのでワンチャンあんじゃね~?と思っていた。

D、わからんって言ってたらshop_oneさんのチームが通した。ワンチャン無かった。

E、わからん

F、コンテスト終わりかけに問題概要を聞いて(自分で問題読んでも何書いてあるかよくわかんなかった)、何か後ろからUnionFindか部分永続UnionFindでも使うんじゃないんすか~?と言いながら雑に書いたらWA、はい。

 

コンテスト後

 やったね。

 

感想

直前に組んだ寄せ集めのチームにしては健闘したと思います。

アジアに行けなかったのは残念だけど、まぁちょっと相手が悪かったですね。

shop_oneさんを闇討ちしなかったshop_one(僕)の落ち度です。

JAG夏合宿2019参加記

JAG夏合宿2019に参加しました。

コンテストで僕はほぼ置物だったので、コンテストの話はほとんどありません(何のための合宿ですか)。

 

前日まで

ICPCのチームIntegrated Qualityからは僕とTi11192916さんの2人が参加することになった。

Ti11192916さんの「しーあるは他の世界を知るべき(意訳)。」の意向で、合宿中に行われるコンテストは、僕とTi11192916さん(ともう一人誰か)が同じチームになる日と、僕とTi11192916さんが別のチームになる日があると良いかもねとなっていた。

notさんがTwitterで作ってくれていた参加者のリストを見ると、僕がフォローしてる人が1/4くらいしかいなくて(冷静に考えると十分多くね)、チームとか部屋割りとかの不安でお腹が痛くなってしまう。

 

day1

11:00-12:00集合のところ、11:00ぴったりに会場に着く。参加者1番乗り。

ちょっとずつ他の人も来るが、知ってる人がいない(人々はTwitterのアイコンと同じ見た目をしてないので知ってる人かどうかわからない)のでどんどんツラい気持ちになる。

運営が「ハンドルネームと本名が並んでるの違和感あるけどTi11192916だけ違和感ねぇw」とか言っててウケる。みたいなツイートを真顔でする僕。ツラそう。

配られたしおりに部屋割りが書いてあって、TABさんとしゃっふりゅーさんとsimさんが僕と同部屋であることがわかる。

TABさんは数少ない喋ったことある競プロer(あとイケメン)だったので助かる~~~~ってなった。

集合時間が過ぎ、ガイダンスが始まり、一人ひとり自己紹介する流れに。

コミュニケーション能力に難があるので、人前で自己紹介するの無理無理無理無理無理無理無理と思ってたが、みんな所属とハンドルネームを言う程度で終わってたので命びろいした。

とはいえちょっとは印象に残らんとなと思ったので、わざとチーム名を噛むことにした。

「○○大学 イン…インテ…インテグ……Integrated Qualityのしーあるです。よろしくお願いします。」

ちょっと笑い声が聞こえたから勝ち。決して滑ってなどない。

その後コンテストが始まるまでの間にこたつがめさんが来てくれて話しかけてくれる。これマジありがたかった。

コンテスト

僕とTi11192916さんとぴーよさんで組むことに。

ぴーよさんに「つい先日までじゅっぴーさんとインターンしてた人ですよね!?!?」などと絡みたかったが、すっかり弱気になってた僕はあんまり喋れなかった。悲しいね。

とりあえずAは僕に任せてもらえることになったので頑張る。

実装に手間取ってる間に、他の複数の問題の方針が立ってて、僕が実装終わるの待ちになってそうな雰囲気を感じて申し訳ねぇ~~~ってなる。

Aを通しました。後は問題をいくつか読んでその概要を伝えるをしました。以上です(おいおい)。

その後

夕食食べたりこたつがめさんと一緒に風呂入ったりちょっとだけボドゲやったりして終わり。

 

 day2

朝食激混みで萎え。

コンテスト

僕とわくさんとmo3tthiで組むことに。

僕はIを実装できないを担当しました。本当にごめんなさい。以上です(ツラい)。

 

懇親会

19時から懇親会があるので、それまで部屋で仮眠を取ることに。部屋にはしゃっふりゅーさんもいた。

~19時半頃~

しゃっふりゅーさん「やべぇ!懇親会始まってる!!」

しーある「えぇっ!?!?」

懇親会絶起勢です。なんやねん。

遅刻して行ったら、食べ物は無残な姿のカツサンドくんとマフィンしかなかった。

もうある程度人の集まりが形成されていて、後から入ってくのしんどいなぁってなってしまう。

とはいえ会場を回って知ってる人とある程度は話せたので頑張りました。

beetさんにABC141の問題教えてくれと言ったら教えてくれた。

beetさん「Aがtoptreeで、Bが多倍長なんだけどPythonだと重くて通せなくて、Cがコーナーケースが100個ある幾何で、Dが証明してないんだけどできそうだから出した。」

 ????????

その後

こたつがめさんがABC141でコードゴルフすると言うので観戦することに。

開始とともにA問題を開き、問題ページ下部のコードを提出するところに直接コードをべた書きし始めてそのまま提出。一発AC。ヤバ。

以降もほとんどエディタを使うことなくコードを縮めていく。激ヤバ。

Eまでは順調にACしてたんだけど、Fで一度こたつがめさんの手が止まる。順位表見ても結構ヤバそう。

こたつがめさんの隣でMisterさんがうししゃんのgithub.ioを見てたので、ぶろっくくずしを開いてこたつがめさんの邪魔をしようとしたが、リンクが貼ってなかった。

誰かリンクくれ~ってツイートしたらbeetさんが送ってくれる。流石ぶろっくくずしガチ勢(ぶろっくくずしが何かわからない人はbeetさんに聞いてね(丸投げ))。

 僕はこたつがめさんのABCの観戦をした後に風呂に入ろうと思ってたが、ABCが終わった後に風呂に行くと、ABCに参加してた人たちで風呂が混んでしまうので、こたつがめさんにさっさとFを通して欲しい気持ちになる。

しーある「早くF通して。」

こたつがめさん「キレてしまった。」

ごめん。

それでも直後にFの実装が終わって、提出をしたものの、ジャッジが遅い。

こたつがめさん「通れ通れ通れ」

しーある「ありがとうありがとうありがとう」

こたつがめさん「ありがとうありがとうありがとう」

通る。きれいな言葉をかけたジャッジはACを返すことが証明される。

 比較的空いてる時間に風呂に入れました。やったね。

 

day3

なんかあったっけ

コンテスト

僕とTi11192916さんとclaw88さんと組むことに。

とりあえずAは僕に任せてもらえることになったので頑張る。

一応通せた。良かった。以上です(あの)。

 その後

解散。駅でこたつがめさんに会う。別の電車に乗るということで、また会いましょうねと言って別れる。2駅先で再び会う。あれ?

帰宅。終わり。

 

感想

参加記でコンテストの感想が書ける程度には活躍できるといいねという気持ちになった(この参加記を書いてる間虚しい気持ちだったので)。

あとこたつがめさんのことばっか書いてて全然人と交流してなくないかとなってしまった。

競プロ力も社会性も身につけたいですね。以上です。

ICPC国内予選2019参加記

チームIntegrated Qualityで参加しました。
メンバーはTi11192916さんとmintくんと僕です。
チーム名の由来はTi11192916さんが代表をやっていたサークル名です。

前日まで
Dを通さなければ予選突破は厳しいということで、Ti11192916さんがD、僕とmintくんがA~Cを担当することに。

何度かチーム練をした結果、
1.Aは僕がやる。その間にmintくんにBを考えてもらう。
2.僕がAを通した頃にはmintくんはBの考察が終わっていて、そのまま実装に入る。その間に僕がCを考える。
3.Bを通したmintくんと一緒にCを詰める。どっちが実装するかはその時次第。
という流れができ、そこそこの速さで3完できるように。

僕としてはとりあえず3完まですれば、後はTi11192916さんがどうにかしてくれるので、あんまり気負わずに行こうと思っていた。
僕はメンタル雑魚芸人なので気負うと死んでしまうので。

当日、コンテスト前
昼夜逆転マン、眠れず。
コンテスト前に明らかに自分がド緊張しているのがわかる。
Ti11192916さんに「コンテスト前は瞑想をすると良いらしいよ。」と言われ、目を閉じる。眠いので寝た。

コンテスト中
とりあえず僕がAをやる……んだけどメンタル雑魚芸人なので、緊張でキーボードを打つ手がドッタンバッタン大騒ぎする。
「やべーやべー手が超震えてるあわあわわわわわわわわ」ってなってると、Ti11192916さんに背中を撫でられる。落ち着いてAを通す。子どもかな?

練習の流れ通り、僕がAを通し終えた後、既にmintくんがBの考察を終えていて、速攻で通す。偉い、とても偉い。

Cはちょっと困ってしまった。
とりあえず分銅で作れる重さを全列挙するまではすぐに考えついたので、それからどうするかをmintくんと話し合う。
方針が立ったので僕が実装して提出するもWA、あわわわわわわわ。
うーんうーんとしていると、分銅で作れる重さの内、0が抜けていることに気づく。
僕とmintくんで「これで駄目だったら本格的にわからんね〜……」って言いながら出すと通る。
僕とmintくんの間で「あー、良かった。」という空気が流れる、まだ終わってないよ。

後はTi11192916さんがDを通すだけ……と思っていたら、苦戦していて、順位表を見てもDがヤバそうだという雰囲気が出る。
僕とmintくんもDの考察をするも、何も見えず。
しばらくして、僕とmintくんは割と諦めムード。特に僕は、Cで1WAしなければ3完で予選突破できたかもしれないという自責の念に駆られていた。
すると突然、Ti11192916さんが自信満々に「解けました!」と言う。Ti11192916さんによると、そのときの僕のリアクションは見物だったらしい。
その時点で残り時間が30分ちょっとあり、Ti11192916さんが実装を始め、僕とmintくんは安心して駄弁り始める。が、段々雲行きが怪しそうな雰囲気が出てくる。
終盤、僕とmintくんは黙ってただただ祈るだけになる。祈りは届かなかった。

コンテスト後
Ti11192916さんが順位表を見ながら、「ギリギリ通ってそう」と言う。マジ?
順位表をちゃんと整理すると、ギリギリ通ってることがわかる。マジ?
結果として、僕とmintくんでTi11192916さんをアジアに連れて行くという形に。


あれ?

感想
僕としては、僕のせいで予選通過できないという事態だけは避けたいと思っていたので、形はどうあれ通過できてホッとしている。
メンタル雑魚芸人の僕がCを(1WA出したものの)予選通過ライン内で通せたのは、Ti11192916さんがいるから大丈夫だと思いながらやっていたおかげだったり、そもそもチーム練の成果がかなり発揮されていて、その機会を設置してくれたのもTi11192916さんなので、実質Ti11192916さんにアジアに連れて行ってもらったようなもんだと思ってます。
あとmintくんが本番なんかめっちゃ落ち着いていたのも結構大きかった気がします。その落ち着き僕に分けてくれ。
アジアも頑張ります。

全国統一プログラミング王決定戦懇親会参加記

日経新聞主催の全国統一プログラミング王決定戦の予選で、運良くそこそこの成績を収めることができ、決勝が行われる会場に行く権利を得た(決勝に出場する権利ではない)ので行った。

前日
懇親会に名札は必要だろうと思っていたが、作ってはいなかったので、「名札無いや」とツイートしたところ、先輩のAmanukoさんと納豆大好き侍さんが僕の名札を作ってくれることに。

朝
前日まで、夜に起きて昼寝る生活をしていて、家にいると寝落ちしてしまいそうだったので、かなり早めに会場のある東京ドームホテル周辺に。
Twitterを見ると、ゲーセンで音ゲーしてる競プロerがいるっぽいので、僕もjubeatやってれば時間潰せるだろうと思って行ってみたら、SEGAのゲーセンだったので、SEGA製の音ゲー(チュウニズム、オンゲキ、maimai)と太鼓の達人しかなく、詰む。
チュウニズムをやってる集団がいたので、競プロerであることを確認(こたつがめさんとWA_TLEさんと物理好きさんだった)、声をかける勇気が出ず立ち去る。

昼
やることが無いのでとりあえず飯を食う。
f:id:c_r_5:20190218113623j:plain
ペッパーランチ、美味い。
それでも入場受付時間までまだ3時間以上もあり、とりあえず座れる場所欲しさにカラオケ店を探す。
JOYSOUND:建物の水道管工事で休み
BIG ECHO:なんか知らんけど休み
何かよくわからんとこ:何かよくわからんけど休み
キレた。
その後なんとか開いてるカラオケ店を見つけ、ようやく腰を落ち着ける。
しばらくして、Amanukoさんと納豆大好き侍さんがカラオケに合流。
先輩方に作らせてしまった名札をいただく。
f:id:c_r_5:20190218114911j:plain
煽り文が最悪すぎる。
入った途端二人共パソコンを開く。競プロerっぽい(?)
ちょうど決勝の問題のpdfが公開されたので全員問題を解き始める、実質オンサイト。

会場
競プロerとして最高の時間潰しを終え、会場へ。
受付のために並んでいると、chokudaiさんがAtCoderのステッカーを配りに来た。初の生chokudaiさんにテンションが上がる。
受付時に、実は用意されていた名札が配られた。
AtCoderのIDと本名が書かれていたので、プライバシー保護の観点から(?)先輩がたに作っていただいた名札で本名を隠すことに。
もう文字打つの疲れたから後は適当に書く。

トークショー
何か良い感じの話をしていた。とりあえず前で堂々と喋れるだけでもうカッコいいと思った。

表彰式
決勝で20位以内だった人が前に呼ばれる。双子が連順位だったので双子って双子なんだなぁって思った。

エキシビション
超ヤバい(語彙力)
コードゴルフ勢はもう考えたりしてそうと思いながら見てた。

懇親会
500人は多い。特定の人を探すのは無理そうと思ってたら、わりと早めに%20さんに僕を見つけていただける。探索のプロ。
ゴルフ勢が集まってると聞いたので着いていく。
こたつがめさんとうらさんに会う。こたつがめさんはスマホでゴルフをしていた。
しばらくゴルフの話をして、またウロウロし始める。
飯はなんかめっちゃ並んでるから諦め、とりあえず名札を見て知っていたら声をかけるムーブに。
てぃーいけさん:なんか胸にデカい船見結衣の缶バッチ着けてる。ステッカーを貰う。
てんぷらさん:熊じゃなかった。ほむさんどこですか?って聞いたらすぐ近くにいた。流石
ほむさん:ほむ〜
シンヤカトーさん:本名がシンヤカトーじゃない。
おるふぇさん:おるふぇがおるふぇ!!!!以外に何も言ってない気がする。
Joeさん:IDを見ても誰かわかんなかったが「卒論の人です」って言われてJoeさんだとわかった。なんでそれでわかるのか
ふーらくたるさん:コミュ障をした。
鍵月さん:かわいい
はりぼてさん:てぃーいけさんがよくふぁぼる人だ!って言われた。

Twitterでよく見る人たちと会えて良かった。
懇親会で納豆大好き侍さんが「CODE THANKS FESTIVALで優勝した人ですよね?」と話しかけられたり、Amanukoさんが「競技Longest Streakerですよね?」とか話しかけられたりしてて、そういう実績で覚えられてるの良いなぁって思った。僕も“力”が欲しい。
おわり。

AtCoderで水色になった。

AtCoderのレートが水色になりました。やったぜ。

使用言語はPythonです。
元々は学校で習ったことのあるJavaで始めたのですが、何か新しい言語を覚えたいと思ったときに、先輩に「Pythonがいいんじゃない(知らんけど)」と言われたのがきっかけでPythonを使うようになりました。

AtCoderを始めてから、1日1ACをするようにしていたのですが、PythonはJavaと比べて短く書けるので、だんだん横着してスマホでABC-A程度の簡単な問題を提出するようになりました。無駄。

そして、スマホで楽に書くためにできるだけコードを短くしようとした結果、コードを短くする努力をし始めるようになりました。まぁPythonは比較的短く書けるもののRubyとかPerlとかと比べるとそうでもないみたいなところがあるので、あまりショーテストコードは取れませんでしたが、それでも現時点で17番目にショーテストコードを持ってる人にはなれました。(微妙)
f:id:c_r_5:20190129073440j:plain
(上位の人がバケモンすぎる)

そんなことをやってたので実はAtCoderを始めたときからほとんどパフォが変わってないです。
f:id:c_r_5:20190129072450j:plain

青になりたいなぁ…。

追記


怖いこと言わないで…