カードゲームXENOをターミナル上で、コンピューターと対戦して遊べるアプリを作りました。
XENOをご存じないかたのために説明しますと、オリエンタルラジオの中田敦彦さんがプロデュースした新しいカードゲームです。UNOの売り上げをこえたことや、YOUTUBE上でメンタリストのDAIGOさんと白熱した心理戦を繰り広げられたことで話題になりました。
もともとはラブレターというカードゲームを題材として作られており、そこに中田敦彦さんが新たに手を加えて二ヶ月ほどで世界観や設定を作り上げました。AMAZONで770円ほどで購入することができます。
あまり興味がない方もいるかと思いますが、中田敦彦さんはDAIGOさん以外にも、ホリエモン、YouTuberのヒカルさん、クイズ王の伊沢拓司さん、雨上がり決死隊の宮迫博之さんとも白熱した戦いをYOUTUBE上で公開してますので、そちらだけでもぜひ見てみてください。
※追記:研修で習った知識をいかして、オブジェクト指向でXENOを作り直しました(7月12日)
ファイルを作るのが手間かと思いますので、すぐダウンロードしてプレイできるようgithubのコードを掲載させて頂きます。
https://github.com/saitou-daiki/xeno_new
ダウンロードしてフォルダを解凍した後、ターミナル上で「ruby xeno.rb」と打ち込む事ですぐプレイ可能です。
よければ感想もお待ちしています。
工夫した点
・勝敗が決まる直前は、緊迫感を持たせるために文章を一行ずつ表示
・コンティニュー機能を実装
・game_resultのクラスに勝敗の記録を保持
・deck.typeにカード名と番号のハッシュを持たせることで、番号の情報だけででカード名を表示できるようにした。
・メッセージの表示に一部moduleを利用
・変数名をわかりやすいように修正
とにかく見やすさやと操作のしやすさを意識して作りました。
class Game_result
attr_accessor :myplayer
attr_accessor :pcplayer
attr_accessor :compensating
attr_accessor :continue
def initialize
self.myplayer = 0
self.pcplayer = 0
self.compensating = 0
self.continue = true
end
end
Game_resultクラスの解説
myplayerは自分が勝った回数のカウント
pcplayerはPCが勝った回数のカウント
compensatingは相打ちの回数のカウント
continueは、もう一度プレイするかを判定
初回のプレイ時はtrueで定義してあります。
class Deck
attr_accessor :card
attr_accessor :reincarnation
attr_accessor :type
def initialize
self.card = [1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7, 7, 8, 8, 9, 10].shuffle
self.reincarnation = self.card.delete_at(0)
self.type = {1 => "少年(革命)", 2 => "兵士(捜査)", 3 => "占い師(透視)", 4 => "乙女(守護)", 5 => "死神(疫病)", 6 => "貴族(対決)", 7 => "賢者(選択)", 8 => "精霊(交換)", 9 => "皇帝(公開処刑)", 10 => "英雄(潜伏・転生)"}
end
end
Deckのクラスの解説
self.cardにXENOの全てのカードをの情報を代入。
self.reincarnationに転生札のカードの情報を代入(delete_at(0)とすることで、self.cardの配列の0番の値を消しながら、その値をself.reincarnationに代入してます)
typeはカード名を毎回表示できるようにするために作りました。
例
input = 1
deck.type[input]
こう記述すれば数字の情報だけで画面に「少年(革命)」と表示されます。
module Message
def start_message
puts "----------ゲームを開始します----------"
end
def end_message
puts "----------ゲームを終了します----------"
end
def restart_message
puts "----------ゲームを再開します----------"
end
def none_deck_message
puts "山札のカードがなくなりました。"
end
def line
puts "--------------------------------------"
end
def none_effect_message
puts "山札にカードがないため、効果を発動しません。"
end
def wiseman_method_message
puts "次のターン、カードを3枚ドローし1枚を選択して手札に加えます。"
end
def tutorial_message
puts <<~text
カードの種類は全部で10種類です。
① 少年(革命)
1枚目の捨て札は何の効果も発動しないが、場に2枚目が出た時には皇帝と同じ効果「公開処刑」が発動する。
--------------------------------------
② 兵士(捜査)
指定した相手の手札を言い当てると相手は脱落する。
--------------------------------------
③ 占い師(透視)
指定した相手の手札を見る。
--------------------------------------
④ 乙女(守護)
次の自分の手番まで自分への効果を無効にする。
--------------------------------------
⑤ 死神(疫病)
指名した相手に山札から1枚引かせる。
2枚になった相手の手札を非公開にさせたまま、1枚を指定して捨てさせる。
--------------------------------------
⑥ 貴族(対決)
指名した相手と手札を見せ合い、数字の小さい方が脱落する。
見せ合う際には他のプレイヤーに見られないよう密かに見せ合う。
--------------------------------------
⑦ 賢者(選択)
次の手番で山札から1枚引くかわりに3枚引き、そのうち1枚を選ぶことができる。
残り2枚は山札へ戻す。
--------------------------------------
⑧ 精霊(交換)
指名した相手の手札と自分の持っている手札を交換する。
--------------------------------------
⑨ 皇帝(公開処刑)
指名した相手に山札から1枚引かせて、手札を2枚とも公開させる。
そしてどちらか1枚を指定し捨てさせる。
--------------------------------------
⑩ 英雄(潜伏・転生)
場に出すことができず、捨てさせられたら脱落する。
皇帝以外に脱落させらた時に転生札で復活する。
--------------------------------------
text
end
def none_card_message
puts "ありません。"
end
def open_card_message
puts "お互いの持っているカードをオープンします。"
gets.chomp
end
def compensating_message
puts "持っていたカードが同じのため、相打ちです。"
end
def question_continue_message
puts <<~text
もう一度対戦しますか?
[1]はい。
[2]いいえ。
text
end
def one_two_message
puts "1か2の番号を入力してください。"
end
def one_ten_message
puts "1から10までの番号を入力してください。"
end
def shuffle_message
puts "残りのカードを山札に戻してシャッフルしました。"
end
def dis_card_choice_message
puts "[11]使用済みカードを確認する。"
end
def tutorial_choice_message
puts "[0]チュートリアルを閲覧する。"
end
def question_card_message
puts "どの番号のカードを使用しますか?"
end
def question_add_card_message
puts "どの番号のカードを手札に加えますか?"
end
def question_dis_card_message
puts "どの番号のカードを捨て札に送りますか?"
end
def zero_eleven_message(input)
puts "#{input}番のカードは選択できません。"
puts "0か11か手札にあるカードの番号を入力してください。"
puts "--------------------------------------"
end
def hero_not_can_choice_message
puts "英雄のカードは手札から使用することができません。"
puts "--------------------------------------"
end
def my_turn_message(myplayer)
puts "#{myplayer.name}のターンです。"
end
def pc_turn_message(pcplayer)
puts "#{pcplayer.name}のターンです。"
end
def none_draw_card_message(input)
puts "#{input}番のカードは、引いたカードの中にありません。"
end
end
moduleの解説
puts <<~text
text
この記述は複数行の文章を表示するのにとても便利です。
""とputsの記述を省くことができます。
moduleはクラスを継承しても引継ぎができないので、使用するファイルでは毎回「include Message」と「require "./message.rb"」を記述する必要があります。
require "./message"
class Player
attr_accessor :guard
attr_accessor :wiseman
attr_accessor :card
attr_accessor :dis_card
attr_accessor :name
attr_accessor :victory
include Message
def initialize(deck_delete, name)
self.guard = false
self.wiseman = false
self.card = [deck_delete]
self.dis_card = []
self.name = name
self.victory = false
end
#プレイヤーとPCで共通のメソッドを定義。乙女の効果の発動、4、5、7、8番のカード使用時。
def guard_method(deck, input, player)
puts "#{self.name}の#{deck.type[4]}のカードの効果により、#{player.name}の#{deck.type[input]}のカードの効果を無効化しました。"
end
def fortune_teller(deck, player)
puts "#{player.name}が持っていたのは#{player.card[0]}番の#{deck.type[player.card[0]]}のカードです。"
end
def maiden(player)
puts "次のターンまで#{player.name}の攻撃が無効化されます。"
self.guard = true
end
def death_god(deck, player)
if deck.card.empty?
none_effect_message
else
puts "#{deck.type[5]}の効果により、#{player.name}がカードを1枚ドローしました。"
player.card[1] = deck.card.delete_at(0)
input = player.card.delete_at(player.card.find_index(player.card.sample))
player.dis_card << input
if input == 10
puts "#{deck.type[5]}の効果により、#{deck.type[input]}のカードを捨て札に送りました。"
puts "#{player.name}は#{deck.type[input]}の効果により、#{deck.type[player.card[0]]}のカードを捨て、転生札よりカードを引いて復活します。"
player.dis_card << player.card[0]
player.card[0] = deck.reincarnation
else
puts "#{deck.type[5]}の効果により、#{player.name}の#{deck.type[input]}のカードを捨て札に送りました。"
end
end
end
def wiseman_method
wiseman_method_message
self.wiseman = true
end
def spirit(deck, player)
puts "#{self.name}の持っている#{deck.type[self.card[0]]}のカードと、#{player.name}の持っている#{deck.type[player.card[0]]}のカードを交換しました。"
self.card[0], player.card[0] = player.card[0], self.card[0]
end
end
Playerクラスの解説
playerのクラスにmyplayerとpcplayerの共通のメソッド、フィールド、コンストラクタを定義しています。
オブジェクト指向で実装すると修正が容易になる理由が、実装してみてよくわかりました。
設計にものすごく頭を使いますが、修正を考えると楽になりますね。
コンストラクタの記述も解説しておきます。
self.guard(乙女(守護)のカードの効果が働いているかを判定。初期はfalse)
self.wiseman(賢者(選択)のカードの効果が働いているかを判定。初期はfalse)
self.card(手札にあるカードの配列)
self.dis_card(捨て札のカードの配列)
self.victory(勝った方のplayerのvictoryをtrueにすることで、どちらが勝ったかを判定する)
self.name(インスタンス生成時に、引数に記述した名前の文字情報を代入)
require "./player.rb"
require "./message"
class Myplayer < Player
include Message
def wiseman_draw(deck, player)
puts "#{self.name}は#{deck.type[7]}のカードの効果により、カードを3枚ドローしました。"
wisemans = deck.card.first(3)
while true
puts "#{self.name}の現在の手札にあるのは#{self.card[0]}番の#{deck.type[self.card[0]]}のカードです。"
question_add_card_message
dis_card_choice_message
wisemans.each do |wiseman|
puts "[#{wiseman}]#{deck.type[wiseman]}のカードを手札に加える。"
end
input = gets.chomp.to_i
if wisemans.any?(input)
break
elsif input == 11
self.dis_card_print(deck,player)
else
none_draw_card_message(input)
end
end
self.card[1] = deck.card.delete_at(deck.card.find_index(input))
deck.card.shuffle!
puts "#{self.card[1]}番の#{deck.type[self.card[1]]}のカードを手札に加えました。"
shuffle_message
self.wiseman = false
end
def draw(deck)
self.card[1] = deck.card.delete_at(0)
puts "#{self.name}は、山札から#{deck.type[self.card[1]]}のカードをドローしました。"
end
def choice(deck, player)
while true
puts "#{self.name}の手札にあるのは#{self.card[0]}番の#{deck.type[self.card[0]]}と#{self.card[1]}番の#{deck.type[self.card[1]]}のカードです。"
question_card_message
tutorial_choice_message
dis_card_choice_message
puts "[#{self.card[0]}]#{deck.type[self.card[0]]}のカードを使用する。"
puts "[#{self.card[1]}]#{deck.type[self.card[1]]}のカードを使用する。"
input = gets.chomp.to_i
if input == 10
hero_not_can_choice_message
elsif self.card.any?(input)
break
elsif input == 0
tutorial_message
elsif input == 11
self.dis_card_print(deck,player)
else
zero_eleven_message(input)
end
end
self.dis_card << self.card.delete_at(self.card.find_index(input))
puts "#{self.name}が#{input}番の#{deck.type[input]}のカードを使用しました。"
return input
end
def boy(deck, player)
if deck.card.empty?
none_effect_message
elsif (self.dis_card + player.dis_card).count(1)==2
puts "#{deck.type[1]}のカードが使われたのは2枚目のため、効果を発動します。"
player.card[1] = deck.card.delete_at(0)
puts "#{deck.type[1]}の効果により、#{player.name}がカードを1枚ドローしてオープンします。"
while true
puts "#{player.name}が持っているのは、#{player.card[0]}番の#{deck.type[player.card[0]]}と#{player.card[1]}番の#{deck.type[player.card[1]]}のカードです。"
question_dis_card_message
dis_card_choice_message
puts "[#{player.card[0]}]#{deck.type[player.card[0]]}のカードを捨て札に送る。"
puts "[#{player.card[1]}]#{deck.type[player.card[1]]}のカードを捨て札に送る。"
input = gets.chomp.to_i
if player.card.any?(input)
break
elsif input == 11
self.dis_card_print(deck, player)
else
puts "#{input}番のカードは#{player.name}の手札にありません。"
end
end
player.dis_card << player.card.delete_at(player.card.find_index(input))
puts "#{self.name}が#{player.name}の手札にある#{input}番の#{deck.type[input]}のカードを捨て札に送りました。"
if input == 10
puts "#{player.name}は#{deck.type[input]}の効果により、#{deck.type[player.card[0]]}のカードを捨て、転生札よりカードを引いて復活します。"
player.dis_card << player.card[0]
player.card[0] = deck.reincarnation
end
else
puts "#{deck.type[1]}のカードは初めて使われたため、効果を発動しません。"
end
end
def soldier(deck, player)
while true
self.dis_card_print(deck,player)
puts "#{self.name}がいま持っているカードは#{self.card[0]}番の#{deck.type[self.card[0]]}のカードです。"
puts "#{player.name}が持っていると思う1から10までのカードの番号を入力してください。"
input = gets.chomp.to_i
if input >=1 && input<=10
break
else
one_ten_message
end
end
puts "#{self.name}「#{input}番」"
gets.chomp
if input == player.card[0] && input == 10
puts "#{player.name}の持っていたのは、#{deck.type[input]}のカードでした。"
gets.chomp
puts "#{player.name}は#{deck.type[input]}の効果により、#{deck.type[player.card[0]]}のカードを捨て、転生札よりカードを引いて復活します。"
player.dis_card << player.card[0]
player.card[0] = deck.reincarnation
elsif input == player.card[0]
puts "#{player.name}「参りました」"
gets.chomp
puts "#{player.name}が持っていたのは、#{player.card[0]}番の#{deck.type[player.card[0]]}のカードでした。"
puts "#{self.name}の勝ちです。"
self.victory = true
else
puts "#{player.name}「違います」"
gets.chomp
end
end
def emperor(deck, player)
if deck.card.empty?
none_effect_message
else
puts "#{player.name}がカードをドローし、持っているカードをオープンします。"
player.card[1] = deck.card.delete_at(0)
while true
puts "#{player.name}が持っているのは、#{player.card[0]}番の#{deck.type[player.card[0]]}と#{player.card[1]}番の#{deck.type[player.card[1]]}のカードです。"
puts "#{self.name}の手札にあるのは、#{self.card[0]}番の#{deck.type[self.card[0]]}のカードです。"
question_dis_card_message
dis_card_choice_message
puts "[#{player.card[0]}]#{deck.type[player.card[0]]}のカードを捨て札に送る。"
puts "[#{player.card[1]}]#{deck.type[player.card[1]]}のカードを捨て札に送る。"
input = gets.chomp.to_i
if player.card.any?(input)
break
elsif input == 11
self.dis_card_print(deck,player)
else
puts "#{input}番のカードは#{player.name}の手札にありません。"
end
end
puts "#{player.name}の#{deck.type[input]}のカードを捨て札に送りました。"
gets.chomp
player.dis_card << player.card.delete_at(player.card.find_index(input))
if input == 10
puts "#{player.name}「参りました」"
gets.chomp
puts "#{deck.type[9]}の効果により、#{deck.type[input]}は転生できないため#{self.name}の勝利です。"
self.victory = true
end
gets.chomp
end
end
def dis_card_print(deck, player)
puts "#{self.name}の使用ずみカード"
if self.dis_card.empty?
none_card_message
else
self.dis_card.each do |dis|
puts "#{dis}番:#{deck.type[dis]}"
end
end
puts "#{player.name}の使用ずみカード"
if player.dis_card.empty?
none_card_message
else
player.dis_card.each do |dis|
puts "#{dis}番:#{deck.type[dis]}"
end
end
line
end
def duel(deck, player)
open_card_message
puts "#{self.name}が持っていたのは#{self.card[0]}番の#{deck.type[self.card[0]]}のカードです。"
gets.chomp
puts "#{player.name}が持っていたは#{player.card[0]}番の#{deck.type[player.card[0]]}のカードです。"
gets.chomp
if self.card[0] == player.card[0]
compensating_message
elsif self.card[0] > player.card[0]
self.victory = true
puts "#{player.name}「参りました」"
gets.chomp
puts "#{self.name}の勝ちです。"
else self.card[0] < player.card[0]
player.victory = true
puts "#{self.name}の負けです。"
end
gets.chomp
end
def question_continue_print(game_result)
while true
puts "#{self.name}の成績は#{game_result.myplayer}勝#{game_result.pcplayer}敗#{game_result.compensating}分です。"
question_continue_message
input = gets.chomp.to_i
if input == 1
restart_message
break
elsif input == 2
game_result.continue = false
break
else
one_two_message
end
end
end
end
#Myplayerクラスの解説
私たちが操作するプレイヤーのクラスです。
番号を選択する必要があるところは、基本的にmyplayerクラスでメソッドを定義しています。
require "./player"
require "./message"
class Pcplayer < Player
include Message
def wiseman_draw(deck)
self.card[1] = deck.card.delete_at(deck.card.first(3).find_index(deck.card.first(3).max))
deck.card.shuffle!
puts "#{self.name}が#{deck.type[7]}の効果により、カードを3枚ドローし1枚を選択して手札に加えました。"
shuffle_message
self.wiseman = false
end
def draw(deck)
self.card[1] = deck.card.delete_at(0)
puts "#{self.name}は山札からカードをドローしました。"
end
def choice(deck)
input = self.card.delete_at(self.card.find_index(self.card.min))
self.dis_card << input
puts "#{self.name}が#{input}番の#{deck.type[input]}のカードを使用しました。"
return input
end
def boy(deck, player)
if deck.card.empty?
none_effect_message
elsif (self.dis_card + player.dis_card).count(1) == 2
puts "#{deck.type[1]}のカードが使われたのは2枚目のため、効果を発動します。"
player.card[1] = deck.card.delete_at(0)
puts "#{deck.type[1]}の効果により、#{player.name}は山札から#{deck.type[player.card[1]]}のカードをドローして公開します。"
player.dis_card << input = player.card.delete_at(player.card.find_index(player.card.max))
puts "#{self.name}が#{player.name}の手札にある#{deck.type[input]}のカードを捨て札に送りました。"
if input == 10
puts "#{deck.type[input]}の効果により、#{player.name}は#{player.card[0]}の#{deck.type[player.card[0]]}のカードを捨て、転生札よりカードを引いて復活します。"
player.dis_card << player.card[0]
player.card[0] = deck.reincarnation
end
else
puts "#{deck.type[1]}のカードは初めて使われたため、効果を発動しません。"
end
end
def soldier(deck, player)
puts "#{deck.type[2]}の効果により、#{self.name}がカードの番号を宣言します。"
input = (deck.card + player.card).sample
gets.chomp
puts "#{self.name}「#{input}番」"
gets.chomp
if input == 10 && input == player.card[0]
puts "#{player.name}の持っていた#{deck.type[input]}のカードを捨て札に送りました。"
gets.chomp
puts "#{deck.type[input]}の効果により、転生札よりカードを引いて復活します。"
player.dis_card << player.card[0]
player.card[0] = deck.reincarnation
elsif input == player.card[0]
puts "#{player.name}が持っていたのは、#{deck.type[player.card[0]]}のカードでした。"
gets.chomp
puts "#{player.name}の負けです。"
self.victory = true
else
puts "#{player.name}「違います」"
gets.chomp
end
end
def emperor(deck, player)
if deck.card.empty?
none_effect_message
else
player.card[1] = deck.card.delete_at(0)
puts "#{deck.type[9]}の効果により、#{player.name}は山札からカードをドローしてオープンします。"
gets.chomp
input = player.card.max
puts "#{self.name}が#{input}番の#{deck.type[input]}のカードを捨て札に送りました。"
gets.chomp
player.dis_card << player.card.delete_at(player.card.find_index(input))
if input == 10
puts "#{deck.type[9]}の効果により、#{deck.type[10]}は転生できないため#{self.name}の勝ちです。"
self.victory = true
gets.chomp
end
end
end
end
Pcplayerのクラスの解説
コンピューターのプレイヤーのクラスです。
カード名をふせたい場合など、プレイヤーと記述を分けたい場合はpcplayerクラスにメソッドを記述しています。
require "./deck"
require "./player"
require "./myplayer"
require "./pcplayer"
require "./game_result"
require "./message"
include Message
game_result = Game_result.new
start_message
while game_result.continue
deck = Deck.new
myplayer = Myplayer.new(deck.card.delete_at(0), "あなた")
pcplayer = Pcplayer.new(deck.card.delete_at(0), "相手")
while true
my_turn_message(myplayer)
myplayer.guard = false
if deck.card.empty?
none_deck_message
myplayer.duel(deck, pcplayer)
break
elsif myplayer.wiseman == true
myplayer.wiseman_draw(deck, pcplayer)
else
myplayer.draw(deck)
end
line
input = myplayer.choice(deck, pcplayer)
line
if pcplayer.guard == true && input != 4 && input != 7
pcplayer.guard_method(deck, input, myplayer)
else case input
when 1
myplayer.boy(deck, pcplayer)
when 2
myplayer.soldier(deck, pcplayer)
break if myplayer.victory == true
when 3
myplayer.fortune_teller(deck, pcplayer)
when 4
myplayer.maiden(pcplayer)
when 5
myplayer.death_god(deck, pcplayer)
when 6
myplayer.duel(deck, pcplayer)
break
when 7
myplayer.wiseman_method
when 8
myplayer.spirit(deck, pcplayer)
when 9
myplayer.emperor(deck, pcplayer)
break if myplayer.victory == true
end
end
line
pc_turn_message(pcplayer)
pcplayer.guard = false
if deck.card.empty?
none_deck_message
myplayer.duel(deck, pcplayer)
break
elsif pcplayer.wiseman == true
pcplayer.wiseman_draw(deck)
else
pcplayer.draw(deck)
end
line
input = pcplayer.choice(deck)
if myplayer.guard == true && input!= 4 && input!= 7
myplayer.guard_method(deck, input, pcplayer)
else case input
when 1
pcplayer.boy(deck, myplayer)
when 2
pcplayer.soldier(deck, myplayer)
break if pcplayer.victory == true
when 3
pcplayer.fortune_teller(deck, myplayer)
when 4
pcplayer.maiden(myplayer)
when 5
pcplayer.death_god(deck, myplayer)
when 6
myplayer.duel(deck, pcplayer)
break
when 7
pcplayer.wiseman_method
when 8
pcplayer.spirit(deck, myplayer)
when 9
pcplayer.emperor(deck, myplayer)
break if pcplayer.victory == true
end
end
line
end
line
if myplayer.victory == true
game_result.myplayer += 1
elsif pcplayer.victory == true
game_result.pcplayer += 1
else
game_result.compensating += 1
end
myplayer.question_continue_print(game_result)
end
end_message
xeno.rbのファイルの解説
game_resultのインスタンスを最初に定義しているのは、continue時にデータを初期化されないようにするためです。myplayer,pcplayer,deckのインスタンスは、コンティニュー時に全て初期化されるようにプログラムを組んでいます。
「あなた」と「相手」の文章を書き換えることで、プレイヤー名の表示を全て書き換えることもできます。
左側がファイルの構成で、右側がターミナルでプレイしてる時の画像です。
適当なフォルダをデスクトップに作って、そのフォルダのなかに画像と同じようにファイルを7つ作ればプレイできます。
実際にターミナルでプレイする時は、カレントディレクトリをxenoのファイルがある場所に移動させて、ターミナルで「ruby xeno.rb」とコマンドを打ち込むと遊ぶことができます。
(注)RubyはMacBookのパソコンでは標準でインストールされていますが、windowsのパソコンでは標準でインストールされていないようです。
「ruby -v」とターミナルに打ち込んで(-vはversionの略です)Rubyのバージョンが表示されなければ、Rubyをインストールする必要があります。
https://techacademy.jp/magazine/7056
rubyをwindowsでインストールするには、こちらのサイトが参考になるかと思います。
プレイしてみてよかったと思ったら、LGTMやコメントを頂けると嬉しいです。