物件購入の仕組みを理解しよう
忙しい方はここだけ見て
この章で見る大事な場所は、ここです。
Future<void> _handlePropertyTile(BoardTile tile) async {
final PropertyOwner? owner = _findOwner(tile.index);
if (owner == null) {
if (_currentPlayer.cash >= tile.price) {
_pendingPropertyTile = tile;
_phase = GamePhase.choosingProperty;
setState(() {});
} else {
_moveToNextPlayer();
}
return;
}
}
物件購入の流れは、こうです。
物件マスに止まる
↓
その物件に所有者がいるか確認
↓
未購入なら、買えるか確認
↓
所持金が足りれば「購入する / 見送る」を表示
↓
購入すると、現金が減る
↓
物件の所有者として登録される
実際に購入している場所は、ここです。
void _buyPendingProperty() {
final BoardTile? tile = _pendingPropertyTile;
final PlayerState player = _currentPlayer;
_players[_currentPlayerIndex] = player.copyWith(
cash: player.cash - tile.price,
properties: updatedProperties,
);
_propertyOwners.add(
PropertyOwner(
tileIndex: tile.index,
ownerPlayerId: player.id,
),
);
}
まずは、こう覚えればOKです。
物件を買う
↓
現金が減る
↓
自分の所有物になる
この章でやること
この章では、物件マスに止まったときの処理を見ていきます。
人生ゲームでは、物件を買うことで資産を増やせます。
このアプリでも、物件を買うとプレイヤーの所有物になります。
他のプレイヤーがそのマスに止まると、通行料が発生します。
物件を買う
↓
他の人が止まる
↓
通行料をもらえる
この章では、まず「物件を買うところ」までを理解します。
今日のゴール
この章のゴールは、次の3つです。
1. 物件マスに止まったときの流れが分かる
2. 所持金が足りると購入できる仕組みが分かる
3. 購入すると現金が減り、所有者として登録されると分かる
全部を完璧に覚えなくて大丈夫です。
まずは、「物件は価格と通行料を持っている」と分かればOKです。
Step 1:物件マスを確認しよう
物件マスは、_createTiles() の中にあります。
例えば、このマスです。
BoardTile(
index: 2,
age: 23,
stage: '社会人スタート期',
label: '小さなカフェ',
description: '小さなカフェに投資できます。所有すると、他のプレイヤーが止まった時に通行料を得られます。',
type: TileType.property,
price: 300,
rent: 80,
),
大事なのは、この3つです。
type: TileType.property,
price: 300,
rent: 80,
意味はこちらです。
| 項目 | 意味 |
|---|---|
type: TileType.property | 物件マス |
price: 300 | 購入価格 |
rent: 80 | 通行料 |
つまり、このマスは「300で買えて、他の人が止まると80もらえる物件」です。
Step 2:物件マスに止まるとどうなる?
マスごとの分岐では、物件マスのときにこの処理へ進みます。
case TileType.property:
await _handlePropertyTile(tile);
つまり、
物件マスに止まる
↓
_handlePropertyTile() に進む
という流れです。
物件の処理は少し長いので、別の関数に分けています。
Step 3:_handlePropertyTile()を探す
lib/main.dart を開いて、次を検索します。
_handlePropertyTile
このような関数があります。
Future<void> _handlePropertyTile(BoardTile tile) async {
final PropertyOwner? owner = _findOwner(tile.index);
if (owner == null) {
if (_currentPlayer.cash >= tile.price) {
await _showLifeEventModal(
title: tile.label,
categoryLabel: 'ASSET CHANCE',
subtitle: '${tile.age}歳|${tile.stage}',
description:
'${tile.description}\n\n購入価格は ${tile.price}、通行料は ${tile.rent} です。',
icon: Icons.storefront,
accentColor: AppColors.gold,
);
_pendingPropertyTile = tile;
_phase = GamePhase.choosingProperty;
_addLog('${tile.label} は未購入です。価格 ${tile.price} で購入できます。');
setState(() {});
} else {
_moveToNextPlayer();
}
return;
}
}
ここで、物件を買えるかどうかを判断しています。
Step 4:所有者がいるか確認する
最初に、このコードがあります。
final PropertyOwner? owner = _findOwner(tile.index);
これは、
この物件を誰かが持っているか確認する
という意味です。
まだ誰も買っていない場合は、owner == null になります。
owner == null
↓
所有者がいない
↓
まだ買える
すでに誰かが買っている場合は、所有者情報が入っています。
Step 5:_findOwner()とは?
所有者を探しているのは、この関数です。
PropertyOwner? _findOwner(int tileIndex) {
for (final PropertyOwner owner in _propertyOwners) {
if (owner.tileIndex == tileIndex) {
return owner;
}
}
return null;
}
やっていることはシンプルです。
所有者リストを見る
↓
同じマス番号の物件を探す
↓
見つかったら所有者を返す
↓
なければ null を返す
null は、「何もない」という意味です。
Step 6:未購入なら買えるか確認する
次に、この条件を見ます。
if (owner == null) {
if (_currentPlayer.cash >= tile.price) {
// 購入できる
} else {
// お金が足りない
}
}
意味はこうです。
所有者がいない
↓
今のプレイヤーの所持金を確認
↓
所持金が価格以上なら購入できる
例えば、所持金が1500で、物件価格が300なら買えます。
1500 >= 300
↓
買える
所持金が100で、物件価格が300なら買えません。
100 >= 300
↓
買えない
Step 7:購入するか選ぶ状態にする
買える場合は、この処理が動きます。
_pendingPropertyTile = tile;
_phase = GamePhase.choosingProperty;
意味はこちらです。
| コード | 意味 |
|---|---|
_pendingPropertyTile = tile | 購入候補の物件を保存する |
_phase = GamePhase.choosingProperty | 物件購入を選ぶ状態にする |
つまり、
この物件を買うかどうか、今から選びます
という状態にしています。
Step 8:購入ボタンと見送りボタンが出る
_phase が GamePhase.choosingProperty になると、下のボタンが切り替わります。
購入する
見送る
この表示を作っているのは、こちらです。
Widget _buildPropertyActions() {
final BoardTile? tile = _pendingPropertyTile;
if (tile == null) {
return _buildDiceActions(canRoll: false, isRolling: false);
}
return Column(
children: <Widget>[
Text(
'${tile.label} を購入しますか? 価格 ${tile.price} / 通行料 ${tile.rent}',
),
Row(
children: <Widget>[
FilledButton.icon(
onPressed: _buyPendingProperty,
label: const Text('購入する'),
),
OutlinedButton.icon(
onPressed: _skipPendingProperty,
label: const Text('見送る'),
),
],
),
],
);
}
難しく見えますが、やっていることはこうです。
購入候補の物件がある
↓
購入する / 見送る ボタンを表示
Step 9:購入する処理を見る
購入ボタンを押すと、この関数が動きます。
void _buyPendingProperty() {
final BoardTile? tile = _pendingPropertyTile;
if (tile == null) {
return;
}
final PlayerState player = _currentPlayer;
if (player.cash < tile.price) {
_addLog('${player.name} は資金不足で購入できません。');
_pendingPropertyTile = null;
_phase = GamePhase.waitingRoll;
_moveToNextPlayer();
return;
}
final Set<int> updatedProperties = <int>{
...player.properties,
tile.index,
};
_players[_currentPlayerIndex] = player.copyWith(
cash: player.cash - tile.price,
properties: updatedProperties,
);
_propertyOwners.add(
PropertyOwner(
tileIndex: tile.index,
ownerPlayerId: player.id,
),
);
_addLog('${player.name} は ${tile.label} を ${tile.price} で購入しました。');
_pendingPropertyTile = null;
_phase = GamePhase.waitingRoll;
_moveToNextPlayer();
}
長いですが、中心は3つです。
現金を減らす
持ち物に物件を追加する
所有者リストに登録する
Step 10:現金を減らす
購入すると、物件価格の分だけ現金が減ります。
cash: player.cash - tile.price,
例えば、所持金が1500で、物件価格が300なら、
1500 - 300 = 1200
になります。
つまり、購入後の現金は1200です。
Step 11:持っている物件を増やす
次に、このコードを見ます。
final Set<int> updatedProperties = <int>{
...player.properties,
tile.index,
};
これは、プレイヤーの持っている物件一覧に、今回買った物件を追加しています。
tile.index は、物件マスの番号です。
例えば、小さなカフェの index が2なら、
properties に 2 を追加する
という意味です。
Step 12:…player.propertiesとは?
少し難しく見えるのが、この部分です。
...player.properties
これは、今持っている物件をそのまま引き継ぐための書き方です。
イメージはこうです。
今まで持っていた物件
+
今回買った物件
=
新しい物件一覧
例えば、すでに2番の物件を持っていて、新しく5番の物件を買うなら、
{2} + {5} = {2, 5}
になります。
Step 13:プレイヤー情報を更新する
次に、プレイヤー情報を更新します。
_players[_currentPlayerIndex] = player.copyWith(
cash: player.cash - tile.price,
properties: updatedProperties,
);
これは、
現金を減らす
持っている物件を増やす
という更新です。
copyWith() を使うことで、名前や色や現在地はそのままにして、cash と properties だけ変えています。
Step 14:所有者リストに登録する
次に、このコードがあります。
_propertyOwners.add(
PropertyOwner(
tileIndex: tile.index,
ownerPlayerId: player.id,
),
);
これは、
この物件は、このプレイヤーが持っています
という情報を登録しています。
例えば、
tileIndex: 2
ownerPlayerId: 0
なら、
2番の物件は、Player 1が持っている
という意味です。
Step 15:なぜpropertiesと_propertyOwnersの両方があるの?
少しややこしいですが、役割が違います。
| データ | 役割 |
|---|---|
player.properties | プレイヤーが持っている物件一覧 |
_propertyOwners | どの物件を誰が持っているかの一覧 |
イメージはこうです。
player.properties
↓
Player 1は、2番と5番を持っている
_propertyOwners
↓
2番はPlayer 1
5番はPlayer 1
8番はPlayer 2
どちらも、物件管理のために使っています。
Step 16:購入が終わったら次のプレイヤーへ
購入処理の最後には、このような処理があります。
_pendingPropertyTile = null;
_phase = GamePhase.waitingRoll;
_moveToNextPlayer();
意味はこちらです。
| コード | 意味 |
|---|---|
_pendingPropertyTile = null | 購入候補を空にする |
_phase = GamePhase.waitingRoll | サイコロ待ち状態に戻す |
_moveToNextPlayer() | 次のプレイヤーへ交代する |
購入が終わったら、次の人のターンに進みます。
Step 17:見送る処理もある
購入しない場合は、この関数が動きます。
void _skipPendingProperty() {
final BoardTile? tile = _pendingPropertyTile;
if (tile != null) {
_addLog('${_currentPlayer.name} は ${tile.label} の購入を見送りました。');
}
_pendingPropertyTile = null;
_phase = GamePhase.waitingRoll;
_moveToNextPlayer();
}
これは、
物件を買わない
↓
購入候補を空にする
↓
サイコロ待ちに戻す
↓
次のプレイヤーへ
という流れです。
Step 18:お金が足りない場合
所持金が足りない場合は、購入できません。
if (_currentPlayer.cash >= tile.price) {
// 買える
} else {
await _showLifeEventModal(
title: tile.label,
categoryLabel: 'ASSET CHANCE',
description:
'${tile.description}\n\n購入価格は ${tile.price} ですが、現在の所持金では購入できません。',
icon: Icons.storefront,
accentColor: AppColors.gold,
);
_addLog('${_currentPlayer.name} は ${tile.label} を購入する資金が足りません。');
_moveToNextPlayer();
}
流れはこうです。
所持金が足りない
↓
買えないメッセージを表示
↓
次のプレイヤーへ
このようにして、所持金より高い物件は買えないようにしています。
触ってみよう
今回は、物件価格を変えてみましょう。
_createTiles() の中から、小さなカフェを探します。
変更前です。
BoardTile(
index: 2,
age: 23,
stage: '社会人スタート期',
label: '小さなカフェ',
description: '小さなカフェに投資できます。所有すると、他のプレイヤーが止まった時に通行料を得られます。',
type: TileType.property,
price: 300,
rent: 80,
),
少し高くするなら、こうです。
price: 500,
rent: 120,
変更後です。
BoardTile(
index: 2,
age: 23,
stage: '社会人スタート期',
label: '小さなカフェ',
description: '小さなカフェに投資できます。所有すると、他のプレイヤーが止まった時に通行料を得られます。',
type: TileType.property,
price: 500,
rent: 120,
),
保存して、物件マスに止まったときに価格が変わっていれば成功です。
もう1つ触ってみよう
物件名を変えてみます。
変更前です。
label: '小さなカフェ',
変更後です。
label: '小さな本屋',
説明文も合わせて変えます。
description: '小さな本屋に投資できます。所有すると、他のプレイヤーが止まった時に通行料を得られます。',
これだけでも、ゲームの世界観が変わります。
よくあるエラーと直し方
1. priceを文字にしてしまった
悪い例です。
price: '500',
正しくはこちらです。
price: 500,
数字は ' で囲みません。
2. rentを文字にしてしまった
悪い例です。
rent: '120',
正しくはこちらです。
rent: 120,
price と rent は数字です。
3. TileType.propertyを消してしまった
物件マスにしたい場合は、これが必要です。
type: TileType.property,
これを別の種類にすると、物件購入処理に進まなくなります。
4. _pendingPropertyTileを消してしまった
_pendingPropertyTile = tile;
これは、購入候補の物件を保存する大事な処理です。
消すと、購入ボタンを押したときに、どの物件を買うのか分からなくなります。
初心者のうちは消さないようにしましょう。
5. _moveToNextPlayer()を消してしまった
購入後や見送り後には、次のプレイヤーへ進みます。
_moveToNextPlayer();
これを消すと、ターンが進まなくなることがあります。
この章で覚えること
この章で覚えることは、3つだけです。
1. 物件マスは TileType.property
2. price は購入価格、rent は通行料
3. 購入すると cash が減り、所有者として登録される
まずはこれだけで大丈夫です。
やる気を維持するコツ
物件購入が分かると、人生ゲームが一気にゲームらしくなります。
ただ進むだけではなく、
買うか
見送るか
お金を残すか
資産を増やすか
という選択が生まれるからです。
ゲームが面白くなるポイントは、「選べること」です。
まずは物件名や価格を変えて、自分なりのバランスを試してみましょう。
チェックリスト
□ 物件マスが TileType.property だと分かった
□ price が購入価格だと分かった
□ rent が通行料だと分かった
□ _handlePropertyTile() を見つけた
□ _findOwner() が所有者確認だと分かった
□ owner == null が未購入という意味だと分かった
□ 所持金が足りると購入ボタンが出ると分かった
□ _buyPendingProperty() が購入処理だと分かった
□ 購入すると cash が減ると分かった
□ _propertyOwners に所有者が登録されると分かった
□ 物件価格を変更して動作確認した
まとめ
この章では、物件購入の仕組みを確認しました。
物件マスは TileType.property で作ります。
price が購入価格、rent が通行料です。
プレイヤーが未購入の物件マスに止まり、所持金が足りていれば、購入するか見送るかを選べます。
購入すると、プレイヤーの現金が減り、物件が所有者リストに登録されます。
次の章では、通行料の仕組みを見ながら、他のプレイヤーの物件に止まったときのお金の移動を理解していきます。
