Diary of a Perpetual Student

Perpetual Student: A person who remains at university far beyond the normal period

【令和最新版】俺的ゲーム配信環境 2022

コロナ禍の過ごし方として、毎晩、大学の頃のサークルの同級生と Discord に集まって通話をしています。思い思いに好きなゲームをしたり、仲間を募って同じゲームを遊んだりする時間です。バーチャル部室と言ったところでしょうか。

このとき、Discord の画面共有機能を使って各自のゲーム画面を配信しています。今回は、自分がゲームを配信している環境を紹介します。

要件

  • Nintendo Switch の ゲーム画面を、Discord の画面共有にてゲーム音声付きで配信できる
  • 自分がプレイするためのゲーム画面と音声は、余計な遅延や品質低下なく視聴できる
  • PC の音とゲームの音を同じヘッドホンで視聴できる

結論

ハードウェアの構成としては以下のようになりました。

また、使用機材は以下の通りです:

OBS Studio という配信用ソフトウェアで、ソースに映像キャプチャデバイスとして上記キャプチャーボードを指定し、「ウィンドウプロジェクター(プレビュー)」のウィンドウを Discord の画面共有の対象にしています。

右のウィンドウを Discord で共有しています

解説と補足

基本構成

説明のために、まずはシンプルな構成からはじめましょう。ゲームの映像と音声をOBS上で視聴するなら、以下のような構成で実現できるでしょう。

この構成に関する説明はこちらではしないので、他の記事を参照してください。

遅延なくゲームの映像を視聴するために

OBS の画面を見ながらゲームをプレイしても良いのですが、やはり遅延とフレームレートの低下が気になります。そこで、キャプチャーボードのパススルーアウトをモニタに繋ぐことにしました。

これによって映像は問題なくなったのですが、問題は音声です。OBS の音声モニター経由でゲーム音で聞くと、目に入る映像と耳に入る音声が、キャプチャーボードにより発生する遅延分だけずれてしまいます。

しかし、パススルーの音声を聞くにはパススルー用モニタの音声出力を利用するほかありません。PCからの音声はヘッドセットで聴き、ゲーム音はモニタから出る音を直接聴くということになってしまい、都合が悪いです。

遅延なくゲームの音声を視聴するために

PCの音声とパススルーのゲーム音声をなんとか同じヘッドセットで聴く方法を考えました。

もちろん、PCの音声出力とゲームの音声出力をアナログミキサーで mix すれば良いのですが、新たに機器を買い足すのはスペース的に厳しいです。せっかくミキサータイプのオーディオインターフェイスを使っているので、パススルーモニタの音声出力をオーディオインターフェイスアサインすることにしました。

このときに注意するのが STREAM OUT の設定です。一般的には「INPUT MIX」に設定していると思うのですが、これを「DRY CH1-2G」モードに変更します。 こうしないと、ゲーム音がミックスされてマイク入力として載ってしまいます。このモードでは、ch.1 と ch.2G の入力は PC に送られますが、 ch.2-3 の入力は PC に送られません。AG03 のモニターアウトに繋いでいるヘッドセットからは依然として ch.2-3 に入力した音声、すなわちパススルー経由のゲーム音を聴くことができます。

副作用として、以下の問題が発生します:

  • マイク入力音声の信号が L だけになる
    • 「DRY CH1-2G」モードでは、L に、ch.1、R に ch.2G がアサインされるため *2
    • Discord などの通話アプリではステレオ入力がモノラルにダウンミックスされるので、本要件では問題にならないはず
    • NVIDIA Broadcast(AIで環境音を消してくれるソフトウェア)を挟むと L だけの信号が LR の信号になるので活用すると良い
  • ch.1 の音量調整のフェーダーや MUTE スイッチが、PC に送られる信号には効かなくなる
    • AG03 の仕様なのでどうしようもない、妥協して受け入れる

業務で利用するようなミキサーであれば AUX チャンネルを使えば一発なのですが、民生用機器で頑張るのは思いのほか大変ですね。

Discord の画面共有でゲーム音を載せる

※ この話は Discord の画面共有の話なので、YouTube などで 配信したい方は SKIP してください

ここまで来たら要件の達成までもう少しです。OBS のウィンドウプロジェクターを Discord の画面共有します。

このときに問題になるのがやはり音声です。OBS の音声モニターでキャプチャーボードの入力音声を聴いていると、パススルー経由の音声とダブルで聴くことになってしまいます。そのため、愚直にはオーディオの詳細設定から「モニターオフ」にすることになるでしょう。こうすると、ゲーム音はパススルー経由のものだけを聴くことができます。

しかし、これでは Discord の画面共有にゲーム音が載りません。なぜなら、Discord の画面共有は、そのアプリケーションが鳴らしている音を配信するからです。モニターオフにすると OBS が音を出していないことになるので、結果として配信が無音になってしまいます。

これを解決する方法は、OBS のモニター出力を別のデバイスにすることです。設定(オーディオの詳細設定ではなく、グローバルな方)の「音声」を開き、モニタリングデバイスを使用していないデバイスに変更します。その上で、先程のモニター出力をありにします。

このようにすることで、Discord の画面共有にキャプチャーボード由来の音声を載せることと、自分がキャプチャーボード由来の音声を聴かないことを両立することができます。

まとめ

いかがでしたか?

音響の仕事の経験があるのでこの程度はなんとかなるのですが、一般には難しいだろうなと思っています。(このエントリもかなり読みづらいと思います。すみません。)

世の配信者は、満足する配信環境のために裏で頑張って知識を習得しているのでしょうね。それとも、こういった環境の準備をコンサルしてくれる企業があるのでしょうか。

配信環境の準備に関して、知り合いであれば相談を受け付けますので、気軽にご相談ください。

*1:旧式でも問題ないはずです

*2:FAQ: https://faq.yamaha.com/jp/s/article/J0007132

Jest で落ちたアサーションを GitHub のアノテーションに出す

最近 GitHub Actions を弄くり倒すことにハマっていて、 GitHub の Checks API を利用して annotation を出すおもてなしをすることだけが生きがいだと思って生活していました。

そんな中、JavaScript (TypeScript) のコードのテストでよく使われている Jest で、どの assertion が落ちているかを annotation で分かりやすく表示したいと思っていました。自作で頑張ろうかな~と思って調べていると、 Jest 28.0.0 (2022年4月末ごろリリース)から Github Actions で annotation を出す reporter 機能が組み込まれていたという事実を知りました。

jestjs.io

この便利機能が思ったより世の中で使われていない感じがしたので紹介します。

サンプル

こちらをどうぞ:

github.com

CI で動かしているテストが落ちたとき、アサーションを満たしていないところを以下のように表示してくれます。

fail test sample by Arthur1 · Pull Request #1 · Arthur1/jest-github-actions-report-sample · GitHub

やり方

本当に簡単です。

  1. jest を v28.0.0 以上にアップデートする
  2. jest.config.js に 以下のような編集を加える
  3. GitHub Actions 上で jest を実行する(オプションなどは不要)
/** @type {import('jest').Config} */
const config = {
  testEnvironment: "node",
  roots: ["<rootDir>/src"],
  testMatch: [
    "**/__tests__/**/*.+(ts|tsx|js)",
    "**/?(*.)+(spec|test).+(ts|tsx|js)",
  ],
  transform: {
    "^.+\\.(ts|tsx)$": "ts-jest",
  },
  reporters: ["default", "github-actions"], // <- この行を足す
};

module.exports = config;

ドキュメントのリンクも残しておきます。

jestjs.io

感想

最近の GitHub は PR の差分外についた annotation も表示する(ベータ版機能)ので、落ちているテストを放置する運用をしていると annotation だらけになって厳しいかもしれません。まあそんな形骸化したテストを CI で動かす必要はないでしょうし、これが問題になることはないでしょう。

(というより、これができないと、テストコードを編集していない時にが落ちるとアノテーションが出なくなってしまう。ベータ版じゃなくなると良いですね。)

(そしてガッツリ TDD している場合はどうなるんだろう。)

あと assertion と annotation の区別は寝起きの IQ1 の頭脳にはつらいですね。

(2022/9/9 追記) テストコードに差分がないのにテストが落ちた時にどう表示されるか

PR の差分外のコードに annotation を出す機能もせっかくなので紹介しておきたく、例を用意しました。

fail test sample (without test code diff) by Arthur1 · Pull Request #2 · Arthur1/jest-github-actions-report-sample · GitHub

個人開発をクラウドシフトすることにした〜Github Actions 用の IAM ロールを作る〜

個人開発用に VPS を借り始めてもう7年近くになる。これまでにボードゲーム関連で作ったサービスは基本的に VPS 1台に全部載せて運用している。コンテナすら利用していないので、PHPMySQL のバージョンを上げるだけで一苦労だ。

日々の運用が toil になってしまって放置気味なこと、得た知見を業務にそのまま生かすのが難しいことから、今後作るものは Amazon Web ServicesGoogle Cloud などのクラウドサービス上に構築することに決めた。社会人になって資金面の余裕が生まれたという理由もある。

同人活動に近いということもあり収益化はほぼできておらず、普段から赤字を垂れ流しているわけだが、やはり自ら手を動かして得た知見に勝るものはないだろう。これは勉強代ということにしておく。

そして、VPSAmazon EC2 に変えても、勉強としては何の意味もないだろう、ということで、マネージドなサービスにちゃんと寄せていくことにする。とはいえ、個人開発レベルで Amazon RDS や Amazon ElastiCache を使うのはコストに見合うメリットが得られないのは明らかだ。この辺り、どう折り合いをつけていくかは今後の課題とする。

クラウドシフトプロジェクトの手始めに、Github Actions 上で動かす Visual Regression Test の結果を Amazon S3 にアップロードする仕組みを作る作業に取り掛かった。完成品は以下のような感じで、Github の Checks API を利用して良い感じに表示できるようにしてみた。

まずは、Github Actions のコンテナから画像を S3 にアップロードするのに必要な IAM ロールを作ることにした。慣れない Terraform を黙々と書いていった。

terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 4.0"
    }
  }
}

# Configure the AWS Provider
provider "aws" {
  region  = "ap-northeast-1"
  profile = "AgricolaDevJP-admin"
}

// ref: https://zenn.dev/yukin01/articles/github-actions-oidc-provider-terraform
data "http" "github_oidc_configuration" {
  url = "https://token.actions.githubusercontent.com/.well-known/openid-configuration"
}

data "tls_certificate" "github_oidc" {
  url = jsondecode(data.http.github_oidc_configuration.response_body).jwks_uri
}

resource "aws_iam_openid_connect_provider" "github_oidc" {
  url             = "https://token.actions.githubusercontent.com"
  client_id_list  = ["sts.amazonaws.com"]
  thumbprint_list = data.tls_certificate.github_oidc.certificates[*].sha1_fingerprint
}

resource "aws_s3_bucket" "agricola_card_image_generator_snapshot_bucket" {
  bucket        = "agricola-card-image-generator-snapshot"
  force_destroy = true
}

resource "aws_s3_bucket_public_access_block" "agricola_card_image_generator_snapshot_bucket" {
  bucket                  = aws_s3_bucket.agricola_card_image_generator_snapshot_bucket.id
  block_public_acls       = false
  block_public_policy     = false
  ignore_public_acls      = false
  restrict_public_buckets = false
}

resource "aws_s3_bucket_versioning" "agricola_card_image_generator_snapshot_bucket" {
  bucket = aws_s3_bucket.agricola_card_image_generator_snapshot_bucket.id
  versioning_configuration {
    status = "Disabled"
  }
}

resource "aws_s3_bucket_policy" "agricola_card_image_generator_snapshot_bucket" {
  bucket = aws_s3_bucket.agricola_card_image_generator_snapshot_bucket.id
  policy = data.aws_iam_policy_document.agricola_card_image_generator_snapshot_bucket_public_access_policy.json
}

data "aws_iam_policy_document" "agricola_card_image_generator_snapshot_bucket_public_access_policy" {
  statement {
    principals {
      type        = "*"
      identifiers = ["*"]
    }
    actions = [
      "s3:GetObject"
    ]
    resources = [
      "${aws_s3_bucket.agricola_card_image_generator_snapshot_bucket.arn}/*"
    ]
  }
}

resource "aws_iam_role" "agricola_card_image_generator_snapshot_github_actions_role" {
  name               = "agricola-card-image-generator-github-actions-role"
  assume_role_policy = data.aws_iam_policy_document.agricola_card_image_generator_snapshot_github_actions_role_policy.json
}

data "aws_iam_policy_document" "agricola_card_image_generator_snapshot_github_actions_role_policy" {
  statement {
    effect = "Allow"
    principals {
      type        = "Federated"
      identifiers = [aws_iam_openid_connect_provider.github_oidc.arn]
    }
    actions = [
      "sts:AssumeRoleWithWebIdentity"
    ]
    condition {
      test     = "StringLike"
      variable = "token.actions.githubusercontent.com:sub"
      values = [
        "repo:AgricolaDevJP/agricola-card-image-generator:*"
      ]
    }
  }
}

resource "aws_iam_role_policy" "agricola_card_image_generator_snapshot_bucket_put_policy" {
  name   = "agricola-card-image-generator-snapshot-bucket-put-policy"
  role   = aws_iam_role.agricola_card_image_generator_snapshot_github_actions_role.id
  policy = data.aws_iam_policy_document.agricola_card_image_generator_snapshot_bucket_put_policy.json
}

data "aws_iam_policy_document" "agricola_card_image_generator_snapshot_bucket_put_policy" {
  statement {
    effect = "Allow"
    actions = [
      "s3:PutObject"
    ]
    resources = [
      "${aws_s3_bucket.agricola_card_image_generator_snapshot_bucket.arn}/*"
    ]
  }
}

ググっても credential を環境変数に埋め込む方法ばかり出てくるが、セキュリティ上の問題から定期的なローテートが必要で面倒だ。そこで、GitHubAWS アカウントから、こちら側が用意したロールに Assume Role するようにしてみた。

zenn.dev

上記の記事を参考に、thumbprint を直書きしないようにしたのもポイントである。

AWS を扱う上では避けたくても避けられない IAM と仲良くなるための第一歩としてちょうど良いスタートになったと思う。

周囲の iPhone ユーザに迷惑をかけまくっていた話

僕は Apple 信者ではないのに、どういうわけか Apple 製品に囲まれて暮らしている。 M1max の Macbook Pro に Magic Trackpad と Magic Keyboard を繋いで仕事をし、 iPad Air で動画を見ている。 そして、財布には AirTag をつけている。

この AirTag は発売日に即ポチしたのだが、 iPhone を常に持ち歩いている人にしかあまり恩恵がないということを知らずに購入してしまった。 AirTag はインターネットに直接繋がっていなくても、日本に数多と存在する iPhone 総出の力で探し物の場所を特定することができるという優れ物である。 しかし、その仕組み上、自分が iPhone を持っていないと持ち物を所有者自身が持っているという判定ができないのだ。

そして、僕は iPhone を持っていない。 おサイフケータイを昔から使ってきた関係でスマートフォンAndroid 製品を選んでおり、今更変える気もないからだ。

そのため、一応手元の iPad と紐付けはしておいたものの、大きな iPad を普段持ち運ぶことはないため、たまに音が鳴る*1高級ストラップとして運用していた。

使用し始めて15か月が経つ頃には、 iPad に「AirTag の電池がバッテリー残量が少なくなっています」という通知が来ていた。しかし、忘れ物タグとしては使っていないので通知を放置していた。これが事件の引き金となる。

一緒にドライブをしていた友人が、「変な通知が何度も来るんだが」と言ってきた。iPhone の画面を覗き込むと、画面下部に見覚えのあるこんな通知が出ている。

そうだ、これは AirTag を初期設定するときの画面である。電池が切れかかっていたからか、AirTag が初期化されてしまったようだ。 そして、残っていたわずかな電力を使って、周りの iPhone ユーザに母をたずねて三千里していたということらしい。

時代遅れの Lightning 端子を備えた iPhone アンチの僕としてはいい気味だったかもしれない。 しかし、これで第三者に AirTag を登録されたら、自分の居場所が追跡されてしまう可能性もあって笑い事ではなくなってしまう。

「AirTag くんごめん……すぐ楽にしてやるからな」

そう言って僕は AirTag の電池を抜いた。

今日の教訓

AirTag を高級ストラップとして運用している人はボタン電池を外しましょう。

*1:AirTag は、持ち主の元をしばらく離れていると、防犯のために音が鳴る

フロクロさんを語る:マルチタレントとオープンネス

今日はとあるアーティストについての話をします。先日のエントリで1楽曲だけ取り上げた、フロクロ(Frog96)さんです。

音楽制作者としての自分、ソフトウェアエンジニアとしての自分、両方の目線で強く惹かれるところがあったので、ぜひ紹介させてください。

楽曲紹介

映像も素晴らしくて、楽曲というより作品と言った方が良いのかもしれない……

ロスト・デリュージョン

www.youtube.com

ディスプレイスメントマップを利用した MV が見ていて気持ちが良いというか中毒性があります。メディアアート

空を満たして

www.youtube.com

サビの、かすかに聞こえるハット以外はほぼフィルインだけで構成されたドラムフレーズ(たぶん名称があるんだろうけど分からない)をはじめとして、全体的に緊張感を覚えるサウンドです。 ラスサビ直後の映像演出が最高にゾクゾクして好きです。

黒塗り世界宛て書簡

www.youtube.com

リズミカルにビープ音が鳴っているのが印象的ですが、調の主音とビープ音が同じ高さなので、調和していて鬱陶しくない感じがあります。そして何より歌詞とビープ音に隠されたギミックがすごい。

input と output がすごい

フロクロさんはScrapbox に制作物のメイキングや有益情報などをまとめています。これが本当に興味深いのです。

scrapbox.io

フロクロさんの作品の特徴として、作詞、作曲、MVの制作すべて一人で担当されているという点があります。それぞれどういった手法で作ったのかが事細かに公開されていて、勉強になります。

例えば、先ほど紹介した「ロスト・デリュージョン」の音楽メイキング記事では、インスピレーションを受けた楽曲やコード進行、使用しているVSTプラグインまで動画つきで丁寧に説明されています。自分が感覚的に好きだと思った作品が、どのよう過程で、どんな要素を用いて構成されたのかを知ることができるのは面白いというほかありません。

scrapbox.io

これら様々な記事を読んで、 output と input の質は互いに関係があるということを改めて実感しました。音楽にしても映像にしてもその他にしても、思考が圧倒的な知識に裏付けられているように感じられます。

僕は仕事として、コードやその設計思想を Pull Request という形で表現したり、エンジニアリングに関する学びを他者に共有したりしています。そして、それらアウトプットの質・量が自分の働きぶりを評価する指標の一つとなっています。

分野は違えど、ソフトウェアエンジニアとして生きていく上でフロクロさんの姿勢が手本になると思いました。僕も、自分の思考をもっと言語化していきたいし、良い思考をするために自分の知識の幅を広げていきたいです。そして、同じような人が同僚にもいるなあ。

まとめ

色々小難しいことを語りましたが、言いたいことはこれだけ。良いアーティストだから聞いてくれ!!!!!

www.youtube.com

アグリコラ:泥沼からの出発 リバイズドエディション の誤訳指摘

遅くなってしまいましたが、 アグリコラ:泥沼からの出発 リバイズドエディション(日本語版)を開封しましたので、誤訳の指摘や補足情報の提示をします。ゆっくりやればよいと思っていたのですが、お盆休みで多くのプレイヤーが泥沼で遊んでいそうなのを観測したので取り組みました。

hobbyjapan.games

id:arthur-1 は2018年夏に海外で先行販売されたドイツ語プレプリント版を所有しており、そちらを訳したものとの比較になります。現時点ではエラッタが入っており、日本語版に反映されたカードもあるかもしれませんが把握はしていません。

なお、それとは関係なくこちらの情報も間違っているかも知れないので、間違いあれば指摘してください。

更新情報

  • 2022-08-22 13:00 [M068] 教会のアイコン誤記に関する情報を追加
  • 2022-08-12 10:45 [M006] 高熱窯、 [M007] タイル窯は窯である旨に訂正

ルールブック

軽く目を通しましたがドイツ語版との致命的な差異はなかったと思います。強いて言えば、「つい最近まで、泥炭を掘るというということは…」という文章がありますが、つい最近なんてことはなくて、前世紀ぐらいかな〜と思いました。

カード

[M006] 高熱窯

  • 日本語版のカード名に窯と書いてあるが、ドイツ語版・英語版を見る限りは窯ではない
    • 例えば、[C075] 薪 のような、窯を参照するカードの対象にはならない
  • 2022-08-12 追記) こちらのミスで、窯としての扱いで問題ないです
    • 旧版のカード名と同じだと思っていたら変わっていた
    • そもそもルールブックに Brenn- und Heizofen gelten entsprechend ihrem Namen als Öfen. って書いてあった

[M007] タイル窯

  • [M006] 高熱窯と同様に窯ではない
  • 2022-08-12 追記) こちらのミスで、窯としての扱いで問題ないです

[M022] ビオトープ

  • 「自分だけ小麦 そして/または 野菜を植えていれば食料1を得る」のそして/またはの使い方がよく分からない。小麦を植えていれば食料1、野菜を植えていれば食料1の意味が正解
    • 両方達成していれば食料2もらえる
  • 「森タイル そして/または 泥地タイル」のところも同様に、森タイル最多なら食料1、泥地タイル最多なら食料1
  • さらに、森タイル/泥地タイルのカウントについては、単独で最多である必要がある。誰かとタイならもらえない

[M023] 森のはずれ

  • (補足) 柵のスペースとは、タイルとタイルの間にある柵を置ける空間のことを指す。
    • 隣接している、という表現が若干わかりにくいが、タイルとタイルの間にある柵置き場の数を数えてね、と言う意味

[M031] 有用動物市場

  • 前提の「動物5」は「家畜5」

[M032] 泥炭塀の小屋

  • 食糧供給フェイズ -> 食料供給フェイズ

[M039] 特別牧場

  • 「柵で囲んだスペースは、タイルがなくなると牧場になる。」の一文が丸々不要。[M038] 自然保護区域の文章からコピーしてきたのだろう

[M044] 沼地化

  • 沼地タイル -> 泥地タイル

[M049] 不動産地図

  • 前提の「2ラウンド以前に出す」は「ラウンド2以前に出す」の意味

[M055] 道具小屋

  • 『 特別アクション「焼畑」または「泥炭を掘る」を行う前か後、すぐにもう1つの特別アクションも実行できる』とあるが、もう1つのとは同じカードに描かれているはずの2つのアクションのもう一方という意味である
    • 焼畑」と「労働市場」を同時に、と言う芸当ができるわけではない
    • 「もう一方の」という言葉を使うとこのニュアンスが表現できているかもしれない

[M065] 消防署

  • 森タイル5枚以上であればボーナス4点得られる
    • 日本語版のテキストでもそう解釈できはするのだが、すぐ後ろの[M066] 耕作区画のテキストのように「2/3/4/5枚以上 ある場合」、とした方が分かりやすいのではないだろうか

[M066] 耕作区画

  • 注釈として(未使用スペースによるマイナス点に追加される。)とあるが、未使用スペースのマイナス点は変わらずあるよ、という意味
    • このカードで得られる点はボーナス点扱いのまま

[M068] 教会

  • ※誤訳ではないし、日本語版の問題でもない
  • 左下に「泥炭を掘る」マークが描かれているが、カード効果的に不要と思われる
    • 独語版でも同様に印刷されていたので、言語に依存しないミスだろう

[M069] 革の鞍

  • 「馬が3以上になるたび」というよりは「馬が3以上ならば」の方がわかりやすいだろう

[M070] 泥炭考古学

  • 「空いた農場スペース」というのは、ただの未使用スペースということではない。「泥炭を掘る」を行ったことによってちょうど今空いたスペースを指している

[M073] 繁殖手当

  • 旧版の日本語版で勝利点という表記が確かにあったような気もするが、ボーナス点に統一したい
  • 独語版では(以降同様)とは書かれていない。最大でも3倍までしかボーナス点はもらえないと思う

[M074] 管理部門

  • 前提の「手札の進歩* 4枚以上」は「4枚以下」の間違い
    • 旧版の泥沼拡張も同じ誤訳があった

[M078] 小舟

  • 燃料1と木材1を交互に置くのではなく、燃料1と食料1を交互に置く
    • そして、(先に木材1を置く)とあるが、燃料から置くのが正解

[M081] 泥炭船

  • (ここから、建設資材1種類だけを得ることはできない。)という注釈は間違い。建設資材1つだけを得ることができない、が正しい。
    • 燃料2で葦1を買うのはダメだよ、という意図の注釈

[M084] 炭坑場

  • ルール上問題はないが、カード名は炭坑場ではなく炭坑馬の方が適切だと思う

[M092] 乾燥地

  • 「空の農場スペース」とあるが、[M070] 泥炭考古学 での指摘同様に、「泥炭を掘る」を行ったことによってちょうど今空いたスペースを指している

[M096] 休閑地

  • [M070] 泥炭考古学・[M092] 乾燥地 での指摘同様に、「空いている農場スペースに食料1ずつ置く」のではなく、特別アクションによってちょうど今空いたスペース1か所に食料1を置く
    • 先に挙げた2つは正しく解釈できなくもないが、こちらは「食料1ずつ」と書かれており明らかに誤訳

[M099] 泥薬

  • 『人物がベッドに横たわっている「病院」のアクションスペースを使うたび』という表現は不適切。ベッドに寝ている主体は病院ではなく人物であり、「ベッドに横たわっている人物が」とするのがよさそう

[M101] 肉処理台

  • 4、7、9、11、13、14ラウンド -> ラウンド4、7、9、11、13、14

以上です。良い泥沼ライフを!

ダークモードをおまけ感覚で提供するのは厳しい

スマートフォンを先日発売された Google Pixel 6a に乗り換えました。 もともとは Pixel 3a を利用していたのですが、セキュリティアップデートの提供保証が切れたことが買い替えの主な理由です。

store.google.com

機種変更に伴って、ワンタイムパスワードを用いた2要素認証の設定をし直しました。

GitHub の Web 上で表示された2次元コードをスキャンしたのですが、なぜか読み込むことができませんでした。

2次元コードはダミーです

原因はダークモードなのに2次元コードの周囲に白い余白が十分用意されていなかったことでした。 試しに、開発者ツールからスタイルを変更して、以下のように余白を白くすると読み込むことができました。

.qr-code-img {
  background: white !important;
}

近年は、CSS でカスタムプロパティが使えるようになり、変数の値をまとめて変えるだけでフロントエンドのデザインテーマを切り替えられるようになりました。 しかし、ライトなテーマの延長として色を変えるだけで実装すると、このように操作性の悪い UI が生まれてしまうことがあります。

今回の件以外にも、プロパティの設定値によってコントラストの差が小さくなり、視認性が悪くなるという問題が起こることも考えられます。

実装コストとの兼ね合いを考えると気づきベースでなんとかしていくしかないような気もします。しかし、ダークモードが市民権を得た今、アクセシビリティテストを CI で動かすなどの手法(思いつきです、できるかどうかは分からない)により、ダークモードを安心して世に出せる仕組みが整備されたいと思いました。