3
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Squeak Smalltalkでライフゲームを500文字プログラミング(Kotlinへの直訳付き)

Last updated at Posted at 2017-06-17

Mastodon(マストドン)では1トゥート(Twitter のツィートに相当)で500文字使えるのと Qiitadonインスタンスで使えるコードブロック記法を組み合わせた @enu7さんの1トゥートプログラミングにチャレンジするシリーズの第二弾。

標記にあるように今回のお題は「ライフゲーム」。挫折するまでがテンプレと思っていたのですが、今回は GNU Smalltalk をやめて Squeak Smalltalk を使ったのが奏功したか、切り詰めたら500文字以内で収まってしまいました。^^;

|f m|
f:=[:x|
	|s z|
	x:=(x lines collect:#asByteArray)-34.
	s:=x*0.-1to:1do:[:v| -1to:1do:[:u| s:=s+((x collect:[:r| r flipRotated:v*2])flipRotated:u*2)]].
	z:=#(3 4)collect:[:a| s collect:[:r| r collect:[:e| (e=a)asBit]]].
	((z*{1.x})sum+34collect:#asString)asStringWithCr].

m:=
'""""""""""
"""#""#"""
"""#""#"""
"##"##"##"
"""#""#"""
"""#""#"""
"##"##"##"
"""#""#"""
"""#""#"""
""""""""""'.

100timesRepeat:[Transcript cr;cr;show:(m:=f value:m)]

Squeak Smalltalk 処理系は squeak.org から入手可能です。

ワークスペースを開いて(デスクトップメニュー or Toolsメニュー → Workspace)から上のコードをペースト(alt/cmd + v)後、あらためて全体を選択(alt/cmd + a)してから評価(alt/cmd + d)すると実行できます。

実行前にトランスクリプトを開いたり(デスクトップメニュー or Tools メニューから Transcript を選ぶ)、トランスクリプトのフォントを御等幅に変更(右クリックメニュー→ set style... (K) → Accumon 等を選択)する必要はありますが、出力結果はこんな感じ。

""""""""""
""""""""""
"""#""#"""
""#"##"#""
"""#""#"""
"""#""#"""
""#"##"#""
"""#""#"""
""""""""""
""""""""""

""""""""""
""""""""""
"""####"""
""#"##"#""
""##""##""
""##""##""
""#"##"#""
"""####"""
""""""""""
""""""""""

""""""""""
""""##""""
"""#""#"""
""#""""#""
"#""""""#"
"#""""""#"
""#""""#""
"""#""#"""
""""##""""
""""""""""

""""""""""
""""##""""
"""####"""
""#""""#""
"##""""##"
"##""""##"
""#""""#""
"""####"""
""""##""""
""""""""""

""""""""""
"""#""#"""
"""#""#"""
"##"##"##"
"""#""#"""
"""#""#"""
"##"##"##"
"""#""#"""
"""#""#"""
""""""""""

""""""""""
""""""""""
"""#""#"""
""#"##"#""
"""#""#"""
"""#""#"""
""#"##"#""
"""#""#"""
""""""""""
""""""""""

ただでさえなじみのない Smalltalk で不自然にスペースや変数名を切り詰めたならなおさら「読めねー」となるのは必然なので、とりあえずスペースと変数を元に戻した版がこちらです。

| life m |
life := [:xx |
   | sum survivs |
   xx := (xx lines collect: #asByteArray) - 34.
   sum := xx * 0.
   #(-1 0 1) do: [:dx | #(-1 0 1) do: [:dy |
      sum := sum + ((xx collect: [:row | row flipRotated: dx*2]) flipRotated: dy*2)]].
   survivs := #(3 4) collect: [:alive |
      sum collect: [:row | row collect: [:elm | (elm = alive) asBit]]].
   ((survivs * {1. xx}) sum + 34 collect: #asString) asStringWithCr].

m :=
'""""""""""
"""#""#"""
"""#""#"""
"##"##"##"
"""#""#"""
"""#""#"""
"##"##"##"
"""#""#"""
"""#""#"""
""""""""""'.

100 timesRepeat: [Transcript cr; cr; show: (m := life value: m)]

これでも「読めねー」となるのも無理からぬことなので、リファレンス引き引き Kotlin で同じ処理をするように書き換えてみました。今回使用した Squeak Smalltalk の組み込みの機能で Kotlin にない機能は拡張関数に追い出したのと、演算子のオーバーロード等をあえて避けたのでいろいろお見苦しいところはご容赦あれかし(念のため life の定義以降が元の Squeak Smalltalk版に相当する処理になります)。

import java.io.ByteArrayOutputStream

fun ByteArray.plu(num: Int): ByteArray = map{ it + num }.toByteArray()
fun ByteArray.plu(that: ByteArray): ByteArray = zip(that).map{ (a,b) -> a + b }.toByteArray()
fun List<ByteArray>.plu(num: Int): List<ByteArray> = map{ it.plu(num) }
fun List<ByteArray>.sub(num: Int): List<ByteArray> = map{ it.plu(-num) }
fun List<ByteArray>.plu(that: List<ByteArray>): List<ByteArray> = zip(that).map{ (a,b) -> a.plu(b) }
fun ByteArray.mul(num: Int): ByteArray = map{ it * num }.toByteArray()
fun ByteArray.mul(that: ByteArray): ByteArray = zip(that).map{ (a,b) -> a * b }.toByteArray()
fun List<ByteArray>.mul(num: Int): List<ByteArray> = map{ it.mul(num) }
fun List<ByteArray>.mul(that: List<ByteArray>): List<ByteArray> = zip(that).map{ (a,b) -> a.mul(b) }
fun ByteArray.div(num: Int): ByteArray = map{ it / num }.toByteArray()
fun List<ByteArray>.div(num: Int): List<ByteArray> = map{ it.div(num) }


fun List<Int>.toByteArray(): ByteArray =
  ByteArrayOutputStream().use{ bos ->
    forEach { bos.write(it) }
    bos.toByteArray()                             
  }

fun <T> List<T>.rotate(n: Int): List<T> =
    if (n == 0) this
    else if (n > 0) drop(n) + take(n)
    else takeLast(-n) + dropLast(-n)

fun ByteArray.rotate(n: Int): ByteArray =
    if (n == 0) this
    else if (n > 0) (drop(n) + take(n)).toByteArray()
    else (takeLast(-n) + dropLast(-n)).toByteArray()

fun Boolean.toBit(): Int = if (this) 1 else 0


fun life(x: String): String {
    var xs = x.lines().map{ it.toByteArray() }.sub(32).div(3)
    var sum = xs.mul(0)
    for(dx in -1..1){ for(dy in -1..1){ sum = sum.plu(xs.map{ it.rotate(dx) }.rotate(dy)) } }
    var survivs = arrayOf(3, 4).map{ alive ->
       sum.map{ row -> row.map{ (it.toInt() == alive).toBit() }.toByteArray() }
    }
    return survivs[0].plu(survivs[1].mul(xs)).mul(3).plu(32).map{ String(bytes = it) }.joinToString(separator = "\n")
}

fun main(args: Array<String>) {
    var m =
"""          
   #  #   
   #  #   
 ## ## ## 
   #  #   
   #  #   
 ## ## ## 
   #  #   
   #  #   
          """
     for(i in 1..10){ println(m); println(); m = life(x = m) }
}

短いライフゲームということで思いついたアルゴリズムはずいぶん前にRubyのMatzさんも紹介していた こちらの APL のライフゲーム。(これこそ読めねーw)

life ← {↑1 ω∨.^3 4=+/,¯1 0 1∘.Θ¯1 0 1∘.Φ⊂ω}

どんな処理をしているかはこちらの動画が参考になります。

簡単に説明すると、セルごとの生か死かを1と0で表現した配列に変換し、これを上下左右斜めの8方向動かし足し合わせることで該当するセルの生死判定(3なら誕生、自セルを含め3か4なら存続→1、それ以外は死→0)を配列同士の演算で行なってしまうというものです。

Squeak Smalltalk は Smalltalk処理系の中でも比較的強く APL の影響をうけていて、R言語や NumPy ほど強力ではないもののずいぶん前から組み込みで配列同士の演算をサポートしています。これを最大限に活用したのが前述コードです。

ついでに @enu7さんの Kotlin版も Squeak Smalltalk で書き直していますのでこちらも参考まで。

| W H d |

W := 10.
H := 10.
d := #(
   '+--------+'
   '|  o  o  |'
   '|  o  o  |'
   '|oo oo oo|'
   '|  o  o  |'
   '|  o  o  |'
   '|oo oo oo|'
   '|  o  o  |'
   '|  o  o  |'
   '+--------+') deepCopy.

World findATranscript: nil.

[Sensor anyButtonPressed] whileFalse: [
   | v recr |
   v := (Array new: W withAll: (IntegerArray new: H)) deepCopy.
   d do: [:i | Transcript nextPutAll: i; cr].

   Transcript cr; endEntry.
   recr := nil.
   recr := [:x :y |
      ((d at: x) at: y) = $o ifTrue: [
         -1 to: 1 do: [:i |
            -1 to: 1 do: [:j |
               (i ~= 0 or: [j ~= 0]) ifTrue: [(v at: x+i) at: y+j incrementBy: 1]
            ]
         ]
      ].
      x < (W - 1)
         ifTrue: [recr value: x+1 value: y]
         ifFalse: [y < (H - 1) ifTrue: [recr value: 2 value: y+1]].
      (d at: x) at: y put: (
         (((v at: x) at: y) = 3 or: [((v at: x) at: y) = 2 and: [((d at: x) at: y) = $o]])
            ifTrue: [$o]
            ifFalse: [Character space]
      )
   ].
   recr value: 2 value: 2.
   (Delay forMilliseconds: 1000) wait.
]

追記:
Squeak 独自の #flipRotated: を #rotated: として補って、あとちょっとだけ手を加えれば Pharo Smalltalk でも元コードを動かすことができます。Pharo は今もっとも勢いがある Smalltalk 処理系で pharo.org から入手できます。

SequenceableCollection compile: 'rotated: delta
   | size result src |
   size := self size.
   result := self species new: size.
   src := 0.
   1 + delta to: size + delta do: [:idx |
      result at: (src := src + 1) put: (self atWrap: idx)].
   ^ result'
| life m |
life := [:xx |
   | xs sum survivs |
   xs := (xx lines collect: #asByteArray) - 34.
   sum := xs * 0.
   #(-1 0 1) do: [:dx | #(-1 0 1) do: [:dy |
      sum := sum + ((xs collect: [:row | row rotated: dx]) rotated: dy)]].
   survivs := #(3 4) collect: [:alive |
      sum collect: [:row | row collect: [:elm | (elm = alive) asBit]]].
   ((survivs * {1. xs}) sum + 34 collect: #asString) asStringWithCr].

m :=
'""""""""""
"""#""#"""
"""#""#"""
"##"##"##"
"""#""#"""
"""#""#"""
"##"##"##"
"""#""#"""
"""#""#"""
""""""""""'.

100 timesRepeat: [Transcript cr; cr; show: (m := life value: m)]
3
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?