Delphi でコンウェイの "Doomsday Rule" ゲームを作ってみる

# はじめに
ライフゲームで有名なジョン・ホートン・コンウェイ氏が考案したアルゴリズムの中に **"Doomsday Rule"** というものがあります。

 - [Doomsday rule (Wikipedia:en)](https://en.wikipedia.org/wiki/Doomsday_rule)

これは計算により任意の日付の曜日を調べるもので、慣れると暗算も可能です。これをゲームにしてみたいと思います。

# 実装
まずはアルゴリズムの説明から。

## アルゴリズム

曜日とインデックスの関係は次のようになっています。0 ベースで日曜から始まります。

| 値 | 曜日 |
|:---:|:---:|
| 0 | 日曜日 |
| 1 | 月曜日 |
| 2 | 火曜日 |
| 3 | 水曜日 |
| 4 | 木曜日 |
| 5 | 金曜日 |
| 6 | 土曜日 |

これをまず覚えておいてください。

**Doomsday Rule** のアルゴリズムは次の通りです。`2020/04/11` を例に曜日を計算してみましょう。


### アンカーデイ (Anchor day) の算出

 1. 4 桁の年の上 2 桁を取り出します (X=20)。
 2. 4 で割った剰余を求めます (X=0)。
 3. 5 を掛けます (X=0)
 4. 2 を足します (X=2)
 5. 7 で割った剰余を求めます (X=2)。

アンカーデイ (のインデックス) は **2** となります。

`Anchor day = (5 * (C mod 4) + 2) mod 7`

### アンカーデイ (Anchor day) の算出 (2)

4 つの数字を暗記すれば、アンカーデイはもっと簡単に算出できます。

 1. 4 桁の年の上 2 桁を取り出します (X=20)。
 2. 4 で割った剰余を求めます (X=0)。
 3. [2, 0, 5, 3] の配列でアンカーデイを求めます (X=2)。

アンカーデイ (のインデックス) は **2** となります。

`Anchor day = [2, 0, 5, 3][C mod 4]`

### ドゥームズデイ (Doomsday) の算出

 1. 4 桁の年の下 2 桁を取り出します (X=20)。
 2. 奇数ならば 11 を加えます (X=20)。
 3. 2 で割ります (X=10)。
 4. 奇数ならば 11 を加えます (X=10)。
 5. 7 で割った剰余を求めます (X=3)。
 6. 7 から引きます (X=4)。
 7. アンカーデイ (のインデックス) を足します (X=6)。
 8. 7 で割った剰余を求めます (X=6)。

ドゥームズデイは **6**...つまり**土曜日** となります。

### 思い出深い日 (Memorable date)
ここから任意の日の曜日を求めるにはいくつかの日を覚えておく必要があります。

| 月 | 日付 |
|:---:|:---:|
| 4 | 4/4 |
| 6 | 6/6 |
| 8 | 8/8 |
| 10 | 10/10 |
| 12 | 12/12 |

4 月以降の偶数月で月と日が同じ日はドゥームズデイとなります。2020/4/4 は土曜日です。

| 月 | 日付 |
|:---:|:---:|
| 5 | 5/9 |
| 7 | 7/11 |
| 9 | 9/5 |
| 11 | 11/7 |

5 月以降の奇数月の上記の日付はドゥームズデイとなります。月と日を入れ替えた日 (5/9 <-> 9/5, 7/11 <-> 11/7) もドゥームズデイです。

| 月 | 日付 |
|:---:|:---:|
| 1 | 1/3 |
| 2 | 2/28 |

1 月は 1/3 が、2 月は 2/28 がドゥームズデイですが、

| 月 | 日付 |
|:---:|:---:|
| 1 | 1/4 |
| 2 | 2/29 |

うるう年だと一日ずれます。

| 月 | 日付 |
|:---:|:---:|
| 3 | 3/7 |

3 月は 3/7 がドゥームズデイです。ホワイトデー (3/14) など、別の覚えやすい日でも構いません。

| 月 | 日付 | 月 | 日付 |
|:---:|:---:|:---:|:---:|
| 1 | 1/3<br>(うるう年は 1/4) | 2 | 2 月の末日 |
| 3 | 3/7 | 4 | 4/4 |
| 5 | 5/9 | 6 | 6/6 |
| 7 | 7/11 | 8 | 8/8 |
| 9 | 9/5 | 10 | 10/10 |
| 11 | 11/7 | 12 | 12/12 |

まとめるとこんな感じになります。

先ほどの例だと `2020年` のドゥームズデイは**土曜日**でした。4/11 に最も近い日付は 4/4 で、この日も土曜日です。4/4 と 4/11 はちょうど 7 日離れているので **2020/4/11 は土曜日**という事になります。

### 例題
ではもう一問。`1992/7/19` は何曜日でしょう？

 1. 19 mod 4 = 3
 2. [2, 0, 5, 3][3] = 3 (Anchor)
 3. 92 / 2 = 46
 4. 46 mod 7 = 4
 5. 7 - 4 = 3
 6. Anchor + 3 = 6 (Doomsday = 土曜日)
 6. 7/11 とは +8 日離れているので (6 + 8) mod 7 = 0 (日曜日)

[夏希先輩](http://s-wars.jp/characters/characters02.html)の誕生日は日曜日です。

**See also:**

 - [サマーウォーズ (公式サイト)](http://s-wars.jp/)

## ソースコード
先述のアルゴリズムを Delphi で書いてゲームにしてみました。

```pascal:DoomsdayRule.dpr
program DoomsdayRule;

{$APPTYPE CONSOLE}

uses
  System.SysUtils, System.DateUtils, System.Diagnostics;

begin
  // 初期化
  var DoWarr := FormatSettings.LongDayNames; // [1..7]
  Randomize;

  // タイトル
  Writeln('=== The Doomsday Rule ===');
  Writeln;

  // 出題 (グレゴリオ暦: 1582/10/15～2199/12/31)
  var TargetDate := EncodeDate(1582, 10, 15);
  TargetDate := TargetDate + Random(Succ(Trunc(EncodeDate(2199, 12, 31) - TargetDate)));

  Writeln(FormatDateTime('Q. YYYY" 年 "M" 月 "D" 日 は何曜日？"', TargetDate));
  var LineStr := '';
  for var i in [0..6] do
    LineStr := LineStr + Format('%d:%s ', [i, DoWarr[Succ(i)]]);
  Writeln(' (', Trim(LineStr), ')');
  Writeln;

  // 測定開始
  var sw := TStopWatch.StartNew;

  // 解答
  var Ch: Char;
  while True do
    begin
      Readln(Ch);
      if CharInSet(Ch, ['0'..'6']) then
        Break;
      Writeln('不正な入力です。');
    end;
  var DoW := Ord(Ch) - Ord('0');
  Writeln('A. ', DoWarr[Succ(DoW)]);
  Writeln;

  // 正解の場合
  if DayOfWeek(TargetDate) = Succ(DoW) then
    begin
      Writeln('正解です！');
      Writeln('Time: ', sw.Elapsed.ToString); // 経過時間
      Exit;
    end;

  // 不正解の場合
  Writeln('残念。正解は "', DoWarr[DayOfWeek(TargetDate)], '" です。');
  Writeln;

  // Doomsday Century
  var Year: WORD := YearOf(TargetDate);
  var C := Year div 100;
  var AnchorDay := (5 * (C mod 4) + 2) mod 7;
//var AnchorDay := TArray<Integer>.Create(2, 0, 5, 3)[C mod 4]; // [2, 0, 5, 3]
  Writeln('Anchor day (A): ', AnchorDay, ':', DoWarr[Succ(AnchorDay)]);

  // 2-digit year
  var X := Year mod 100;
  Writeln('2-digit year: ', X);

  // Leap year?
  Writeln('Leap year?: ', TArray<string>.Create('No', 'Yes')[Ord(IsLeapYear(Year))]);

  // Odd or Even?
  Write('Odd or Even?: ');
  if Odd(X) then
    begin
      Writeln('Odd');
      Inc(X, 11);
      Writeln('Add 11: ', X);
    end
  else
    Writeln('Even');
  // Divide by 2
  X := X div 2;
  Writeln('Divide by 2: ', X);
  // Odd or Even?
  Write('Odd or Even?: ');
  if Odd(X) then
    begin
      Writeln('Odd');
      Inc(X, 11);
      Writeln('Add 11: ', X);
    end
  else
    Writeln('Even');
  // Modulo 7
  X := X mod 7;
  Writeln('Modulo 7: ', X);
  // Subtract from 7
  X := 7 - X;
  Writeln('Subtract from 7 (B): ', X);

  // Doomsday
  var Doomsday := (AnchorDay + X) mod 7;
  Writeln('Doomsday = (A + B) MOD 7: ', Doomsday, ':', DoWarr[Succ(Doomsday)]);

  // Memorable Date
  Writeln('Memorable Date (= Doomsday):');
  if IsLeapYear(Year) then
    Writeln('・01/04 (非閏年は 01/03)・02/29 (2 月の最終日)')
  else
    Writeln('・01/03 (閏年は 01/04)  ・02/28 (2 月の最終日)');
  Writeln('・03/07 (固定)          ・04/04 (月と日が同じ)');
  Writeln('・05/09 (09/05 も)      ・06/06 (月と日が同じ)');
  Writeln('・07/11 (11/07 も)      ・08/08 (月と日が同じ)');
  Writeln('・09/05 (05/09 も)      ・10/10 (月と日が同じ)');
  Writeln('・11/07 (07/11 も)      ・12/12 (月と日が同じ)');
end.
```
Delphi 10.3 Rio で開いてコンパイルするとコンソールアプリケーションが得られます。

**See also:**

 - [コンパイル済 Win32 バイナリ (Delphi Forum)](https://ht-deko.com/delphiforum/?vasthtmlaction=viewtopic&t=2259.0#postid-3974)


### 遊び方
DoomsdayRule.exe (Windows の場合) を実行します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/21785/505f3b83-2c1c-6b29-c615-a4975a398fd6.png)
答えの番号 (0～6) を入力して〔Enter〕キーを押します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/21785/840686e6-106a-6dfe-3a9c-7b8d962016d7.png)
正解すると解答に掛かった時間を表示します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/21785/52bdc536-eacc-6243-a257-15a0bbea5487.png)
答えを間違うと正解と解法が表示されます。

>  Conway could usually give the correct answer in under two seconds. To improve his speed, he practiced his calendrical calculations on his computer, which was programmed to quiz him with random dates every time he logged on.
>
> (Wikipedia 「Doomsday rule」より引用)

コンウェイ氏は問題を 2 秒で解答できたそうで、今回のようなアプリをトレーニングのために書いたようです。

# おわりに

2020/04/11、ジョン・ホートン・コンウェイ氏は新型コロナウィルス感染症 (COVID-19) のため永眠されました。謹んで哀悼の意を表します。

**See also:**

 - [ジョン・ホートン・コンウェイ (Wikipedia)](https://ja.wikipedia.org/wiki/%E3%82%B8%E3%83%A7%E3%83%B3%E3%83%BB%E3%83%9B%E3%83%BC%E3%83%88%E3%83%B3%E3%83%BB%E3%82%B3%E3%83%B3%E3%82%A6%E3%82%A7%E3%82%A4)
 - [C# で書かれたライフゲームを Delphi に移植してみる (Qiita)](https://qiita.com/ht_deko/items/4754f3252c83e3f601c3)
 - [[暗算] 特定日時の曜日暗算方法 (Qiita:@fujisystem)](https://qiita.com/fujisystem/items/171a949c58561dbd5046)
 - [日付から曜日を簡単に計算してしまう裏技 – あなたの誕生日は何曜日？ (数学の面白いこと・役に立つことをまとめたサイト)](https://analytics-notty.tech/trick-to-calculate-day-from-date/)