【Kotlin】レッスン5-☆3:石取りゲームを作ろう|抽象クラスと継承を実践しよう

一つ前のページではマルバツゲームについて学習しました。
今回は 石取りゲーム について見ていきましょう。
Lesson1:基礎文法編
Lesson2:制御構造編
Lesson3:関数編
Lesson4:コレクション編
Lesson5:オブジェクト指向編
・Lesson5-1:クラスの基本を理解しよう
・Lesson5-2:プライマリコンストラクタを理解しよう
・Lesson5-3:セカンダリコンストラクタを理解しよう
・Lesson5-4:ふたつのコンストラクタを使いこなそう
・Lesson5-5:アクセス修飾子とカプセル化を理解しよう
・Lesson5-6:クラスメンバとインスタンスメンバを理解しよう
・Lesson5-7:クラスの継承を理解しよう
・Lesson5-8:メソッドのオーバーライドを理解しよう
・Lesson5-9:クラスを拡張しよう
・Lesson5-10:抽象クラスを理解しよう
・Lesson5-11:インターフェースを理解しよう
・Lesson5-12:データクラスを理解しよう
・確認問題5-☆1:モンスター捕獲ゲームを作ろう
・確認問題5-☆2:マルバツゲームを作ろう
・確認問題5-☆3:石取りゲームを作ろう ◁今回はココ
Kotlinのゲームコード一覧はこちら
確認問題|抽象クラスとAIのロジックを使って石取りゲームを作成しよう

この課題では、2人のプレイヤーが交互に石を取り合う「石取りゲーム」を作成します。
各プレイヤーは1ターンで1~3個の石を取ることができます。最後の石を取ったプレイヤーが負けとなります。
この問題の要件
以下の要件に従ってコードを完成させてください。
- 抽象クラス
Player
を定義することname
という名前のプロパティ(String
型)を持つこと。takeStones
という抽象関数を持つこと。この関数は、プレイヤーが石を取る処理を定義します。
Player
クラスを継承するHumanPlayer
クラスを定義することtakeStones
関数をオーバーライドし、プレイヤーに1~3個の石を取るように指示すること。
Player
クラスを継承するAIPlayer
クラスを定義することtakeStones
関数をオーバーライドし、ランダムに1~3個の石を取ること。
Game
クラスを定義し、ゲームのロジックを実装することtotalStones
という名前のプロパティを持ち、ゲーム開始時の石の数を格納すること。startGame
という名前のメンバ関数を定義し、ゲームの進行を制御すること。
main
関数でゲームを実行することGame
オブジェクトを作成し、石の数を設定すること(例えば20個)。HumanPlayer
とAIPlayer
のオブジェクトを作成し、ゲームを開始すること。
ただし、以下のような実行結果となること。
残りの石の数: 20 あなた さん、何個の石を取りますか? (1~3個): 2 残りの石の数: 18 コンピュータ は 3 個の石を取りました 残りの石の数: 15 あなた さん、何個の石を取りますか? (1~3個): 3 残りの石の数: 12 コンピュータ は 1 個の石を取りました 残りの石の数: 11 あなた さん、何個の石を取りますか? (1~3個): 2 残りの石の数: 9 コンピュータ は 2 個の石を取りました 残りの石の数: 7 あなた さん、何個の石を取りますか? (1~3個): 3 残りの石の数: 4 コンピュータ は 1 個の石を取りました 残りの石の数: 3 あなた さん、何個の石を取りますか? (1~3個): 3 あなた が最後の石を取りました! あなた の負けです!
この問題を解くヒント
1からコードを組み立てることが難しい場合は、以下のヒントを開いて参考にしましょう。
- ヒント1【コードの構成を見る】
-
正解のコードは上から順に以下のような構成となっています。
1:kotlin.random.Randomモジュールのインポート
2:抽象クラスPlayerの定義
□ プライマリコンストラクタでnameプロパティを初期化
□ takeStones抽象関数を定義
3:HumanPlayerクラスの定義
□ Playerクラスを継承
□ takeStones関数をオーバーライド
□ □ ユーザー入力を受け取り、1~3の範囲に制限して返す
4:AIPlayerクラスの定義
□ Playerクラスを継承
□ takeStones関数をオーバーライド
□ □ 1~3のランダムな数を生成して返す
5:Gameクラスの定義
□ プライマリコンストラクタでtotalStonesプロパティを初期化
□ remainingStonesプロパティを定義し初期化
□ startGameメソッドの定義
□ □ ゲームループを開始
□ □ □ 残りの石の数を出力
□ □ □ 現在のプレイヤーのtakeStones関数を呼び出して石を取る
□ □ □ remainingStonesを更新
□ □ □ remainingStonesが0以下なら敗者を出力しループ終了
□ □ □ プレイヤー交代
6:main関数の定義
□ Gameオブジェクトgameを生成
□ HumanPlayerオブジェクトhumanを生成
□ AIPlayerオブジェクトaiを生成
□ gameのstartGameメソッドを呼び出しゲームを開始
- ヒント2【穴埋め問題にする】
-
以下のコードをコピーし、コメントに従ってコードを完成させて下さい。
import kotlin.random.Random // 抽象クラス Player を定義 abstract class Player(val name: String) { // プレイヤーが石を取るための抽象関数 abstract fun takeStones(totalStones: Int): Int } // 人間プレイヤーを表すクラス class HumanPlayer(name: String) : Player(name) { // 人間プレイヤーが石を取る処理を実装 override fun takeStones(totalStones: Int): Int { println("$name さん、何個の石を取りますか? (1~3個): ") /* 【穴埋め問題1】 ここにユーザーの入力を受け取り、変数 stones に格納するコードを書いてください。 1~3の範囲に制限するための処理も含めてください。 */ } } // AIプレイヤーを表すクラス class AIPlayer(name: String) : Player(name) { // AIプレイヤーがランダムに石を取る処理を実装 override fun takeStones(totalStones: Int): Int { /* 【穴埋め問題2】 ここにAIが1~3個のランダムな数の石を取るコードを書いてください。 */ } } // ゲームのロジックを実装するクラス class Game(val totalStones: Int) { private var remainingStones = totalStones // ゲームを開始するメンバ関数 fun startGame(player1: Player, player2: Player) { var currentPlayer = player1 while (remainingStones > 0) { println("残りの石の数: $remainingStones") /* 【穴埋め問題3】 ここに現在のプレイヤーが石を取る処理と、 その結果、残りの石を減らすコードを書いてください。 最後の石を取ったプレイヤーを判定し、負けと表示する処理も含めてください。 */ // プレイヤー交代 currentPlayer = if (currentPlayer == player1) player2 else player1 } } } fun main() { // 石の数を設定してゲームを開始 val game = Game(20) // 人間プレイヤーとAIプレイヤーを作成 val human = HumanPlayer("あなた") val ai = AIPlayer("コンピュータ") // ゲーム開始 game.startGame(human, ai) }
このヒントを見てもまだ回答を導き出すのが難しいと感じる場合は、先に正解のコードと解説を見て内容を理解するようにしましょう。
練習問題の解答と解説
この問題の正解コードとその解説は以下の通りです。
クリックして開いて確認してください。
- 正解コード
-
import kotlin.random.Random // 抽象クラス Player を定義 abstract class Player(val name: String) { // プレイヤーが石を取るための抽象関数 abstract fun takeStones(totalStones: Int): Int } // 人間プレイヤーを表すクラス class HumanPlayer(name: String) : Player(name) { // 人間プレイヤーが石を取る処理を実装 override fun takeStones(totalStones: Int): Int { println("$name さん、何個の石を取りますか? (1~3個): ") val stones = readLine()?.toIntOrNull() ?: 1 return stones.coerceIn(1, 3) // 1~3の範囲に制限 } } // AIプレイヤーを表すクラス class AIPlayer(name: String) : Player(name) { // AIプレイヤーがランダムに石を取る処理を実装 override fun takeStones(totalStones: Int): Int { val stones = Random.nextInt(1, 4) // 1~3のランダムな数 println("$name は $stones 個の石を取りました") return stones } } // ゲームのロジックを実装するクラス class Game(val totalStones: Int) { private var remainingStones = totalStones // ゲームを開始するメンバ関数 fun startGame(player1: Player, player2: Player) { var currentPlayer = player1 while (remainingStones > 0) { println("残りの石の数: $remainingStones") val stonesTaken = currentPlayer.takeStones(remainingStones) remainingStones -= stonesTaken if (remainingStones <= 0) { println("${currentPlayer.name} が最後の石を取りました!") println("${currentPlayer.name} の負けです!") break } // プレイヤー交代 currentPlayer = if (currentPlayer == player1) player2 else player1 } } } fun main() { // 石の数を設定してゲームを開始 val game = Game(20) // 人間プレイヤーとAIプレイヤーを作成 val human = HumanPlayer("あなた") val ai = AIPlayer("コンピュータ") // ゲーム開始 game.startGame(human, ai) }
- 正解コードの解説
-
コードをブロックごとに分割して解説します。
抽象クラス
Player
の定義abstract class Player(val name: String) { abstract fun takeStones(totalStones: Int): Int }
- 抽象クラス:
abstract
キーワードを使い、共通の振る舞いを持たせる基底クラスを定義しています。- 目的:名前プロパティを持つプレイヤーを統一的に扱う。
派生クラスで必ずtakeStones
メソッドを実装することを強制。
- 抽象メソッド:
abstract fun takeStones(totalStones: Int): Int
は未実装で、派生クラスで具体的な実装を行います。
HumanPlayer
クラスclass HumanPlayer(name: String) : Player(name) { override fun takeStones(totalStones: Int): Int { println("$name さん、何個の石を取りますか? (1~3個): ") val stones = readLine()?.toIntOrNull() ?: 1 return stones.coerceIn(1, 3) } }
- 継承:
HumanPlayer
はPlayer
クラスを継承しています。 override
キーワード:抽象クラスのtakeStones
メソッドを具体的に実装します。- 入力処理:
readLine()
でユーザー入力を取得します。- エラーハンドリング:
toIntOrNull()
で整数に変換し、失敗した場合はデフォルトで1
を返すようにしています。 - 範囲制限:
coerceIn(1, 3)
で、入力値を1~3の範囲内に制限します。
- エラーハンドリング:
AIPlayer
クラスclass AIPlayer(name: String) : Player(name) { override fun takeStones(totalStones: Int): Int { val stones = Random.nextInt(1, 4) println("$name は $stones 個の石を取りました") return stones } }
- ランダム処理:
Random.nextInt(1, 4)
で1~3のランダムな整数を生成します。 - 出力:AIの選択をコンソールに表示します。
ゲームロジック
a.
Game
クラスの定義class Game(val totalStones: Int) { private var remainingStones = totalStones }
totalStones
はゲーム開始時の石の総数。remainingStones
は現在残っている石の数を追跡します。
b. ゲーム進行ロジック
fun startGame(player1: Player, player2: Player) { var currentPlayer = player1 while (remainingStones > 0) { println("残りの石の数: $remainingStones") val stonesTaken = currentPlayer.takeStones(remainingStones) remainingStones -= stonesTaken if (remainingStones <= 0) { println("${currentPlayer.name} が最後の石を取りました!") println("${currentPlayer.name} の負けです!") break } currentPlayer = if (currentPlayer == player1) player2 else player1 } }
- 現在のプレイヤーの操作:
- 現在のプレイヤーが石を取る処理を実行。
remainingStones
を更新します。
- 終了条件:
- 石が0以下になった場合、現在のプレイヤーが負けと判定。
- プレイヤー交代:
if (currentPlayer == player1) player2 else player1
で手番を切り替えます。
main
関数fun main() { val game = Game(20) val human = HumanPlayer("あなた") val ai = AIPlayer("コンピュータ") game.startGame(human, ai) }
- ゲーム設定:
Game(20)
で石の総数を20個に設定。 - プレイヤー作成:
HumanPlayer
とAIPlayer
のインスタンスを生成。 - ゲーム開始:
startGame
でゲームを開始します。
- 抽象クラス:
まとめ
このコードでは抽象クラスや継承、動的なプレイヤーの交代ロジックを利用して石取りゲームを実装しています。以下の学びが得られます:
- 抽象クラスの使用法とオーバーライド。
- ランダム処理や入力の安全な処理。
- シンプルなゲームループの構築方法。
初心者の方は、このコードを基にカスタマイズ(例えば石の数の変更やAIの戦略追加)を試してみてください!
Kotlinのゲームコード一覧はこちら