⇒ 動作確認はこちらからどうぞ。
このゲームはすぐには終わらないのでゲームの途中で保存し、あとで続きをプレイできる機能を追加します。データをファイルとしてダウンロードして、これをアップロードすればその場面から再開できるようにするのです。
C#であれば複数のデータを簡単にXMLファイルとして保存できますが、Blazor WebAssemblyでこれをやろうとすると、XmlSerializer.Deserializeで例外(Synchronous reads are not supported)が発生します。XmlSerializer.Serializeは問題なくできるのですが、どうなっているのでしょうか?
XmlSerializer.Deserializeを非同期で呼び出しても同じエラーが出るので他の方法でやることにします。
ファイルをダウンロードできるようにするには index.htmlを以下のように変更します。
| 
					 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38  | 
						<!DOCTYPE html> <html> <head>     <meta charset="utf-8" />     <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />     <title>信長の野望もどき</title>     <link href="css/bootstrap/bootstrap.min.css" rel="stylesheet" />     <link href="css/app.css" rel="stylesheet" />     <link href="_WebApplicationNobunaga.styles.css" rel="stylesheet" /> </head> <body>     <div id="app">Loading...</div>     <div id="blazor-error-ui">         An unhandled error has occurred.         <a href="" class="reload">Reload</a>         <a class="dismiss">??</a>     </div>     <script src="_framework/blazor.webassembly.js"></script>     <!-- Blazor.Extensions.Canvasを使うので必要 -->     <script src="_content/Blazor.Extensions.Canvas/blazor.extensions.canvas.js"></script>     <!-- ファイルをダウンロードできるようにするためにJavaScriptの関数を追加 -->     <script>         function fileDownload(str) {             var blob = new Blob([str], { "type": "text/plain" });             let link = document.createElement('a')             link.href = window.URL.createObjectURL(blob)             link.download = 'data.txt'             link.click()         }     </script> </body> </html>  | 
					
Index.razorにダウンロード用のボタンとアップロード用のボタンを追加します。
[保存]ボタンをクリックするとテキストファイルがダウンロードされるのですが、これはいつでもできるのではなくターンが回ってきたときだけにします。
| 
					 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27  | 
						@page "/" @using Blazor.Extensions.Canvas @inject IJSRuntime JSRuntime @inject HttpClient HttpClient @* 途中省略 *@ <button class="btn btn-primary" @onclick="btnGameStartClick" style="display:@btnStartDisplay">ゲーム開始</button> <p>@yearAtring</p> @* ファイルをアップロードするために必要 *@ <InputFile OnChange="@OnInputFileChange" /> <p>@turn</p> <p>@message</p> <button class="btn btn-primary" @onclick="btnNextClick" style="display:@btnNextDisplay">次</button> @* 他のボタン省略 *@ @* ターンが回ってきたとき(@btnWaitDisplay == "inline-block")だけしかクリックできない *@ <button class="btn btn-primary" @onclick="btnSaveClick" style="display:@btnWaitDisplay">保存</button> @* 以下、省略 *@  | 
					
[保存]ボタンをクリックすると以下の処理がおこなわれます。城のID、名前、城主など、それからCastleStatus.AttackingCorpsプロパティやCastleStatus.SiegingCorpsプロパティがnullではない場合はそれらの情報もカンマで区切ってテキストファイルとして保存します。
| 
					 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70  | 
						public partial class Index {     void btnSaveClick()     {         if (CastleStatusList == null)         {             message = "ゲームを開始しないと保存できません!";             return;         }         string str = "";         foreach (CastleStatus status in CastleStatusList)         {             str += status.CastleID + ",";             str += status.CastleName + ",";             str += status.CastleOwner + ",";             str += status.SoldierCount + ",";             str += status.MilitaryFood + ",";             str += status.Yield + ",";             if (status.AttackingCorps != null)             {                 Corps attacking = status.AttackingCorps;                 str += attacking.AttackBaseName + ",";                 str += attacking.AttackBaseOwner + ",";                 str += attacking.SoldierCount + ",";                 str += attacking.MilitaryFood + ",";                 str += attacking.AttackBase != null ? "true" : "false";                 str += ",";             }             else                 str += ",,,,,";             if (status.SiegingCorps != null)             {                 Corps sieging = status.SiegingCorps;                 str += sieging.AttackBaseName + ",";                 str += sieging.AttackBaseOwner + ",";                 str += sieging.SoldierCount + ",";                 str += sieging.MilitaryFood + ",";                 str += sieging.AttackBase != null ? "true" : "false";                 str += ",";             }             else                 str += ",,,,,";             if (status.SupportCorps != null)             {                 Corps support = status.SupportCorps;                 str += support.AttackBaseName + ",";                 str += support.AttackBaseOwner + ",";                 str += support.SoldierCount + ",";                 str += support.MilitaryFood + ",";                 str += support.AttackBase != null ? "true" : "false";                 str += ",";             }             else                 str += ",,,,,";             str += status.Done ? "true" : "false";             str += "\n"; // CastleStatusごとに改行をいれる         }         str += month + "\n"; // 最後に月情報を追加する         List<string> vs = new List<string>();         vs.Add(str);         JSRuntime.InvokeAsync<string>("fileDownload", vs.ToArray());     } }  | 
					
ファイルがアップロードされたらOnInputFileChangeメソッドが実行されます。
アップロードされたファイルを一行ずつ読み込んで、これをカンマで分割します。これを元にCastleStatusのプロパティをセットします。
攻め込んだ軍団が城に戻れなくなっている場合、従来のCorpsクラスのコンストラクタでは対応できないので、別のコンストラクタを作成しました。
| 
					 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112  | 
						public partial class Index {     protected async void OnInputFileChange(Microsoft.AspNetCore.Components.Forms.InputFileChangeEventArgs e)     {         Stream stm = e.File.OpenReadStream();         StreamReader sr = new StreamReader(stm);         // CastleStatusListを初期化する         InitCastleStatusList();         // ターン順に城IDを格納する         List<int> vs1 = new List<int>();         while (true)         {             string str = await sr.ReadLineAsync();             if (str == null)             {                 month = 0;                 break;             }             if (str.Length < 5)             {                 // 最後は月情報しかない行なのでここでbreakするはず・・・                 month = int.Parse(str);                 break;             }             // カンマで分割し配列に格納。             // 配列の添字は [0]城ID [1]城名 [2]城主 [3]兵数 [4]兵糧 [5]収穫量             string[] vs2 = str.Split(new char[] { ',' }, StringSplitOptions.None);             // ターン順に城IDを格納する             vs1.Add(int.Parse(vs2[0]));             CastleStatus status = CastleStatusList.First(x => x.CastleID == int.Parse(vs2[0]));             status.CastleName = vs2[1];             status.CastleOwner = vs2[2];             status.SoldierCount = int.Parse(vs2[3]);             status.MilitaryFood = int.Parse(vs2[4]);             status.Yield = int.Parse(vs2[5]);             // [6]城名が空文字列のとき攻め込もうとしている軍団は存在しない             if (vs2[6] != "")             {                 //[6]城名 [7]城主 [8]兵数 [9]兵糧 [10]城に戻れるか?;                 CastleStatus status1 = CastleStatusList.First(x => x.CastleName == vs2[6]);                 if (vs2[10] == "true")                     status.AttackingCorps = new Corps(int.Parse(vs2[8]), int.Parse(vs2[9]), status1);                 else                     status.AttackingCorps = new Corps(vs2[6], vs2[7], int.Parse(vs2[8]), int.Parse(vs2[9]));             }             // [11]城名が空文字列のとき城を包囲している軍団は存在しない             if (vs2[11] != "")             {                 //[11]城名 [12]城主 [13]兵数 [14]兵糧 [15]城に戻れるか?;                 CastleStatus status1 = CastleStatusList.First(x => x.CastleName == vs2[11]);                 if (vs2[15] == "true")                     status.SiegingCorps = new Corps(int.Parse(vs2[13]), int.Parse(vs2[14]), status1);                 else                     status.SiegingCorps = new Corps(vs2[11], vs2[12], int.Parse(vs2[13]), int.Parse(vs2[14]));             }             // [16]城名が城におくられた援軍は存在しない             if (vs2[16] != "")             {                 //[16]城名 [17]城主 [18]兵数 [19]兵糧 [20]城に戻れるか?;                 CastleStatus status1 = CastleStatusList.First(x => x.CastleName == vs2[16]);                 if (vs2[20] == "true")                     status.SupportCorps = new Corps(int.Parse(vs2[18]), int.Parse(vs2[19]), status1);                 else                     status.SiegingCorps = new Corps(vs2[16], vs2[17], int.Parse(vs2[18]), int.Parse(vs2[19]));             }             //[21]ターンは終わったか?             status.Done = vs2[21] == "true" ? true : false;         }         sr.Close();         // ターンの順番も復元する         List<CastleStatus> statuses = new List<CastleStatus>();         foreach (int i in vs1)         {             statuses.Add(CastleStatusList.First(x => x.CastleID == i));         }         CastleStatusList = statuses;         // 表示されている「状況」をクリアする         _situations.Clear();         // 何年何月の文字列をセット         yearAtring = GetYearMonth(month);         // ターンの順番の文字列をセット         string s = "";         foreach (CastleStatus status in CastleStatusList)         {             s += " >> " + status.CastleName;         }         turn = s;         // CastleStatus.Doneがfalseで最初のものを CurentCastleStatusにセットし         // Turnメソッドを実行すれば自分の城のターンでゲームが再開される。         CurentCastleStatus = CastleStatusList.First(x => !x.Done);         this.Turn();         // セットされた文字列を表示         this.StateHasChanged();     } }  | 
					
以下は城に戻れない場合のCorpsクラスのコンストラクタです。
| 
					 1 2 3 4 5 6 7 8 9 10 11 12  | 
						public class Corps {     public Corps(string castleName, string castleOwner, int soldierCount, int militaryFood)     {         SoldierCount = soldierCount;         MilitaryFood = militaryFood;         AttackBase = null;         _attackBaseName = castleName;         _attackBaseOwner = castleOwner;     } }  | 
					
