スロットマシンを作る (5)

作りやすそうなところをまず着手。

spin ボタンを何度も押せてしまうという現象があり、これはまずい。 @State 変数 isSpin を作って、ドラムの回転状況を保存。 初期値は false すなわち回転していない。 false の時は spin ボタンを押せるように、.disable() に記載。 また、stop ボタンも同様に、isSpin の状態に応じて色をかえる。

変数 isSpin の値を変えるのは spin ボタン及び stop ボタンの method が起動したとき。

という考えでソース改訂。うまくいった。

import SwiftUI

struct ContentView: View {
    // 定数なので let 変数なら var
    static let aMinimumCharacterSet = [ "🍒", "🍉", "🩷", "🔔"]
    var aReelOne: [ String ] {
        ContentView.aMinimumCharacterSet + ContentView.aMinimumCharacterSet
    }
    
    static let symbolHeight: CGFloat = 120.0

    private var initialYOffset: CGFloat {
        ( CGFloat( aReelOne.count ) - 3 ) * ContentView.symbolHeight / 2 * -1
    }

    @State private var yOffset: CGFloat = 0
    // ? は optional 型指定、nil 許容。覚えにくいなぁ
    @State var timer: Timer? = nil
    
    // private: そこでしか使えない
    // static: そのクラスでしか使えない
    @State var isSpin = false
    

    
    var body: some View {

        VStack( spacing: 40 ) {
            Text( "スロットマシーン" )
                .padding()
            ZStack {
                // 背景の黒い枠(3コマ分の高さ)
                RoundedRectangle( cornerRadius: 20 )
                    .fill( Color(white: 0.15 ))
                    .frame( width: 160, height: 360 )
                    .overlay(
                        VStack {
                            Spacer()
                            Rectangle()
                                .fill( Color.red.opacity( 0.4 ))
                                .frame( height: 4 )
                            Spacer()
                        }
                    )
                    .overlay(
                        RoundedRectangle( cornerRadius: 20 )
                            .stroke( Color.yellow, lineWidth: 5 )
                    )

                VStack( spacing: 0 ){
                    ForEach( 0 ..< aReelOne.count, id: \.self ){ index in
                        Text( aReelOne[ index ] )
                            .font( .system( size: 75 ))
                            .frame( width: 160, height: ContentView.symbolHeight, alignment: .center )
                    }
                }
                .onAppear { yOffset = initialYOffset }
                .offset( y: yOffset )

            } //ZStack
            .frame( width: 160, height: 360 )
            .clipped()

            Text( "状態" )
                .font( .headline )
                .foregroundColor( .black )
                .frame( height: 30 )
                .padding()
        }
        
        HStack( spacing: 30 ) {
            Button( action: { startSpin() }) {
                Text( "Spin" )
                    .font( .headline ).bold()
                    .padding()
                    .frame( width: 140 )
                    .background( isSpin ? Color.gray : Color.yellow )
                    .foregroundColor( .black )
                    .cornerRadius( 10 )
            }
            .disabled( isSpin )
            
            Button( action: { stopSpin() }) {
                Text( "Stop" )
                    .font( .headline ).bold()
                    .padding()
                    .frame( width: 140 )
                    .background( isSpin ? Color.yellow : Color.gray )
                    .foregroundColor( .black )
                    .cornerRadius( 10 )
            }
            .disabled( !isSpin )
        }  // HStack
    } // body

    func startSpin() {
        timer = Timer.scheduledTimer( withTimeInterval: 0.01, repeats: true ) {
            _ in
            yOffset += 10
            if yOffset > ContentView.symbolHeight {
                yOffset = initialYOffset - 40
            } // end of if
        } // end of timer
        isSpin.toggle()
    } // end of startSpin()
    
    func stopSpin(){
        timer?.invalidate()
        timer = nil
        isSpin.toggle()
    } // end of stopSpin()
    
} // struct


#Preview {
    ContentView()
}

このほか更新が必要なのは次の通り。

  • ドラムの絵柄と回転スピードを本物に合わせる。ガイドラインがあるはずなので調べよう。
  • 絵柄がぴったりの位置で止まらず、途中の位置で止まる。実物のスロットマシーンは途中で止まらないので改良が必要。
  • 当たりフラグが立たないと特定の絵柄を止められない。例えばチェリーフラグが立って初めてチェリーを止めることができる。当たりフラグが立っていないならドラムを滑らせ表示外にする。

スロットマシンを作る (4)

今回はドラムを回転させる。 回転させるには Timer.scheduledTimer を使う。 Timer.scheduledTimer はかなり難解な構文だ。 引数は 2 つ。withTimeInterval: と repeats: だ。

withTimerInterval はタイマーの動作時間のこと。今回は絵を下に 10 ずつずらす。 大きい数字を指定するとゆっくりになる(ゆっくり 10 下がる)。短くすると素早くなる(素早く 10 下がる)。 試験してみたところ、0.01 がちょう度良かったので指定。

repeats は繰り返すかどうか。繰り返したいので true を指定。

次に、{ _ in ...} の表記。これ、初見?となった。 { ... } は Timer 内で処理される内容を書く所なんだけど、自分自身を受け取る変数名を指定できる。 { ... } 内で Timer を止めたい、という場合、受ける変数がないと書けないので、変数を置く。 timer in とするのが自分としては分かりやすいけど、runninngTimer in のように書く人もいるそうだ。 別に変数を受け取りたくない、という場合は _ を指定。

回転させたら止めないと。 止めるのはタイマーを無効化する。 stop ボタンに method 追加。 timer.invalidate() でタイマー無効、ドラムは止まる。

import SwiftUI

struct ContentView: View {
    // 定数なので let 変数なら var
    static let aMinimumCharacterSet = [ "🍒", "🍉", "🩷", "🔔"]
    var aReelOne: [ String ] {
        ContentView.aMinimumCharacterSet + ContentView.aMinimumCharacterSet
    }
    
    static let symbolHeight: CGFloat = 120.0

    private var initialYOffset: CGFloat {
        ( CGFloat( aReelOne.count ) - 3 ) * ContentView.symbolHeight / 2 * -1
    }

    @State private var yOffset: CGFloat = 0
    // ? は optional 型指定、nil 許容。覚えにくいなぁ
    @State var timer: Timer? = nil

    
    var body: some View {

        VStack( spacing: 40 ) {
            Text( "スロットマシーン" )
                .padding()
            ZStack {
                // 背景の黒い枠(3コマ分の高さ)
                RoundedRectangle( cornerRadius: 20 )
                    .fill( Color(white: 0.15 ))
                    .frame( width: 160, height: 360 )
                    .overlay(
                        VStack {
                            Spacer()
                            Rectangle()
                                .fill( Color.red.opacity( 0.4 ))
                                .frame( height: 4 )
                            Spacer()
                        }
                    )
                    .overlay(
                        RoundedRectangle( cornerRadius: 20 )
                            .stroke( Color.yellow, lineWidth: 5 )
                    )

                VStack( spacing: 0 ){
                    ForEach( 0 ..< aReelOne.count, id: \.self ){ index in
                        Text( aReelOne[ index ] )
                            .font( .system( size: 75 ))
                            .frame( width: 160, height: ContentView.symbolHeight, alignment: .center )
                    }
                }
                .onAppear { yOffset = initialYOffset }
                .offset( y: yOffset )

            } //ZStack
            .frame( width: 160, height: 360 )
            .clipped()

            Text( "状態" )
                .font( .headline )
                .foregroundColor( .black )
                .frame( height: 30 )
                .padding()
        }
        
        HStack( spacing: 30 ) {
            Button( action: { startSpin() }) {
                Text( "Spin" )
                    .font( .headline ).bold()
                    .padding()
                    .frame( width: 140 )
                    .background( Color.gray )
                    .foregroundColor( .black )
                    .cornerRadius( 10 )
            }
            .disabled( false )
            Button( action: { stopSpin() }) {
                Text( "Stop" )
                    .font( .headline ).bold()
                    .padding()
                    .frame( width: 140 )
                    .background( Color.gray )
                    .foregroundColor( .black )
                    .cornerRadius( 10 )
            }
            .disabled( false )
        }  // HStack
    } // body

    func startSpin() {
        timer = Timer.scheduledTimer( withTimeInterval: 0.01, repeats: true ) {
            _ in
            yOffset += 10
            if yOffset > ContentView.symbolHeight {
                yOffset = initialYOffset - 40
            } // end of if
        } // end of timer
    } // end of startSpin()
    
    func stopSpin(){
        timer?.invalidate()
        timer = nil
    } // end of stopSpin()
    
} // struct


#Preview {
    ContentView()
}

さて、このままでは本物っぽくない。 改良点は次の通り。

  • spin ボタンを何度も押すと、回転のスピードが上がってしまう。正しくは、spin 押したらもう押せなくする。
  • ドラムの絵柄と回転スピードを本物に合わせる。ガイドラインがあるはずなので調べよう。
  • 絵柄がぴったりの位置で止まらず、途中の位置で止まる。実物のスロットマシーンは途中で止まらないので改良が必要。
  • 当たりフラグが立たないと特定の絵柄を止められない。例えばチェリーフラグが立って初めてチェリーを止めることができる。当たりフラグが立っていないならドラムを滑らせ表示外にする。

スロットマシンを作る (3)

前回のからだいぶいじった。

まず、絵柄を4つに。 表示は3つなので、一つ増やして4つ。

内部では、スロットの絵柄は二倍に。そうしないと絵が欠けてしまうため。

spin ボタンを押すと絵柄が 10 ズレる。 4コマ表示し切ったら巻き戻して初期表示にする。 一瞬なので、繋がっているように見える。

できたのがこれ。

import SwiftUI

struct ContentView: View {
    // 定数なので let 変数なら var
    static let aMinimumCharacterSet = [ "🍒", "🍉", "🩷", "🔔"]
    var aReelOne: [ String ] {
        ContentView.aMinimumCharacterSet + ContentView.aMinimumCharacterSet
    }
    
    static let symbolHeight: CGFloat = 120.0

    private var initialYOffset: CGFloat {
        ( CGFloat( aReelOne.count ) - 3 ) * ContentView.symbolHeight / 2 * -1
    }

    @State private var yOffset: CGFloat = 0

    var body: some View {

        VStack( spacing: 40 ) {
            Text( "スロットマシーン" )
                .padding()
            ZStack {
                // 背景の黒い枠(3コマ分の高さ)
                RoundedRectangle( cornerRadius: 20 )
                    .fill( Color(white: 0.15 ))
                    .frame( width: 160, height: 360 )
                    .overlay(
                        VStack {
                            Spacer()
                            Rectangle()
                                .fill( Color.red.opacity( 0.4 ))
                                .frame( height: 4 )
                            Spacer()
                        }
                    )
                    .overlay(
                        RoundedRectangle( cornerRadius: 20 )
                            .stroke( Color.yellow, lineWidth: 5 )
                    )

                VStack( spacing: 0 ){
                    ForEach( 0 ..< aReelOne.count, id: \.self ){ index in
                        Text( aReelOne[ index ] )
                            .font( .system( size: 75 ))
                            .frame( width: 160, height: ContentView.symbolHeight, alignment: .center )
                    }
                }
                .onAppear { yOffset = initialYOffset }
                .offset( y: yOffset )

            } //ZStack
            .frame( width: 160, height: 360 )
            .clipped()

            Text( "状態" )
                .font( .headline )
                .foregroundColor( .black )
                .frame( height: 30 )
                .padding()
        }
        
        HStack( spacing: 30 ) {
            Button( action: { startSpin() }) {
                Text( "Spin" )
                    .font( .headline ).bold()
                    .padding()
                    .frame( width: 140 )
                    .background( Color.gray )
                    .foregroundColor( .black )
                    .cornerRadius( 10 )
            }
            .disabled( false )
            Button( action: { () }) {
                Text( "Stop" )
                    .font( .headline ).bold()
                    .padding()
                    .frame( width: 140 )
                    .background( Color.gray )
                    .foregroundColor( .black )
                    .cornerRadius( 10 )
            }
            .disabled( true )
        }  // HStack
    } // body

    func startSpin() {
        yOffset += 10
        if yOffset > ContentView.symbolHeight {
            yOffset = initialYOffset - 40
        }
    }
} // struct


#Preview {
    ContentView()
}

次は、spin ボタンを押したら回転、stop ボタンを押したら止まるようにしよう。

スロットマシンを作る (2)

さて、図柄を少しずつずらして、ある程度ズレたら元の位置に戻す、というのを繰り返すとドラムが回っているように見える(だろう)。 しかしながら、本物のドラムは立体的であり、ある程度下に図柄がズレたら、図柄の一部が欠けなければ本物っぽくないわな。 今は、単に下にずらしているだけなので、なんか変。

こういうのをうまく表現するには、次のようにすればよいと。

  • ZStack を使用
  • 背景を表示
  • 絵柄を表示
  • clipped() で背景の範囲のみ表示するよう指示

表示する絵柄を 2 パターンにして、元に戻った時の違和感を無くすように調整。 Spin ボタンを押しまくる必要があるけど、まるで回転しているようになった。

ContentView.swift

import SwiftUI

struct ContentView: View {
    // 定数なので let 変数なら var
    let reelOne = [ "🍒", "🍉", "🍇", "🔔", "🌟", "🩷" ]
    // 省略しないなら次の書き方
    // let reelOne: [ String ] = ["🍒", "🍉", "🍇", "🔔", "🌟", "🩷"]

    @State private var yOffset: CGFloat = 0
    
    var body: some View {

        VStack( spacing: 40 ) {
            Text( "スロットマシーン" )
                .padding()
            ZStack {
                // 背景の黒い枠(3コマ分の高さ)
                RoundedRectangle(cornerRadius: 20)
                    .fill(Color(white: 0.15))
                    .frame(width: 160, height: 360 )

                VStack( spacing: 0 ){
                    Text( reelOne[ 0 ] )
                        .font( .system( size: 75 ))
                        .frame( width: 160, height: 120, alignment: .center )
                    Text( reelOne[ 1 ] )
                        .font( .system(size: 75 ))
                        .frame( width: 160, height: 120, alignment: .center )
                    Text(reelOne[ 0 ])
                        .font( .system(size: 75 ))
                        .frame( width: 160, height: 120, alignment: .center )
                    Text(reelOne[ 1 ])
                        .font( .system(size: 75 ))
                        .frame( width: 160, height: 120, alignment: .center )
                }
                .offset( y: yOffset )

            } //ZStack
            .frame( width: 160, height: 360 )
            .clipped()

            Text( "状態" )
                .font( .headline )
                .foregroundColor( .black )
                .frame( height: 30 )
                .padding()
        }
        
        HStack( spacing: 30 ) {
            Button( action: { startSpin() }) {
                Text( "Spin" )
                    .font( .headline ).bold()
                    .padding()
                    .frame( width: 140 )
                    .background( Color.gray )
                    .foregroundColor( .black )
                    .cornerRadius( 10 )
            }
            .disabled( false )
            Button( action: { () }) {
                Text( "Stop" )
                    .font( .headline ).bold()
                    .padding()
                    .frame( width: 140 )
                    .background( Color.gray )
                    .foregroundColor( .black )
                    .cornerRadius( 10 )
            }
            .disabled( true )
        }  // HStack
    } // body

    func startSpin() {
        yOffset += 120 / 4
        if yOffset >= 120 {
            yOffset = 0
        }
    }


} // struct



#Preview {
    ContentView()
}

次は・・・ 絵柄を下に積み上げると、絵柄が長いので初期表示で変な位置で止まっている。 これを調整してぴったりの位置に絵柄を表示するようにしよう。

スロットマシンを作る (1)

以前パチンコとかスロットやってて、お金がかかるからソフト作れないかなと思ってて。

パチンコは作るのが難しい。理由は玉が盤上でたくさん踊るためで、これを再現するのはかなり難しいなと。

パチスロは出来そうに思ってて。動いているのはドラム 3 つで、ストップボタンで止める。実際のドラムは立体的で、筒絵柄が描かれている。コンピューター上でこれをどう再現するのかな、というのが難しいところ。実際に筒を定義する・・・というのは厄介そうなので、もう少し簡単そうなのを考えてみる。

配列に絵柄を入れる 絵柄を下にずらす 配列の一番最後にきたら、絵柄を一番上に戻す

これならうまくいくのではないかと。 最近、Google に相談したら Swift のコードも書いてくれるので、それを参考に作ってみる。

構想

  • 絵柄の表示枠は縦に 3 つ
  • 初期表示は止まっている
  • Spin ボタンを押すと絵柄が下に連続してズレる
  • Stop ボタンを押すと絵柄が止まる

これ、できたとしてなんの役に立つのか・・・目押しの練習はできるか。ま、20 年ぐらい前から作ってみたいと思っていたので、深く考えずに作ることにしよう。

作ったアプリ(v0.1)

  • ドラムは一つだけ
  • 絵柄の表示枠は縦に 3 つ
  • 初期表示は止まっている
  • Spin ボタンを押すと絵柄が下に少しズレる
  • Spin ボタンを何度も押すと、絵柄が初期表示位置に戻る
  • Stop ボタンは押しても何も反応しない
  • 将来役に立つだろうと思い、状態表示枠を設置

できたコードはこんな感じ。

ContentView.swift

import SwiftUI

struct ContentView: View {
    // 定数なので let 変数なら var
    let reelOne = [ "🍒", "🍉", "🍇", "🔔", "🌟", "🩷" ]
    // 省略しないなら次の書き方
    // let reelOne: [ String ] = ["🍒", "🍉", "🍇", "🔔", "🌟", "🩷"]

    @State private var yOffset: CGFloat = 0
    
    var body: some View {

        VStack( spacing: 40 ) {
            Text( "スロットマシーン" )
                .padding()
            
            VStack( spacing: 0 ){
                Text( reelOne[ 0 ] )
                    .font( .system( size: 75 ))
                    .frame( width: 160, height: 120, alignment: .center )
                Text( reelOne[ 1 ] )
                    .font( .system(size: 75 ))
                    .frame( width: 160, height: 120, alignment: .center )
                Text(reelOne[ 3 ])
                    .font( .system(size: 75 ))
                    .frame( width: 160, height: 120, alignment: .center )
            }
            .offset( y: yOffset )

            Text( "状態" )
                .font( .headline )
                .foregroundColor( .black )
                .frame( height: 30 )
                .padding()
        }
        
        HStack( spacing: 30 ) {
            Button( action: { startSpin() }) {
                Text( "Spin" )
                    .font( .headline ).bold()
                    .padding()
                    .frame( width: 140 )
                    .background( Color.gray )
                    .foregroundColor( .black )
                    .cornerRadius( 10 )
            }
            .disabled( false )
            Button( action: { () }) {
                Text( "Stop" )
                    .font( .headline ).bold()
                    .padding()
                    .frame( width: 140 )
                    .background( Color.gray )
                    .foregroundColor( .black )
                    .cornerRadius( 10 )
            }
            .disabled( true )
        }  // HStack
    } // body

    func startSpin() {
        yOffset += 10
        if yOffset > 75 {
            yOffset = 0
        }
    }


} // struct



#Preview {
    ContentView()
}

patagonia のセール

今日確認したら、.com のセール開始。

去年もそうだったんだけど、.com のセールが始まるも.jp はまだ。だいたい一週間後ぐらいに .jp でもセールが始まる。同時にセールして欲しいところだけど。.com で余ったら .jp に回すかというところなんだろう。

.com で予習しておくか。フリースのカーディガン欲しかったんだけど、あるね。40% off。.jp でも同じ割引率なら買うか。

macOS に emacs

console で which emacs すると、command not found.

手動で入れるのか。ググってみると、Top にここ

https://emacsformacosx.com/

絶対すぐ分かる。

インストールしてみる。最近の emacs は便利なもので、window を自前で持っている。でも、自分がしたいのは console 上で編集なのだ。

console で、/Application/emacs してみる。だめだ。console 上で起動しない。前述のページ、How to というリンクを辿ると、シンボリックリンクを貼れと。

sudo ln -s /Applications/Emacs.app/Contents/MacOS/emacs-nw /usr/local/bin/emacs

これで無事 console 上で編集できた。