VB.NETでビンゴゲーム作成

タグの編集
投稿者 じょう  (学生) 投稿日時 2010/4/26 10:43:00
勉強のためVB.NETでビンゴゲームを作成したいと思っているのですが
サンプルが見つけられなかったのでご存知の方教えてください。

作りたいモノとしては
①スタートボタンで数字が回転(『数字が回転』の部分の作成方法が分かりません)
②ストップボタンで数字が止まる(できれば3秒くらい廻ったら勝手に止まる仕様にしたい)
③既出の数字は省きたい。

以上です。
よろしくお願いします。

開発環境:visualstudio2005
投稿者 じょう  (学生) 投稿日時 2010/4/26 11:56:45
質問の①は解決しました。
十分に調べたつもりでしたが、調べ方を変えたら簡単に見つかりました。
失礼しました。

残りの②、③については引き続きよろしくお願い致します。
投稿者 るきお  (社会人) 投稿日時 2010/4/26 13:03:25
こんにちは。

「勉強のため」ということは、ずばりな方法ではなく、
ヒントを小出しにしていく方が良いでしょうか?

②、③のプログラム方法は①によって変わってくると思いますので、
①のプログラムを貼り付けることはできますか?

私のイメージでは、
スロットマシーンのようなイメージ、数字が上から下に
すごいスピードで回るものです。

①の話になってしまいますが、
この場合は、数字がたてに並んでいる長細い画像を用意して、
プログラムではその画像の座標(0, y)から座標(表示部分の横幅, y + 表示部分の縦幅)
を抜き出して表示するというものです。
yを少しずつカウントアップすることで、見た目にはスロットが回転しているように見えます。
少しずつカウントアップさせるにはTimerを使用するとよいと思います。

②は停止ボタンを押したらyのカウントアップを中止するか、押してから3秒たった時点でカウントアップを中止する、または、①を開始して3秒経ってからカウントアップを中止することで実現できます。

このとき、画像が停止した位置にある数値をプログラムから簡単取得できるように、
yと画像の数値の位置をプログラムや設定ファイルなどで関連付けておくと良いです。

③はこの方式だとちょっと厳しいんですが、
あらかじめ用意しておいた縦長の画像から、②で登場した数値部分をカットする画像を
つぎつぎと生成すればいけると思います。

なお、回転のアニメーション効果は不要で、単に数字が次々と表示されればよいという
レベルならもっとずっと簡単にできます。
今回、『数字が回転』と強調してあったのでこのような回答にしました。
投稿者 じょう  (学生) 投稿日時 2010/4/26 13:42:01
はじめまして。

>「勉強のため」ということは、ずばりな方法ではなく、
>ヒントを小出しにしていく方が良いでしょうか?
そうですね。お手数お掛けしてしまって申し訳ないんですけど、
「ここが間違っているから、これ調べてみな?」とか言っていただけると嬉しいです。

とりあえず自力で作成した部分を貼ります。

Private Sub btnStart_Click(ByVal sender As System.Object, ByVal e As                                  System.EventArgs) Handles btnStart.Click
   Randomize()
   Me.Timer1.Enabled = True
End Sub

Private Sub Timer1_Tick(ByVal sender As Object, ByVal e As System.EventArgs)                                          Handles Timer1.Tick
   Me.Label1.Text = Int(100 * Rnd())

   Timer1.Tag = Timer1.Tag - 1
   If Timer1.Tag <= 0 Then
       Timer1.Enabled = False
   End If

End Sub

以上です。

TimerのIntervalを1、Tagを50に設定してみました。

すると1回目にbtnStartを押したときは想定通りに止まるんですけど
2回目に押すと、Label1に表示されている数字がパっと変わってしまいます。
(再度、数字が回転して止まって欲しいです。)

Tagの使い方を間違えているのでしょうか?
(「数字が50回変わったら止まる」と理解しました。)
投稿者 魔界の仮面弁士  (社会人) 投稿日時 2010/4/26 15:23:08
> TimerのIntervalを1、Tagを50に設定してみました。
> すると1回目にbtnStartを押したときは想定通りに止まるんですけど
1回目にタイマーが開始された時に、Tag の値は『50』から開始されますよね。


> 2回目に押すと、Label1に表示されている数字がパっと変わってしまいます。
そしてタイマーが止まるのは、以下の条件を通過した時です。
   If Timer1.Tag <= 0 Then
       Timer1.Enabled = False
   End If


すなわち、この処理が完了したとき Tag の中身は『0以下』になっているわけです。
具体的には 0 という値ですね。

では 2 回目を開始するときに、この Tag の値は何か別の値に変更していますか?
それとも 0 のままですか?


> (「数字が50回変わったら止まる」と理解しました。) 
Tag を 50 から始めれば、Tag ≦ 0 になるまでの 50 回は数字が変わりますね。

Tag を  2 から始めれば、2 回だけ実行され、Tag の中身は  0 になりますし、
Tag を  1 から始めれば、1 回だけ実行され、Tag の中身は  0 になりますし、
Tag を  0 から始めれば、1 回だけ実行され、Tag の中身は -1 になりますし、
Tag を -1 から始めれば、1 回だけ実行され、Tag の中身は -1 になります。
投稿者 じょう  (学生) 投稿日時 2010/4/26 15:46:52
はじめまして。

>すなわち、この処理が完了したとき Tag の中身は『0以下』になっているわけです。
本当でした。
1回目にタイマーが止まったときのTimer1.Tagは0,0(Double)で
2回目は-1,0となっていました。
自分で書いておきながらタイマーが止まる条件を見落としていました。

そこでbtnStart_ClickイベントのMe.Timer1.Enabled = True前に
Me.Timer1.Tag = 50
と常に50からスタートするようにしたら、何度押しても想定通りに止まるようになりました!

あとは③をどう実装するかです。

「タイマーが止まった時点の数字をListなどに詰めていって、既にListに含まれている数字はタイマーを止めない」としようかと考えていますが、現実的ではない気がします。
50回数字が変わって51回目の数字がLabel1に表示されているとするならば
その数字がまた51回目の数字にならないという保障はないですよね?

何か良いヒントがあれば教えてください。
投稿者 魔界の仮面弁士  (社会人) 投稿日時 2010/4/26 19:56:23
> Tag を -1 から始めれば、1 回だけ実行され、Tag の中身は -1 になります。 
失礼しました。-1 から始めたら、-2 ですね。


> そこでbtnStart_ClickイベントのMe.Timer1.Enabled = True前に
Timer の停止/再開は Enabled プロパティでも行えますが、
Stop / Start メソッドを使った方が、意図が分かりやすいコードになると思いますよ。

また乱数生成も、Randomize / Rnd を使うのではなく、Random クラスを使った方が良いかと。



> 「タイマーが止まった時点の数字をListなどに詰めていって、既にListに含まれている数字はタイマーを止めない」としようかと考えていますが、現実的ではない気がします。
そういう方法でも実装できますが、実際のビンゴゲームと同様のロジックの方が都合が良いでしょう。
http://www.adroom.co.jp/wp-content/uploads/383_1.jpg


ビンゴゲームだと分かりにくければ、「トランプ」を思い浮かべてみて下さい。
トランプには 53種類のカードがありますよね。

その中から、好きな位置にあるカードを一枚を抜いてもらいます。
次に、残った52枚から、さらにもう一枚カードを引きます。
その次は、残った 51 枚から…と順に抜いていくイメージです。

この場合、同じカードが2回続けて引かれる心配はありませんよね。
それと同様の方法にすれば良いわけです。


たとえば 0~9 の 10 個の値を使うのだとすれば、
List に 0~9 の連番を記録しておきます。これがトランプのかわりです。
(実際には連番で無くとも良いですが、とにかくすべて別の値にしておきます)

最初、List の中身は 0,1,2,3,4,5,6,7,8,9 という並びです。
0番目の位置には 0 という値、9番目には 9 という値が入っています。



さて、最初のリストの数は 10 個なので、0~9 の範囲で乱数を生成します。

もしも乱数が 8 という値だったら、8番目にある「8」を取り出します。
もしも乱数が 4 という値だったら、4番目にある「4」を取り出します。
もしも乱数が 0 という値だったら、0番目にある「0」を取り出します。

タイマーが止まった場合には、その位置の値を RemoveAt で削除します。たとえば、
3 番目の位置を削除すれば、List の中身は 0,1,2,4,5,6,7,8,9 になりますし、
7 番目の位置を削除すれば、List の中身は 0,1,2,3,4,5,6,8,9 になるわけです。


2回目は、リストが 9 個に減ったので、0~8 の範囲で乱数を生成します。
得られた乱数を List 内の位置として、同様に値を取り除いていきます。

以前に選ばれた数字は、既に List 内からは取り除かれていますので、
何番目の値を取り出しても、同じ数字が出現する心配はありませんね。
投稿者 じょう  (学生) 投稿日時 2010/4/27 13:18:53
こんにちは。

トランプのイメージ分かりやすかったです!

昨日の夜に早速作ってみましたが、
>2回目は、リストが 9 個に減ったので、0~8 の範囲で乱数を生成します。
の実装方法で止まっています。

http://homepage1.nifty.com/rucio/main/dotnet/ClassLibrary/L002_System.Random.htm
こちらのページを参考にしているのですが、
Randomクラスを使用する場合、Nextメソッドに(今回でいう)Listを引数にすることは
できるのでしょうか?
もし出来れば、Listにある数字から乱数を取得とすれば
>その中から、好きな位置にあるカードを一枚を抜いてもらいます。
>次に、残った52枚から、さらにもう一枚カードを引きます。
>その次は、残った 51 枚から…と順に抜いていくイメージです。
>この場合、同じカードが2回続けて引かれる心配はありませんよね。
このイメージ通りの実装が可能だと思ったのですが。


とりあえず今日の夜にまた出来た分のソースをアップするので、
お時間があればまた宜しくお願い致します。


>Timer の停止/再開は Enabled プロパティでも行えますが、
>Stop / Start メソッドを使った方が、意図が分かりやすいコードになると思いますよ。
そうなんですね!
Enabledを使っても動きとしては同じなんでしょうか?

>また乱数生成も、Randomize / Rnd を使うのではなく、Random クラスを使った方が良いかと。
これも自分で調べて思ったことなので自信がないんですが、VB6.0での書き方なのでしょうか?

「Randomizeを使用しない場合、常に同じパターンで乱数が取得されてしまう。」
ということは分かったのですが、それ以外に大きな問題があるのでしょうか?
もう少し調べてみたいと思います。

投稿者 るしぇ  (社会人) 投稿日時 2010/4/27 16:40:32
> Randomクラスを使用する場合、Nextメソッドに(今回でいう)Listを引数にすることは
> できるのでしょうか?
> もし出来れば、Listにある数字から乱数を取得とすれば
Listにある数字じゃなくて、Listに残ってる数字の個数だけ分かればいいでしょ?

1と3と6が残っていても、
2と3と5が残っていても、
残っている数字の個数は3個。

1から3までの数値から Random で取得するだけ。
投稿者 魔界の仮面弁士  (社会人) 投稿日時 2010/4/27 21:40:25
> Listにある数字じゃなくて、Listに残ってる数字の個数だけ分かればいいでしょ?
すなわち、List クラスの Count プロパティですね。
それを Random クラスの Next(Integer) メソッドの引数に渡してやれば OK です。

> 残っている数字の個数は3個。
> 1から3までの数値から Random で取得するだけ。
1~3 ではなく、0~2 の範囲で取得した方が都合が良いと思いますよ。
投稿者 (削除されました)  () 投稿日時 2010/4/28 11:40:30
(削除されました)
投稿者 じょう  (学生) 投稿日時 2010/4/28 12:03:27
るしぇさん、はじめまして。

私、まだ本当に.NETの勉強始めたばっかりで皆さんにとって常識なことでも
全然分からないんです。
なのでどうしてそうなるのか・・・というアプローチ方法を示してもらえると助かります。

魔界の仮面弁士さん

遅くなってしまいましたが、作成した部分のソースを貼ります。

Public Class Form1

    Dim list As New List(Of Integer)

    Private Sub btnStart_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles btnStart.Click
        Me.Timer1.Tag = 50
        Me.Timer1.Start()
    End Sub

    Private Sub Timer1_Tick(ByVal sender As Object, ByVal e As System.EventArgs)Handles Timer1.Tick
        Dim MyRandom As New Random
        Me.Label1.Text = MyRandom.Next(list.Count)

        Timer1.Tag = Timer1.Tag - 1
        If Timer1.Tag <= 0 Then
            Timer1.Stop()

            list.Remove(Me.Label1.Text)
        End If
    End Sub

    Private Sub Form1_Load(ByVal sender As Object, ByVal e As System.EventArgs)Handles Me.Load
        For i As Integer = 1 To 10
            list.Add(i)
        Next
    End Sub
End Class

この実装だと、既出の数字も出てしまいます。
Nextの引数の渡し方が問題なのかと思いますが・・・

>Listに残ってる数字の個数だけ分かればいいでしょ?
リストに残っているのが1と3と5でもリストの個数(3)を引数にすると「0~2までのランダムな整数を取得する」となりませんか?
そうすると既出でリストには残ってない数字「2」がでてしまうんです・・・

一生懸命色々な情報を見て考えたんですけど、「リストの個数を指定する」という考え方がわかりません・・・

またアドバイスよろしくお願いします。
投稿者 (削除されました)  () 投稿日時 2010/4/28 13:21:10
(削除されました)
投稿者 るしぇ  (社会人) 投稿日時 2010/4/28 13:26:18
えーと、魔界の仮面弁士さんのコメントに一通り答えが載っているので^^;
何度も読み直して理解してくださいね。

別にプログラムの話じゃないので、小学生のなぞなぞが解けるかといったレベル?
考え方は何ら難しくないです。分かってみれば普通のことです。日常生活で
自分が考えている手順の通りプログラムすればできます。

リストに入っている数値は一切見なくてもできます。
現実問題としてはトランプを裏返しのまま一切おもての数字を見ないで
実現することを考えてください。

適当にシャッフルして裏返したままトランプを1列に並べます。
1から52までの中から好きな数字を言います。
トランプの列から、上の数字"番目"のカードを抜きます。
次に1から51までの中から好きな数字を言います。
トランプの列から、上の数字"番目"のカードを抜きます。
・・・最後の1枚が無くなるまで繰り返します。
おもての数字に関係なく実現できるでしょう?
プログラムの答えはたくさんあるので、一例として。


Public Class Form1
    Dim list As New List(Of Integer)
    Const ListMax As Integer = 52

    Private Sub btnStart_Click(ByVal sender As System.ObjectByVal e As System.EventArgs) Handles btnStart.Click
        Debug.Print("Start")
        '毎回リスト作成 
        For i As Integer = 1 To ListMax
            list.Add(i)
        Next
        Me.Timer1.Start()
    End Sub

    Private Sub Timer1_Tick(ByVal sender As ObjectByVal e As System.EventArgs) Handles Timer1.Tick
        Dim MyRandom As New Random
        Dim ListIndex As Integer
        ListIndex = MyRandom.Next(list.Count - 1) '何番目? 
        Me.Label1.Text = list.Item(ListIndex).ToString 'ListIndex番目の数字を表示。 
        Debug.Print(Me.Label1.Text) 'ラベルの表示は残らないので、イミディエイトウィンドウに出力 
        list.RemoveAt(ListIndex) '表示した数字はリストから削除。Remove でなく、RemoveAt を使っているので注意 
        If list.Count <= 0 Then
            'リストに何も無くなったら終了 
            Timer1.Stop()
            Debug.Print("Stop")
        End If
    End Sub

End Class

投稿者 魔界の仮面弁士  (社会人) 投稿日時 2010/4/28 14:01:29
> 「Randomizeを使用しない場合、常に同じパターンで乱数が取得されてしまう。」
> ということは分かったのですが、それ以外に大きな問題があるのでしょうか?
主な理由としては、「n 以上 m 未満」という範囲の乱数を生成するのに都合が良いからです。

たとえば、『1000~9999 の範囲でランダムな番号を得たい』というケースにおいては、
Random クラスの方が扱いやすいです。Next メソッドで生成範囲を調整することできるからです。

一方 Rnd の方は、常に 0.0 以上 1.0 未満の値しか出力できないため、
範囲指定する際に、そこからさらに追加の計算処理が必要になってしまいます。

また、Rnd 関数よりも Random クラスの方の方が数値の偏りが少なくなるという利点もあります。 



> Me.Label1.Text = MyRandom.Next(list.Count)

この部分が間違っています。

今回の Random の Next メソッドから得た値というのは、『ビンゴの玉に書かれた番号』や
『トランプの内容(4種のA,2,3,4,5,6,7,8,9,10,J,Q,K、およびジョーカー)』を
表しているわけではありません。

そうではなく、『トランプの山札で、上から何番目のカードかを表す値』を意味しています。
これはすなわち、list 変数内の位置を表す番号であるということです。

たとえば、Next メソッドで得た乱数を変数 n に格納していたとすれば、
カードの内容(ビンゴの玉に書かれた番号)は「list(n)」として得る事ができますし、
カードを抜く(ビンゴの玉を除去する)作業は、「list.RemoveAt(n)」で行えます。
投稿者 じょう  (学生) 投稿日時 2010/4/28 23:25:50
あ~!!なるほど!
トランプは裏返っていたのですね。

乱数で取得するのは「表の数字」じゃなくて、「何番目のトランプを引くかの数字」だったと。
だからCountを使うわけだ・・・

最初に魔界の仮面弁士さんに教えてもらった"番目"の意味をきちんと理解していませんでした。
(RemoveAtも同様ですね。)

もうチョットでビンゴの核とするところは完成できそうです。

現在のソースはこうなりました。
(他は変更なしです。)

Private Sub Timer1_Tick(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Timer1.Tick
   Dim myRnd As New Random
   Dim index As Integer = myRnd.Next(list.Count - 1)
   Me.lblNum.Text = list(index).ToString

   Timer1.Tag = Timer1.Tag - 1
   If Timer1.Tag <= 0 Then
      Timer1.Enabled = False
      Me.list.RemoveAt(index)
   End If
End Sub

ちなみにですが、8回目から回転しないように見えるんですけど
それはList内の個数が減っているからと考えて良いですか?


あと、1コ実現したいことがあります。

出た数字がフォーム上に残るようにしたいんです。
フォームにラベルを10個貼りました。
乱数で取得した数字(上のソースで言うindex)とフォーム上のラベルをどうにかして
結び付けられないかと考えています。

度々申し訳ないですが、またアドバイスお願いします。

魔界の仮面弁士さん、
Randomize/Rndの説明ありがとうございます。
覚えておきます。
投稿者 魔界の仮面弁士  (社会人) 投稿日時 2010/4/29 22:39:45
当然ながら、List の残数が少なくなるほど、数字の切り替わりも鈍くなっていきます。
(トランプの枚数/ビンゴの玉が残り一つとなれば、その値は確定してしまいますしね)

また、トランプの山札/ビンゴの玉数よりも多い回数の抽選もできませんから、
プログラム上では、抽選回数に制限を持たせることも忘れてはいけません。

そうしたことも考えと、最初に用意しておく List 内の個数は、少し多めに用意しておいた方が
良いかと思います。そもそもビンゴゲームの場合、トーナメントのくじ引き等とは異なり、
最後の一個まで抽選を繰り返すことは稀でしょうしね。


> 乱数で取得した数字(上のソースで言うindex)とフォーム上のラベルをどうにかして
> 結び付けられないかと考えています。
Label を配列に入れておくと良いでしょう。そうすれば、1回目の抽選では最初のラベルにセットし、
2回目の抽選ではその次のラベルにといった処理も記述しやすくなります。

# この『回数』は、乱数で得た index や list(index) で得た値とは別物である事に注意。


なお、乱数(Rnd/Random)を使ったこの手の処理については、過去にも話題になっていますので、
過去ログも参照してみると、新たな発見があるかもしれませんよ。


> Dim index As Integer = myRnd.Next(list.Count - 1)
ここで、myRnd.Next(5) となった場合、それは「0以上5未満の値」すなわち 0~4 を得る事に
なるわけですが、その点は大丈夫でしょうか?
http://msdn.microsoft.com/ja-jp/library/zd1bc8e5%28v%3dVS.90%29.aspx

たとえば、list の中身が「3,7,9」の3個だった場合、「list.Count - 1」は 2 を意味しますので、
myRnd.Next(list.Count - 1) からは、0 または 1 のいずれかしか返されない事になります。
という事は、「3,7,9」からは 0 番目の値である「3」と、1番目の値である「7」は得られても、
最後の2番目の値となる「9」は得られない事になってしまうのです。


つまり list 内の最後の位置も乱数に含まれるようにするためには、
.Count - 1 ではなく、.Count そのものを渡さねばならないということです。

先の投稿に記述された
> すなわち、List クラスの Count プロパティですね。
> それを Random クラスの Next(Integer) メソッドの引数に渡してやれば OK です。
および
> 主な理由としては、「n 以上 m 未満」という範囲の乱数を生成するのに都合が良いからです。
という内容が指し示す意味を、もう一度確認しておいてください。
投稿者 じょう  (学生) 投稿日時 2010/5/2 14:02:43
Labelの配列、ちょっと難しくってきちんと理解できているか自信がありませんが
自分なりに考えて作ってみました。
(labelsがそれに当たります)

とりあえずは自分が想定している動きにすることができました。
これからステップ実行などしながら本当に正しいか検証してみたいと思います。

今後どなかたの参考になればと思うので、全てのソースを貼ります。

Public Class frmBINGO

    Dim list As New List(Of Integer)
    Dim index As Integer
    Dim labels As New ArrayList

    Private Sub Form1_Load(ByVal sender As Object, ByVal e As System.EventArgs) Handles Me.Load
        For i As Integer = 1 To 75
            list.Add(i)
        Next

        For j As Integer = 1 To 75
            labels.Add("Label" & j.ToString)
        Next
    End Sub

    Private Sub btnStart_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles btnStart.Click
        Me.Timer1.Tag = 50
        Me.Timer1.Start()
    End Sub

    Private Sub Timer1_Tick(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Timer1.Tick
        Dim myRnd As New Random
        index = myRnd.Next(list.Count)
        Me.lblNum.Text = list(index).ToString

        Timer1.Tag = Timer1.Tag - 1
        If Timer1.Tag <= 0 Then
            Timer1.Enabled = False
            Me.list.RemoveAt(index)

            Dim label As Label = Me.Controls(labels(Me.lblNum.Text - 1))
            label.Visible = True
            Dim newFont As Font = New Font(label.Font.FontFamily, 20, label.Font.Style)
            label.Font = newFont
            label.Text = Me.lblNum.Text

        End If
    End Sub
End Class

最初に「勉強のため」と申しましたが、実はもう一つ目的がありました。
今度友人が結婚をすることになりまして、その2次会のパーティーでビンゴをするので
それ用に作ってプレゼントをしようと考えていました。
(間に合わなければフリーソフトを使用するつもり。)

ここで色々なアドバイスをいただけたおかげでどうにか間に合いそうです。

魔界の仮面弁士さん、るしぇさん、るきおさん
お忙しい中最後までお付き合いいただきまして、本当にありがとうございました。