プログラミングの備忘録

プログラムをつくる過程を残すもの

processingの備忘録 -ダイクストラ法-

こんにちは。
今回は「ダイクストラ法」を扱います。


前回、幅優先探索深さ優先探索を実装してみました。

taq.hatenadiary.jp

これで単純なグラフや迷路などは最短経路が求められるようになりました。
しかし、エッジも値を持つような、重みつきのグラフなどを対象にはできません。

そんなときに登場するのが「ダイクストラ法」です。


目次


ダイクストラ法とは

ダイクストラ法は、ノード間の距離が 1 ではなく自然数の "重み" を持つようなグラフについて最短経路を求めるアルゴリズムです。
幅優先探索の発展形とも言えるようなもののようです。

ちなみに「ダイクストラ」は人の名前で、オランダの計算科学者エドガー・ダイクストラさんが考案したことに由来するそうです。
(ダイクストラのスペルは Dijkstra らしいです。初見じゃ読めないですよね。)

重みつきグラフの最短経路と言われてもパッと出てこないですが、カーナビの経路案内や鉄道の乗り換え案内などが身近な例かと思います。

重みがどれも同じだった場合、幅優先探索よりも早くなるとかならないとか。


アルゴリズム

詳しい説明は数多ありますので、雰囲気だけ説明します。

全ノードの始点からの距離を暫定的に無限大にし、始点から始点までの距離を 0 に決定した盤面について、始点からの距離が最も近いノードを見つけ、そこまでの距離を始点からの距離として確定させます。
先程見つけたノードを中心に見て距離が一番近いノードを暫定のノードの中から見つけ、そこまでの距離が現在値よりも小さければ更新します (まだ暫定)。
この、始点からの距離が一番近いノードを見つけて確定、そこから一番近いノードを更新、という操作を全てのノードが確定されるまで繰り返します。

最終的に各ノードが持っている「始点からの距離」の値が、始点からの最短距離になります。


丁寧な説明は、例えばこちらなど。

Dijkstra's Algorithm


実装

では実装に移りますが、基本的には前回つくった幅優先探索のプログラムを改変することでダイクストラ法を実装します。
ダイクストラ法の大枠は幅優先探索なので使い回せるかなと思ったのですが、冗長だったり非効率だったりする部分も出てくるかもしれません。


大きな違いとしては、

  • "重み" を管理する配列 weights を追加した

  • 見たかどうかではなく確定したかどうかで判断したいため isCheckedisFixed になった (中身は同じ)

  • queue の中から最短のものを探すため current の取り出しがやや複雑になった

  • "重み" を考慮しながら処理するようになった

といったところでしょうか。


グラフ

今回は下図のような重みつきグラフを対象にします。


重みつきグラフではエッジだけでなく重みも考えなければいけません。
単純には、重みの情報を持った配列を追加すれば良いでしょう。

int[][] edges = {{1, 2}, {0, 3, 5}, {0, 3, 4}, {1, 2, 4, 5}, {2, 3, 5}, {1, 3, 4}};
int[][] weights = {{0, 2, 4, 0, 0, 0},
                   {0, 0, 0, 2, 0, 7},
                   {0, 0, 0, 1, 4, 0},
                   {0, 0, 0, 0, 2, 4},
                   {0, 0, 0, 0, 0, 3},
                   {0, 0, 0, 0, 0, 0}};

edges はこれまでと同様、ノード i につながっているノードの番号を edges[i] に入れておきます。
weights はノード i とノード j をつなぐエッジに重み w があった場合、weights[i][j] = w となる二次元配列です。
これはいわゆる上三角行列の形になっていますが、下三角でも、上下どちらも埋めた行列でも問題ありません。


今回は幅優先探索のプログラムを再利用する形でつくっていったので edgesweights の両方を使っていますが、やり方によっては weights だけでもノード間のつながりを取得できます。

weights を取り出したとき、0 ならつながり無、0 より大きければつながり有ということになりますので、

    for(int i = 0; i < weights[current].length; i ++){
      int next = 10000;
      if(weights[current][i] > 0){
        next = i;
      }
      else{
        continue;
      }

      // 以下に処理が続く
}

next の取得をこんな風にすれば良いと思います。


また、queue からは「暫定的に始点から最も近いノード」を取り出すので、幅・深さ優先探索のときのように単純に queue の端から取り出すだけではうまくいきません。

    int minDist = 10000;
    int minNode = 10000;
    int index = 10000;

    for(int i = 0; i < queue.size(); i ++){ // queue 全体について
      int node = queue.get(i); // queue 内 i 番目のノードの番号を取得
      if(dist[node] < minDist){ // そのノードが始点により近ければ
        // 更新
        minDist = dist[node];
        minNode = node;
        index = i;
      }
    }

    int current = minNode;
    isFixed[current] = true;
    queue.remove(index);


求めたいノードの番号とその始点からの距離を入れておく変数を用意します。(indexremove() の都合で必要)
queue 全体についてループを回し、queuei 番目のノード nodedist[node]minDist の現在地より小さければ、諸々の変数を更新します。
queue を見終えたところで currentisFixed[current] を更新し、queue から index 番目のノード minNode を消します。


あとはだいたい幅優先探索のコードと似ていますが、ところどころで "重み" を考慮した処理が出てきます。

というわけで、エッジと "重み"、始点、終点の情報を受け取って最短経路を出力する関数 dijkstra() はこんな形になりました。

void dijkstra(int[][] edges, int[][] weights, int start, int end){
  int n = edges.length;
  
  boolean[] isFixed = new boolean[n];
  for(int i = 0; i < n; i ++){
    isFixed[i] = false;
  }
  isFixed[start] = true;
  
  IntList queue = new IntList();
  queue.append(start);
  
  int[] dist = new int[n];
  for(int i = 0; i < n; i ++){
    dist[i] = 10000;
  }
  dist[start] = 0;
  
  int[] prev = new int[n];
  for(int i = 0; i < n; i ++){
    prev[i] = 10000;
  }

  while(queue.size() > 0){
    int minCost = 10000;
    int minNode = 10000;
    int index = 10000;
    for(int i = 0; i < queue.size(); i ++){
      int node = queue.get(i);
      if(dist[node] < minCost){
        minCost = dist[node];
        minNode = node;
        index = i;
      }
    }
    int current = minNode;
    isFixed[current] = true;
    queue.remove(index);
    
    if(dist[current] < minCost){ continue; }
    
    for(int i = 0; i < edges[current].length; i ++){
      int next = edges[current][i];
      
      if(isFixed[next] == true){ continue; }
      
      int newCost = dist[current] + weights[current][next];
      if(newCost < dist[next]){
        queue.append(next);
        dist[next] = newCost;
        prev[next] = current;
      }
    }
  }

  IntList route = new IntList();
  int p = end;
  
  while(true){
    route.append(p);
    
    if(p == start){ break; }
    
    p = int(prev[p]);
  }
  route.reverse();
  
  println(route);
}


はじめに挙げたような重みつきグラフで、ノード 0 からノード 5 までの最短経路を探してもらうと、

IntList size=4 [ 0, 1, 3, 5 ]

と出力されました。合っていそうです。


地図

続いて、迷路や地図のようなものを対象としてダイクストラ法で最短経路を求めます。

今回も先程書いたコードから、二次元配列にしたりベクトルを使ったりします。

ArrayList<PVector> dijkstra(int[][] map, int[][] weights, PVector start, PVector end){
  int[] dx = {-1, 1, 0, 0};
  int[] dy = {0, 0, -1, 1};
  
  int nx = map.length;
  int ny = map[0].length;

  boolean[][] isFixed = new boolean[nx][ny];
  for(int i = 0; i < nx; i ++){
    for(int j = 0; j < ny; j ++){
      isFixed[i][j] = false;
    }
  }
  isFixed[int(start.y)][int(start.x)] = true;
  
  ArrayList<PVector> queue = new ArrayList<PVector>();
  queue.add(start);
  
  int[][] dist = new int[nx][ny];
  for(int i = 0; i < nx; i ++){
    for(int j = 0; j < ny; j ++){
      dist[i][j] = 10000;
    }
  }
  dist[int(start.y)][int(start.x)] = 0;
  
  PVector[] prev = new PVector[nx*ny];
  for(int i = 0; i < nx*ny; i ++){
    prev[i] = new PVector(10000, 10000);
  }
  
  while(queue.size() > 0){
    int minCost = 10000;
    PVector minNode = new PVector(10000, 10000);
    int index = 10000;
    for(int i = 0; i < queue.size(); i ++){
      PVector node = queue.get(i);
      if(dist[int(node.y)][int(node.x)] < minCost){
        minCost = dist[int(node.y)][int(node.x)];
        minNode = node;
        index = i;
      }
    }
    PVector current = minNode;
    isFixed[int(current.y)][int(current.x)] = true;
    queue.remove(index);
    visited.add(current);
    
    if(dist[int(current.y)][int(current.x)] < minCost){ continue; } 
    
    for(int i = 0; i < 4; i ++){
      int nextx = int(current.x) + dx[i];
      int nexty = int(current.y) + dy[i];
      
      if(nextx < 0 || nx <= nextx || nexty < 0 || ny <= nexty){ continue; }
      if(isFixed[nexty][nextx] == true){ continue; }
      if(map[nexty][nextx] == 1){ continue; }
      
      int newCost = dist[int(current.y)][int(current.x)] + weights[nexty][nextx];
      if(newCost < dist[nexty][nextx]){
        queue.add(new PVector(nextx, nexty));
        dist[nexty][nextx] = newCost;
        prev[nexty*ny+nextx] = current;
      }
    }
  }

  ArrayList<PVector> route = new ArrayList<PVector>();
  int x = int(end.x);
  int y = int(end.y);
  
  while(true){
    route.add(0, new PVector(x, y));
    
    if(x == start.x && y == start.y){ break; }
    
    int index = y*ny+x;
    
    x = int(prev[index].x);
    y = int(prev[index].y);
  }
  
  return(route);
}


ここで、mapweightscsvファイルでつくっておき、読み込んでおきます。

processingの備忘録 -迷路- - プログラミングの備忘録

processingの備忘録 -幅優先探索・深さ優先探索- - プログラミングの備忘録

processingの備忘録 -txt、csvの生成- - プログラミングの備忘録

processingの備忘録 -画像、テキストの読み込み- - プログラミングの備忘録


map を用意しましたが、壁の情報は weights に非常に大きな "重み" をつけておくことで表せると思います。
(井戸型ポテンシャルみたいな感じで、場合によっては漏れ出すこともあるかもしれませんが。)

まあ、ここでは幅優先探索のプログラムを再利用する形でつくったので、細かい改善は今後やっていけば良いかなと思っています。


実行してみると、重みなしの通常の地図は幅優先探索チックに探索してくれますし、

壁無しで weights だけを指定した場合も、山 (黒い、weights の値が大きい部分) を避けるような経路が示されることが見えると思います。

weightsnoise() で指定してみると、

谷 (色の薄い、weights の値が小さい部分) を縫うような経路が出ます。

ちなみに、weights の設定は以下のような感じです。

  float yoff = 0;
  for(int y = 0; y < n; y ++){
    float xoff = 0;
    for(int x = 0; x < n; x ++){
      terrain[x][y] = map(noise(xoff, yoff), 0, 1, 0, 10);
      xoff += 0.3;
    }
    yoff += 0.3;
  }

processingの備忘録 -3次元グラフ- - プログラミングの備忘録


まとめ

以上、processing にダイクストラ法を実装してみました。

これをさらに改良した (?) A*アルゴリズムというものもあるようですので、今度つくってみたいと思います。
その他、気になった探索法も触ってみるかもしれません。

最後まで読んでいただきありがとうございました。また次回。


参考

processingの備忘録 -幅優先探索・深さ優先探索-

こんにちは。
今回は「幅優先探索」と「深さ優先探索」を扱ってみます。


以前、迷路をつくったり、解いたり、迷路の形が雷の軌跡のモデルになるらしいと聞いて実際にやってみたりしました。

taq.hatenadiary.jp

taq.hatenadiary.jp


1つ目の記事で迷路を解く方法として、行き止まりを埋めていくもの (言わば穴埋め法?) を実装してみました。

そんなある種外道なやり方だけ取りあげて、有名な幅優先探索深さ優先探索はノータッチでしたので今回やってみます。


はじめてのように書いていますが、改めて読んでみたら雷の軌跡の件で幅優先探索だけやっていたようです。
2年前にちょろっと触れたことなのですっかり忘れていました。


目次


はじめに

幅優先探索深さ優先探索は、ともにグラフ理論においてある点から別の点への経路を探す方法です。

幅優先探索では、始点から同じ深さにあるノードを優先するため、同心円状に広がるように、全ての分岐を同時に試していきます。
最短経路を求められることでも有名かと思います。
一方、深さ優先探索では、始点から深いノードを優先するため、行けるところまでいった後に分岐点に返ってきます。


例えば、適当につくった "木" を今回つくったプログラムで探索してみると、探索する順番は以下のようになります。

「S」から始まり、それぞれ横方向と縦方向を意識した動きになっているのがわかるかと思います。
もちろん、エッジの無いところは到達できないので、探索しません。


実装

グラフの扱い方

プログラム上でのグラフは、あるノードがどのノードにつながっているかを配列の形で記述することで表現します。

先程挙げたような "木" で考えてみると、

int[][] edges = {{1, 2}, {0, 3, 4, 5}, {0, 6}, {1}, {1, 7}, {1}, {2}, {4}, {}};

ノード 0 につながっているものは edges[0] で得られ、ノード 1 と 2 につながっていることがわかります。
ノード 8 は何もつながっていないので、edges[8] は空の配列になっています。


幅優先探索

幅優先探索では、同じ深さにあるノードを優先して探索します。

始点をノード 0 としたとき、進む先はノード 1 か 2 です。
ノード 1 に注目すれば、進む先の候補はノード 3 か 4 か 5 になります。
幅優先なので次はノード 2 に注目して、候補はノード 6。
ノード 1 や 2 と同じ深さのものが無いのでさらに深くに進みノード 3 に注目して…と続けていきます。


ここから言えることは、進む先の候補を配列に入れていき、その先頭から取り出していけば次に探索すべきノードがわかる、ということです。
(この右から入れて左から出すような配列は「キュー (queue)」とよばれます。)

加えて、edges には親ノードの情報も入っているので探索済みであれば飛ばす、ということも必要です。


先程の説明なら、
始点をノード 0 としたとき、進む先はノード 1 か 2。

注目: 0 キュー: {1, 2}

ノード 1 に注目すれば、進む先の候補はノード 0 か 3 か 4 か 5。

注目: 1 キュー: {2, 0, 3, 4, 5}

幅優先なので次はノード 2 に注目して、候補はノード 6。

注目: 2 キュー: {0, 3, 4, 5, 6}

ノード 0 は探索済みのため飛ばす。
ノード 1 や 2 と同じ深さのものが無いのでさらに深くに進みノード 3 に注目して…

注目: 3 キュー: {4, 5, 6}

のようにキューが処理されていきます。


最終的には進む先の候補が無くなった、すなわちキューが空になったところで全てを探索し終えます。

終点を設定している場合は終点に到達した時点で終了となるでしょう。


グラフ

全範囲で探索を行い、各ノードが始点からどのくらい離れているかを計算するプログラムを書いてみます。

幅優先探索を行う関数を「breadth-first search」からとって bfs() とします。
ここで扱う配列は、

  • 探索済みかを記録する isChecked

  • 後に探索するノードを入れておく queue

  • 始点からの距離を記録する dist_list

です。

void bfs(int[][] edges, int start){
  int n = edges.length;
  
  boolean[] isChecked = new boolean[n]; // 探索済みかどうかを記録する配列
  for(int i = 0; i < n; i ++){
    isChecked[i] = false;
  }
  isChecked[start] = true;
  
  IntList queue = new IntList(); // 後に探索する点を入れておく配列
  queue.append(start);
  
  int[] dist_list = new int[n]; // 始点からの距離を入れておく配列
  for(int i = 0; i < n; i ++){
    dist_list[i] = 10000;
  }
  dist_list[start] = 0;
  
  // queue が無くなるまで繰り返す
  while(queue.size() > 0){
    // 幅優先探索の処理が入る
  }
  
  println(dist_list);


始点ははじめから queue に入れておき、探索済み、距離 0 とします。

dist_list の初期値は 10000 としていますが、取り得ない値とすることで到達不可能なことや正しく処理されていることを確認するのにも役立ちます。


そして、queue が空になるまで探索を繰り返します。

  // queue が無くなるまで繰り返す
  while(queue.size() > 0){
    int current = queue.remove(0); // queue の最初の要素を取り出す
    
    // current の次を探索
    for(int i = 0; i < edges[current].length; i ++){
      int next = edges[current][i];
      
      // 探索済みなら飛ばす
      if(isChecked[next] == true){ continue; }
      
      // 未探索なら…
      isChecked[next] = true; // 探索済みにする
      queue.append(next); // キューに加える
      dist_list[next] = dist_list[current] + 1; // 距離をそれまでの値+1にする
    }
  }


できた関数を setup() 等で呼び出して実行してみると、

[0] 0
[1] 1
[2] 1
[3] 2
[4] 2
[5] 2
[6] 2
[7] 3
[8] 10000

と出力され、手で数えてみても同じ結果になるので合っていそうです。


始点と終点を決めておき、その間の最短経路がどんなものかも気になるところです。

bfs() は終点も受け取るようにし、各ノードについてひとつ手前のノードを記録する配列 prev を追加します。

  int[] prev = new int[n]; // 一歩前を記録する配列
  for(int i = 0; i < n; i ++){
    prev[i] = 10000;
  }

「未探索なら…」の部分に以下を追加して、一歩手前を保存しておきます。

  prev[next] = current; // 一歩前を記録する


最後に prev を終点から遡っていき、始点まで戻れれば最短経路が求められます。

  IntList route = new IntList();
  int p = end;
  
  // 無限ループ
  while(true){
    route.append(p);
    
    // スタートまできたら終了
    if(p == start){ break; }
    
    p = int(prev[p]);
  }

  route.reverse(); // append() の都合上逆順にしないと始点→終点にならない
  
  println(route);


ノード 0 からノード 7 までの経路を出力させると、

IntList size=4 [ 0, 1, 4, 7 ]

となりました。合っていそうです。


迷路

幅優先探索を迷路に適用すれば、スタートからゴールまでの最短経路が求められます。

迷路も一応グラフのように考えることはできますが、ノード間のつながりをわざわざ配列に起こすのは面倒です。
過去に迷路をつくりましたが、出力形式は道と壁がそれぞれ 0 と 1 で表された二次元配列でしたので、それを活用できるとうれしいですよね。


迷路なら進む先の候補として上下左右の4通りが必ずあるので、「current の次を探索」の部分では edges[current] を考えるのではなく、current の上下左右を考えれば良いということになります。

道であれば queue に入れ、壁であれば飛ばす、というようにすれば問題なさそうです。


先程書いたコードから、二次元配列にしたりベクトルを使ったりして…

void bfs(int[][] maze, PVector start, PVector end){
  // 進行方向4通り
  int[] dx = {-1, 1, 0, 0};
  int[] dy = {0, 0, -1, 1};

  // edge の長さを取得
  int nx = maze.length;
  int ny = maze[0].length;
  
  boolean[][] isChecked = new boolean[nx][ny]; // 探索済みかどうかを記録する配列
  for(int i = 0; i < nx; i ++){
    for(int j = 0; j < ny; j ++){
      isChecked[i][j] = false;
    }
  }
  isChecked[int(start.y)][int(start.x)] = true;

  ArrayList<PVector> stack = new ArrayList<PVector>(); // 次に探索する点を入れておく配列
  stack.add(start);
  
  PVector[] prev = new PVector[nx*ny]; // 一歩前を記録する配列
  for(int i = 0; i < nx*ny; i ++){
    prev[i] = new PVector(10000, 10000);
  }
  
  // stack が無くなるまで繰り返す
  while(stack.size() > 0){
    PVector current = stack.remove(stack.size()-1); // queue の最後の要素を取り出す
    
    // current の次を探索
    for(int i = 0; i < 4; i ++){
      int nextx = int(current.x) + dx[i];
      int nexty = int(current.y) + dy[i];
      
      // 配列の外なら飛ばす
      if(nextx < 0 || nx <= nextx || nexty < 0 || ny <= nexty){ continue; }
      // 探索済みなら飛ばす
      if(isChecked[nexty][nextx] == true){ continue; }
      // 壁なら飛ばす
      if(maze[nexty][nextx] == 1){ continue; }
      
      // 未探索なら…
      isChecked[nexty][nextx] = true;  // 探索済みにする
      stack.add(new PVector(nextx, nexty)); // スタックに加える
      prev[nexty*ny+nextx] = new PVector(current.x, current.y); // 一歩前を記録する配列
    }
  }
  
  // 最短経路を復元
  ArrayList<PVector> route = new ArrayList<PVector>();
  int x = int(end.x);
  int y = int(end.y);
  
  // 無限ループ
  while(true){
    route.add(0, new PVector(x, y));
    
    // スタートまできたら終了
    if(x == start.x && y == start.y){ break; }
    
    int index = y*ny+x;
    x = int(prev[index].x);
    y = int(prev[index].y);
  }
  
  println(route);
}


忘れていましたが、迷路は事前につくっておいてcsvファイルで入れてあります。

迷路の回で触れていますし、

processingの備忘録 -迷路- - プログラミングの備忘録

ファイルの扱いをまとめた記事もあります。

processingの備忘録 -txt、csvの生成- - プログラミングの備忘録

processingの備忘録 -画像、テキストの読み込み- - プログラミングの備忘録


実行すると結果が返ってきますが、迷路など対象が大きくなるとコンソールに出力されても何が何やらわかりません。
図示できるようにしましょう。

迷路のほぼ全域を探索してやっと経路が見つかっていますね。


地図

迷路と似ているかもしれませんが、少し通路が広いので障害物を避けるような動きが見えます。
幅優先探索の本領発揮というか、このようなものを対象にした方が幅優先探索で最短経路が求められるということがわかりやすいかもしれません。


深さ優先探索

深さ優先探索では、なるべく深くまで探索して分岐点まで戻ってきます。

始点をノード 0 としたとき、進む先はノード 1 か 2 です。
ノード 2 に注目すれば、進む先の候補はノード 6 になります。
深さ優先なので次はノード 6 に注目して、進む先の候補はなし。
行き止まりまで来たので最初の分岐点まで戻りノード 1 に注目して…と続けていきます。


実は、深さ優先探索では進む先の候補を配列に入れていき、その最後から取り出していけば次に探索すべきノードがわかる、ということになります。
(この右から入れて右から出すような配列は「スタック (stack)」とよばれます。)

つまり、幅優先探索のキューをスタックに変えるだけで深さ優先探索が実装できてしまいます。


先程の説明なら、
始点をノード 0 としたとき、進む先はノード 1 か 2。

注目: 0 スタック: {1, 2}

ノード 2 に注目すれば、進む先の候補はノード 6。

注目: 2 スタック: {1, 6}

深さ優先なので次はノード 6 に注目して、進む先の候補はなし。

注目: 6 スタック: {1}

行き止まりまで来たので最初の分岐点まで戻りノード 1 に注目して…

注目: 1 スタック: {3, 4, 5}

のようにスタックが処理されていきます。


コード自体は remove() の引数を 0 から queue.size()-1 に変えれば動くので省略します。
(実際はスタックなので queuestack に変えるべきかもしれませんが。関数名も「depth-first search」から dfs() などにしても良いでしょう。)


迷路

dxdy の並びを変えると黄色の及ぶ範囲が変わります。

何やらゴールまで一直線に向かっているような動きをしていますが、おそらく穴掘り法が深さ優先探索に似た動きをするからかと思います。
棒倒し法や壁伸ばし法だともっと黄色が広範囲になって迷っている感じがしました。


グラフは違いがわかりづらいし、地図に関しては自由度が高すぎて迷走しまくってたのでカットしました。


まとめ

以上、processing で幅優先探索深さ優先探索を実装してみました。

他にも探索法は様々あるようですので、記事にしてみたいと思います。

最後まで読んでいただきありがとうございました。また次回。


参考

shapez で全自動工場をつくる

こんにちは。
今回は、shapez で全自動工場をつくってみます。


前回、shapez の紹介を行いました。

taq.hatenadiary.jp

いきなりですがレベル26まではすっ飛ばして、それ以降のフリープレイモードについての記事になります。


レベル26までやってきた方であれば、shapez の操作に慣れてきたことと思います。
そして同時に、その面白さを知っていただけたのではないでしょうか。
ここからももっと面白くなってきます。


注意: shapez の面白さは工場をどうするか考えるところにありますので、本記事はネタバレになります。


目次


はじめに

これから全自動工場をつくっていくわけですが、割と規模が大きなものになりますので、設計図のようなものがあると良いと思います。
なんとなくでもこんな配置にして、各工程はこんな形で、など。

遠くにつくって、後で近くに移転させてもいいかもしれません。
レベル12で要求される CbCbCbRb:CwCwCwCw (ブループリント) はあって困ることはありませんので、たくさんつくっておきましょう。

そしてこれは一番大事なのですが、ここで書く方法が正解というわけではありません。
結局動けばいいんです。


全自動工場の流れ

  1. 素材を集める

  2. 必要な部品を取り出す

  3. 着色する

  4. 1層分を組み合わせる

  5. 積層させる

  6. 完成


参考までに、私の工場はこんな感じになっています。
行き当たりばったりでつくったのであまり洗練された感じではないですが…。

ちょっと間を詰めてみたら半分くらいになりました。


素材を集める

素材は工場の近場から集めます。
丸、四角、星は完全な形が既にありますので、コンベアが最高率になるように連鎖抽出機をつなげて取り出します。
おそらく16個/秒くらいになるようにすれば良いと思います。

扇については一部のみのものしかないので自分でつくる必要があります。
半分が扇由来の形となっているものを使うと良いです。
近くに無ければ4分の1のものでも十分な速さでつくることができるならば問題ありません。

仮想的にはこんな処理をすれば良いことになります。


色に関しても同様に集めましょう。
都度混色するよりも、事前に混色しておいていつでも使えるようにしておいた方が効率が良さそうです。


工場本体にはストレージを介してつなげておけば、使わないときは貯蔵に回せるので無駄が出にくいと思います。


必要な部品を取り出す

ここが肝です。回路を使います。

要求されている図形が1層のみの場合を説明します。
最終的には4層まで積層した図形を要求されますが、1層用の処理を4回繰り返せばいいです。


今、基本素材を全て集めたので、HUBから要求された図形に必要なものだけを取り出してきます。

Eキーを押すと、要求されている図形の情報の取り出し口がHUBの左下に見えます。

これを形状解析機につなげると、右上の色と形を読み取ってくれます。

この場合なら、白色の四角ということになります。

得られた情報と合致する素材が流れているコンベアから、アイテムフィルタを使って分岐させ、図形作成場に持っていきます。


具体的にはこのような配置になります。
1ヶ所あたりに必要な色・形は1つずつなので、分岐したコンベアはトンネルで縦一列に並ぶようにしておくとつくりやすいと思います。

これにより、四角のみが下に流れていくようになりました。

色についても同様に分岐させましょう。


1層あたり4ヶ所の色と形が必要になりますが、仮想回転機で1回回した後に仮想解析機に通せば、元の図形の左上の情報が得られます。
繰り返せば他の場所についても得ることができます。


着色する

続いて、色をつけていきます。


仮想解析機で得られる情報は右上の情報なので、使うのは4分の1個分で十分です。
そこで、切断機 (四分割) で切るという前処理を施します。 回転させて全て右上にくるようにしておけば、1個の素材から4個分得られます。


この配置を1単位として、4つつなげれば16個/秒の速さが出せると思います。
着色機 (ダブル) を使っていますが、通常の着色機でもコンベアをいっぱいにできれば問題ありません。


これを4ヶ所分行い、次の工程へ向かいます。


1層分を組み合わせる

着色の工程でできた4つの部品を積層機を使って1つにまとめます。


以下を1単位として8つつなげれば16個/秒は出せるでしょう。
部品は4つあるので対応する位置にくるように回転機で調整して3回繰り返す必要があります。


要求された図形が複数層から成る場合は、各層について同様に処理をして層の数の分だけ図形をつくっておきます。


積層させる

以降は、要求された図形が複数層から成る場合に必要です。
といってもやることは同じで、要求通りに重ねていけば良いです。


完成

積層が完了したら完成です。

実際に稼働させて、工場を一切いじることなくレベルをクリアできれば実績解除です。


しかし、実はまだ不十分で、フリープレイモードを進めるうちに工場が止まってしまうことがあるかと思います。
なぜかというと、一部が欠けたものや無色のものには対応していないからです。

そのような図形に対しては、形状解析機が $\phi$ を返してきますので、アイテムフィルタなどを使って工程を飛ばさせたりという工夫が必要です。


また、これはMAM (Make Anything Machine) とよばれる、不完全な全自動工場になっています。
具体的には、レベル20の RuCw--Cw:----Ru-- やレベル26の CbCuCbCu:Sr------:--CrSrCr:CwCwCwCw のような単純に各層を積層するだけではつくることができない図形には対応していません。
(どうつくれば良いか苦戦しませんでしたか。)

このような図形に対しては、TMAM (True Make Anything Machine) とよばれる工場が必要になります。
処理が難しそうなので、機会があればまとめます。
が、フリープレイモードをやる上ではMAMで十分っぽいです。


納品方法

フリープレイモードでは図形の量ではなく速さで要求されます。
先程から何度か書いていますが、コンベア1本あたり16個/秒くらいが最大なので、工夫をしなければ高レベルの要求を満たすことができません。

ならば何本も使えば良いですね。
ある程度貯めておいて、HUBにある口の複数からに同時に納品すれば良いということになります。

ではどこに貯めるか。
ストレージに貯める手もありますが、上限が5000個と多いかつタイマーでも無い限り途中でシグナル変化が生じないということで、別の方法が良いでしょう。

薄々感じていたかもしれませんが、部品それぞれにも若干容量があります。
そして、なぜか知りませんが合流機 (コンパクト) と分流器 (コンパクト) で多いです。
というわけで、それらを組み合わせた簡易ストレージを準備し、そこがいっぱいになったら放流して一気に納品、という形にしてみます。

実際には、ストレージの性質を利用して貯まったことを検知します。
ストレージには出口が2つありますが、左側の星付きの口から優先して出されます。右側の口からは、左側の口からの出力が止まったときに出てきます。
なので、右側の口を簡易ストレージにつなげ、左側の口にはベルトリーダーとゴミ箱をつけておけば、左側が詰まって右側にアイテムが流れるようになったときにベルトリーダーが1を出力するので、それをきっかけとして簡易ストレージを開ければ良いことになります。

しかしこのままでは簡易ストレージが開いた瞬間に右側の流れが復活してしまい、ベルトリーダーからの信号が0に戻ってしまいます。

簡易ストレージの出口側にもベルトリーダーをつけておき、両方の信号をORゲートを介してアイテムフィルタにつなげておけば、簡易ストレージの入り口側か出口側どちらかのベルトリーダーが1を出力している間は出し続ける、ということができるので、貯まったアイテムを全て出してから閉じるようにできます。

HUBの12口を使って、150個/秒くらいにはなりました。


効率化

速さに関しては (1レーンのみで運ぶ場合) 16個/秒くらいが最速かと思いますが、あるレベルをクリアして次のレベルに移ってから図形ができあがるまでの時間も短くしたいところです。

簡単なのは要らない素材を捨てることでしょう。


捨てるようにする前の速さを測れていないので比較できませんが、なんとなく捨てるようにしてみたところ、レベルクリアから次の図形が到着するまでにだいたい2分半くらいかかりました。
多分もっと速くできると思います。

この動画では1分と少しくらいで来ているように見えますね (2:59:00~)。

[アーカイブ]shapez世界一が全自動工場(MAM)を造る配信 - YouTube


まとめ

以上、shapez の全自動工場をつくってみました。
とりあえず動くので記事を書いてみようと今に至りますが、これからもっと洗練された形にしていければと思っています。

それでは、良い shapez ライフを。また次回。

「shapez」をやろう

こんにちは。
今回は「shapez」というゲームを紹介します。


過去に INDIE Live Expo か何かで見たことがあって Steam のウィッシュリストにも入っていたのですが、すっかり忘れていました。
友人に好きそうだからと勧められたときに思い出し、体験版をやってみたところ見事にハマってしまい、ちょうどセール中で120円でしたので即決で買いました。

プログラミングとは直接関係しませんが工場設計ゲームなので、まずこれをして次にこれ…というように組み立てていく感覚は近いものがあると思い、記事にしました。
というのは建前で、純粋にやってみてほしい。土日を溶かしたこのゲームの面白さを伝えたい。

過度なネタバレは避け、セールが7月30日までと残り3日間しか無く早く書き終えたいということで、珍しくあっさりした記事になります。


shapez とは

タイトルに「shape」とあるように図形がキーワードになります。
丸、四角、星、扇の図形を切ったり組み合わせたり色を塗ったりして、指定された図形をつくります。

store.steampowered.com

序盤は要求された図形に応じて工場を建てて納品するのですが、最終的には図形の確認から納品まで自動でやってくれる工場を建てることになります。

ちなみにですが、これらの図形は文字列として表現する方法もあります。

shapez.io shape generator


実際にはこんな感じ

これがハブとよばれ、図形の指定をしてくるのでここに納品します。
レベル1では無色の"丸" (CuCuCuCu) を30個要求されます。

"丸"の源から「抽出機」を使って図形を取り出し、コンベアでハブまで運びます。

要求をクリアする毎に、切断、回転、着色、積層など新たな要素が増えていきます。
納品した図形を消費して、コンベアの速さなどをアップグレードすることもできます。


レベル26までは要求される図形は決まっています (いわばストーリーモード) が、それ以降はフリープレイモードとなり、図形はランダムになります。
そして同時に、全自動工場の建設が始まります。

要求される図形は、例えば…

全自動工場ができてしまえばほとんど放置ゲームに変わってしまいます。
が、ときどき既存の工場では対応できない図形が要求されたりするので、都度改良が必要になります。


他、より無駄の無い工場に改良していったり、目的の図形をどれだけコンパクトな工場でつくれるか試したり、Steam の実績を進めたり…。
興味がありましたら、RTAなども見てみると面白いと思います。(そんな実績もありますし)


まとめ

以上、「shapez」の紹介をしてみました。
シンプルながら様々な遊び方のあるゲームだと思います。

今後、全自動工場などについても記事を書いてみたい。

7月30日までなら90%オフの120円という破格になっていますので、ぜひ。

processingの備忘録 -マーブリング-

こんにちは。
今回は、processing で「マーブリング」をやってみます。


以前にこんなツイートを見ました。

流体シミュレータ的なものに興味があったので、こんなに単純化したモデルもあったのか、と感心していました。


そしてつい最近、我らが Shiffman 先生が Coding Challenge で似たものを取りあげてくださいました。


興味があってやり方もわかったとなれば、やるしかありません。


目次


マーブリングとは

マーブリングは、水面に絵の具を垂らしたり少しかき混ぜたりすることにより、いわゆる「マーブル模様」をつくり出す手法です。
日本では「墨流し」が有名ですね。
紙や布などに写しとって作品とされることが多いようです。

フルイドアートやラテアートなどもこの一種といえるのではないでしょうか。


実装

絵の具の滴下と押しのけ処理

まずは、クリックで円が描かれるようにしてみます。


ArrayList<Drop> drops = new ArrayList<Drop>();

void setup(){
  size(500, 500);
  noStroke();
}

void draw(){
  background(255);
  
  for(Drop d: drops){
    d.display();
  }
}

void mousePressed(){
  Drop drop = new Drop(mouseX, mouseY);
  drops.add(drop);
}

class Drop{
  PVector center; 
  float r;
  color col;
  int n;
  PVector[] edges;
  
  Drop(float _x, float _y){
    center = new PVector(_x, _y);
    r = 50;
    col = int(random(50, 200));
    
    n = 100;
    edges = new PVector[n];
    for(int i = 0; i < n; i ++){
      float theta = 2*PI*i/n;
      edges[i] = new PVector(center.x + r * cos(theta), 
                             center.y + r * sin(theta));
    }
  }
  
  void display(){
    fill(col);    
    beginShape();
      for(int i = 0; i < n; i ++){
        vertex(edges[i].x, edges[i].y);
      }
    endShape();
  }
}

垂らした絵の具を「drop」とよぶことにし、クラスをつくりました。
また、今後計算にあたって円の外周の点が必要なので「ある点から一定距離にある点の集まり edges」として円を表現しています。


これだけでは単に円が重なっていくだけのプログラムですが、ここに以下のような関係を導入することで絵の具が押しのけられるような挙動を表現できます。

$$ P' = C + (P - C) \times \sqrt{ 1 + \frac{ r^{2} }{ |P-C|^{2} } } $$

ここで、P'P は絵の具の外周の点の位置 (P は移動前、P' は移動後)、C は新たに垂らしている絵の具の中心の位置、r は新たに垂らしている絵の具の半径を指します。
ある色の絵の具の範囲を表すにはその外周の点がわかれば良いので、外周の点 P が新たに垂らされた絵の具によってどのくらい動くか、を計算すれば良いということになります。

$C$ に $P - C$ を足すと $P$ になりますが、既存の絵の具は新たに垂らされている絵の具によって押し広げられるので、その分を $\displaystyle \sqrt{ 1 + \frac{ r^{2} }{ |P-C|^{2} } }$ として $P - C$ にかけることにより $C$ を中心として若干遠くに動かしています。
既存の絵の具の移動量は、新たな絵の具が遠い (= $|P - C|$ が大きい) ほど小さく、新たな絵の具が多い (= $r$ が大きい) ほど大きいと考えられるので $\displaystyle \frac{ r }{ |P-C| }$ のという形となっていると思われます。


というわけで、この数式を実装します。

クラスに以下のメソッドを追加し、

void isPushedBy(Drop drop){
  for(PVector edge: edges){
    PVector c = drop.center;
    PVector p = edge;
    float r = drop.r;
      
    PVector s = PVector.sub(p, c);
    float m = s.mag();
    float root = sqrt(1 + (r*r)/(m*m));
      
    edge.set(PVector.add(c, s.mult(root)));
  }
}

呼び出しは mousePressed() 内で行うことにします。

void mousePressed(){
  Drop drop = new Drop(mouseX, mouseY);
  drops.add(drop);
  
  for(int i = 0; i < drops.size()-1; i ++){
    Drop other = drops.get(i);
    other.isPushedBy(drop);
  }
}

新たな絵の具 drop により既存の絵の具 other が押しのけられるので、isPushedBy() という名前にしてみました。
isPushedBy() 関数では先程挙げた数式から、edge の情報を更新しています。

実行するとこんな感じ。

押しのけられる挙動が見えるようになりました。


滴下処理をいじる

より実際のマーブリングらしく、長押しで絵の具の面積が大きくなっていくようにしてみます。

mousePressed() では押した瞬間のみの実行になってしまいますが、draw() 内で mousePressed とif文を組み合わせればマウスボタンが押されている間ずっと実行させることができるようになります。
そこで、mousePressed() には新たな絵の具の追加の処理を残したまま、mousePressed を使って押しのける処理を行うことにしました。


ArrayList<Drop> drops = new ArrayList<Drop>();

void setup(){
  size(500, 500);
  noStroke();
}

void draw(){
  background(255);
  
  for(Drop d: drops){
    d.display();
  }
  
  if(mousePressed == true){
    Drop drop = drops.get(drops.size()-1);
    
    for(int i = 0; i < drops.size(); i ++){
      Drop other = drops.get(i);
      other.isPushedBy(drop);
    }
  }
}

void mousePressed(){
  Drop drop = new Drop(mouseX, mouseY);
  drops.add(drop);
}

class Drop{
  PVector center; 
  float r;
  color col;
  int n;
  PVector[] edges;
  
  Drop(float _x, float _y){
    center = new PVector(_x, _y);
    r = 10;
    col = int(random(0, 255));
    
    n = 100;
    edges = new PVector[n];
    for(int i = 0; i < n; i ++){
      float theta = 2*PI*i/n;
      edges[i] = new PVector(center.x + r * cos(theta), 
                             center.y + r * sin(theta));
    }
  }
  
  void display(){
    fill(col);    
    beginShape();
      for(int i = 0; i < n; i ++){
        vertex(edges[i].x, edges[i].y);
      }
    endShape();
  }
  
  void isPushedBy(Drop drop){
    for(PVector edge: edges){
      PVector c = drop.center;
      PVector p = edge;
      float r = drop.r;
      
      PVector s = PVector.sub(p, c);
      float m = s.mag();
      float root = sqrt(1 + (r*r)/(m*m));
      
      edge.set(PVector.add(c, s.mult(root)));
    }
  }
}

処理的には、新たに追加された絵の具による押しのけをクリックしている間ずっと続ける、という形なので押しのけ処理を適用する絵の具は drops.size()-1 までにする必要があります。


実行してみると、


乱し処理

ここまででなんとなくできてきましたが、マーブリングでは乱す操作も重要な要素です。

直線

まずは直線状に乱します。なぞるような感じでしょうか。


軸に沿って

y軸に沿った方向になぞる場合、移動前の $P = (x, y)$ はなぞることにより $P' = (x, y + z u ^ {| x - x_{L} |})$ に移動します。
ここで、$u$ は双曲線に近似するための値だというようなことが書かれており (The Mathematics of Marbling)、$u = \left( \frac{1}{2} \right) ^ {\frac{1}{c}}$ で表されるようです。
また、$z$ は歪みの大きさ、$c$ は歪みの鋭さ、$x_{\mathrm{L}}$ はなぞる位置のx座標を表します。

これを適用すると、$z$ を係数として $x _{\mathrm{L}}$ に近い点は大きく、遠い点は小さく移動する、という感じになります。


コード上では、Dropクラスに tineLine() という関数を追加して処理を行います。

void tineLine(float xl, float z, float c){
  for(PVector edge: edges){
    float u = 1 / pow(2, 1/c);
    
    edge.x = edge.x;
    edge.y = edge.y + z*pow(u, abs(edge.x-xl));
  }
}

変数として $x_{\mathrm{L}}$、$z$、$c$ がありますので、引数として受け取るようにしました。
そして計算式に従い、円周上の点である egdes の座標を変更していきます。

呼び出しは draw() 内で、

if(keyPressed == true){
  for(Drop d: drops){
    d.tineLine(mouseX, 5, 10);
  }
}

として、キーを押したときに実行されるようにしてみました。

zc を変えることで、若干ですが見た目も変わると思います。


x軸に沿った場合は、$x$ と $y$ の関係が入れ替わるだけです。
つまり、$P = (x, y)$ が $P' = (x + z u ^ {| y - y_{L} |}, y)$ に移動します。


一般化

これまでの計算方法では、軸に沿った向きにしか歪みを加えられませんでした。
実際には棒か何かで任意の方向になぞっていく感じになると思いますので、上の式を一般化します。

といっても、やること自体は軸に沿った場合のものを任意の角度に回転させただけです。

つまり、

$$ P' = P + z u^{|(P - B) \cdot N|} M $$

$P$ に対して、移動方向に $z u^{|(P - B) \cdot N|}$ だけ動かす分のベクトルを足す、という計算になります。
ここで、大文字はベクトルだと考え、$N$ と ${M}$ はそれぞれの方向の単位ベクトルとします。


ちなみに、距離が $|(P - B) \cdot N|$ と表されていることについては、

と見たとき、赤い点線の長さは $|P - B| \mathrm{cos} ~ \theta$ と表されることから、$N$ は単位ベクトルなので $|P - B| |N| \mathrm{cos} ~ \theta$、内積の定義から $(P - B) \cdot N$ と変形できることから説明できます。


ではプログラムに組み込んでいきます。
先程つくった tineLine() を一般化したものにつくりかえれば良いでしょう。

void tineLine(PVector start, PVector direction, float z, float c){
  for(PVector edge: edges){
    PVector l = direction;
    PVector p = edge;
    PVector b = start;
    PVector n = l.copy().rotate(PI/2).normalize();
    PVector m = l.copy().normalize();
        
    float u = 1 / pow(2, 1/c);
    float d = abs(PVector.dot(PVector.sub(p, b), n));
        
    edge.set(PVector.add(p, m.mult(z*pow(u, d))));
  }
}

ベクトルの演算に関して、PVector.sub(p, b) のように呼び出した場合は問題ありません。
しかし、何か特定のベクトルのメソッドとして、p.sub(b) のように呼び出した場合は p の情報を変更しながら計算が行われてしまうので、予期せぬ挙動になってしまいます。
この問題は、p.copy().sub(b) のように copy() を介することで回避できます。

呼び出しは draw() 内で、キーを押したときに。

if(keyPressed == true){
  for(Drop d: drops){
    PVector s = new PVector(mouseX, mouseY);
    PVector v = new PVector(0, 1);
    d.tineLine(s, v, 5, 10);
  }
}

マウスの位置 s を始点として、方向 v に歪みを加えます。
(0, 1) なら、y軸方向に、(1, 1) なら45°の方向に、など指定できますし、うまくやればマウスでなぞった部分に適用するようにもできると思います。


序盤で $u$ は双曲線に近似するための値だというようなことを書きましたが、論文 (https://ieeexplore.ieee.org/document/5887299) の方には近似の形ではない式があります。

$$ P' = P + \frac{\alpha \lambda}{d + \lambda} M $$

ここで、$d$ は $|(P - B) \cdot N|$、$\alpha$ と $\lambda$ は $z$ や $c$ と同様、それぞれ歪みの大きさと鋭さを表すパラメータです。

$z$ ($\alpha$)、$c$ ($\lambda$) がともに 10 のとき、基準 $x_{\mathrm{L}}$ からの距離と $P' - P$ の大きさの関係を見てみると、近似あり (青線) が近似なし (橙線) に近い値となっていることがわかると思います。

というわけで、表現が違うだけでやりたいことは同じだと思いますので、どちらを使っても問題ないと思います。


渦の中心を $B$ として、渦から一定の距離にある $P$ は、

$$ P' = B + (P - B) \begin{pmatrix} \mathrm{cos} ~ a & -\mathrm{sin} ~ a \\ \mathrm{sin} ~ a & \mathrm{cos} ~ a \end{pmatrix} $$

で表される $P'$ に移動します。
ここで、$a$ は渦により $P$ が何度回転するかを表す値で、$\displaystyle a = \frac{zu^{||P - B| - r|}}{|P - B|}$ と表されます。

また $\begin{pmatrix} \mathrm{cos} ~ a & -\mathrm{sin} ~ a \\ \mathrm{sin} ~ a & \mathrm{cos} ~ a \end{pmatrix}$ は回転行列とよび、これが作用したベクトルは角度 $a$ だけ回転します。
(回転行列については、ベクトルの回転 ― Kinectで学ぶ数学 - Build Insider などに説明があります。)


$a$ について少し説明を加えます。

$P$ から $P'$ への移動量は、直線で歪めたときの関係式から、$zu^{d}$ と表すことができます。
$d$ はなぞった軌跡と $P$ の距離なので、渦の場合は $d = ||P - B| - r|$ となります。

また、軌跡を弧としたときの円の中心 $B$ と $P$ との距離は $|P - B|$ です。

弧度法から、$P$ が $B$ を中心とする円弧上を $zu^{d}$ だけ移動したときの中心角を $a$ とすると、$\displaystyle a = \frac{zu^{d}}{|P - B|}$ と表すことができるので、$\displaystyle a = \frac{zu^{||P - B| - r|}}{|P - B|}$ となります。


以上から、$B$ に $\overrightarrow{ BP }$ を角度 $a$ だけ回転させたベクトルを足すことにより、移動後の $P'$ を計算する、ということになります。


では、これをプログラムに組み込んでいきます。
Dropクラスに tineVortex() という関数を追加して処理を行うことにします。

回転行列はどう計算するのかというところですが、これは線形代数の基本である行列の積の計算を行えば良いです。
つまり、行列がかけられるベクトルを $V = (x, y)$ とすると、

$$ V \begin{pmatrix} \mathrm{cos} ~ a & -\mathrm{sin} ~ a \\ \mathrm{sin} ~ a & \mathrm{cos} ~ a \end{pmatrix} = (x \mathrm{cos} ~ a + y \mathrm{sin} ~ a, -x \mathrm{sin} ~ a + y \mathrm{cos} ~ a) $$

となります。

void tineVortex(PVector vcenter, float radius, float z, float c){
  for(PVector edge: edges){
    PVector p = edge;
    PVector b = vcenter;
    float r = radius;
      
    float u = 1 / pow(2, 1/c);
    PVector s = PVector.sub(p, b);
    float d = abs(s.copy().mag() - r);
    float a = z*pow(u, d) / s.copy().mag();
      
    edge.x = b.x + s.x*cos(a)+s.y*sin(a);
    edge.y = b.y - s.x*sin(a)+s.y*cos(a);
  }
}

軌跡の中心を vcenter、中心から軌跡までの距離を radius として受け取っています。
上で説明した計算をガリガリ行って、最後に edge の座標を更新して完了です。

これまでと同様、呼び出しは draw() 内で、キーを押したときに実行されるようにします。

if(keyPressed == true){
  for(Drop d: drops){
    PVector s = new PVector(mouseX, mouseY);
    d.tineVortex(s, 100, 1, 10);
  }
}


実行すると、

radius を 0 にすれば、The Mathematics of Marbling で Vortex として立項されているものになります。

混ぜすぎると描画がおかしくなってしまうので、描画方法は要検討かもしれません。


波型に乱すことによって、ある点 $P$ は、

$$ P' = P + A \mathrm{sin} ~ (\omega v + \phi) (\mathrm{cos} ~ t, \mathrm{sin} ~ t) $$

で表される $P'$ に移動します。
ここで、$A$ は振幅、$\omega$ は角振動数、$\phi$ は位相、$t$ は波の振動方向を表します。
また $v$ は $P \cdot (\mathrm{sin} ~ t, -\mathrm{cos} ~ t)$ という内積の計算結果が入ります。

$(\mathrm{sin} ~ t, -\mathrm{cos} ~ t)$ は $(\mathrm{cos} ~ t, \mathrm{sin} ~ t)$ を時計回りに90°回したベクトルに相当します。
このベクトルと $P$ との内積をとるので、波の振動方向と垂直な方向の成分を取り出すことができます。
これを変位としてサイン波を計算し、振動方向の単位ベクトルにかけることで波の形をつくりだします。
最後に、つくったベクトルを $P$ に足すことにより波型に変形させることができる、という感じだと思います。


では、これをプログラムに組み込んでいきます。
Dropクラスに tineWave() という関数を追加して処理を行うことにします。

void tineWave(float direction, float amplitude, float frequency, float phase){
  for(PVector edge: edges){
    PVector p = edge;
    float a = amplitude;
    float l = frequency;
    float o = phase;
    float t = direction;
      
    float dot = p.x*sin(t) - p.y*cos(t);
    float f = a*sin(l*dot+o);
      
    edge.x = p.x + f*cos(t);
    edge.y = p.y + f*sin(t);
  }
}

呼び出しは draw() 内で、キーを押したときに実行されるようにします。

if(keyPressed == true){
  for(Drop d: drops){
    d.tineWave(0, 1, 0.1, 0);
  }
}


実行すると、


まとめ

以上、processing でマーブリングを行えるようにしてみました。
このご時世、言語の壁は大きな障害にならないかもしれませんが、日本語版の解説記事的な感じで役立てていただければ幸いです。

乱し方として線分 (直線型をある程度の長さにとどめる) のものもあるようですが、ちらっと見た (https://people.csail.mit.edu/jaffer/Marbling/stroke.pdf) ところ難しそうだったので、追々ここに追記するか、新たに記事を書くかして触れてみようと思っています。
また、形が複雑になってくると描画に不満が出てきます。edges の点数を増やすと重くなってしまうので、別の方法で行う必要があるかなとも思っています。


新年度から割と忙しく、やっと時間が取れてリハビリがてらの更新のつもりがまあまあ長い記事になってしまいました。
最後まで読んでいただきありがとうございました。また次回。


参考

Project Euler を通して Haskell を学ぼう

こんにちは。
今回は Project Euler の第1問から第10問まで Haskell で解いてみようと思います。


Project Euler は、時間制限なしの競技プログラミングのようなもので、過去に紹介しています。

taq.hatenadiary.jp


そして、Haskell も最近始めました。

taq.hatenadiary.jp

実は割と気に入っていて、何かしたいなと思っていたところ Project Euler の存在を思い出しました。

Python で解いていって記事を書いているのですが、Haskell でも練習がてら解いてみます。
(ここ1年くらい更新が止まっていますが…)

taq.hatenadiary.jp


以下、ネタバレになる (かもしれない) ので、嫌だという方は先に解いてみてください。

projecteuler.net


目次


Problem 1 -Multiples of 3 or 5-

10 より小さい 3 または 5 の倍数を並べると 3 5 6 9 となり、これらの和は 23 である。
1000 より小さい 3 または 5 の倍数の和を求めよ。


main = print $ sum [x | x <- [0..999], x `mod` 3 == 0 || x `mod` 5 == 0]

以上です。簡単。

0 から 999 の中から条件を満たす数を選んでリストに入れていき、最後に各要素の和をとって出力すれば完了です。
答えは 233168 と出力され、正解でした。


ちなみに、sum は以下のように定義できます。

sum' [] = 0
sum' (x:xs) = x + sum' xs

引数で受け取ったリストの先頭を足して残りを sum' に、を繰り返していきます。
引数が空のリストになったら 0 を足して終了です。


Problem 2 -Even Fibonacci Numbers-

フィボナッチ数列のある項は、その前2つの数字を足すことで計算される。
1 2 から始めると、初めの10項は 1 2 3 5 8 13 21 34 55 89 となる。
400万を超えない範囲で考えたとき、偶数のフィボナッチ数を全て足すといくらになるか。


fib = 1 : 2 : zipWith (+) fib (tail fib)
main = print $ sum [x | x <- takeWhile (< 4000000) fib, x `mod` 2 == 0]


フィボナッチ数列を出力するプログラムは、Haskell を紹介したときに書きました。
とりあえず zipWith を使ったものを選びました。
問題文から、リストの1, 2番目の要素は 1 2 になるように変更しています。

fib はリストになっているので、400万より小さい要素を取り出し、2 の倍数 (偶数) のものだけを選び、最後に和をとって完了です。
答えは 4613732 と出力され、正解でした。


ちなみに、takeWhile は以下のように定義できます。

takeWhile' c (x:xs)
   | c x = x : takeWhile' c xs
   | otherwise = []

c は条件式を指し、これをリストの先頭の要素に適用した c x が真のときは、その要素をリストにいれて残りの部分を再び takeWhile' に渡します。
偽のときは空のリストを返し、繰り返しが終わります。

また、ここで使っているのは | を行頭において条件を示す書き方で、数式を

$$ f(x) = \begin{cases} x & (x \geq 0) \\ -x & (x < 0) \end{cases} $$

のように書くときと似ています。
(条件と返り値の位置が逆ですが。)


Problem 3 -Largest Prime Factor-

13195 の素因数は 5 7 13 29 である。
600851475143 の素因数の中で最大のものは何か。


factorize 1 = []
factorize n = f : factorize (n `div` f)
  where f = [x | x <- [2..], n `mod` x == 0] !! 0

main = print $ factorize 600851475143


factorize素因数分解をする関数です。

fn の最小の約数 ([x | x <- [2..], n mod x == 0] !! 0) を受け取っています。
2以上の整数 xn を割り、割り切れた場合 xn の約数だとわかるので f に入れる、という感じです。
これは遅延評価の為せる技で、約数が1つでも見つかれば f に渡せるので無駄な計算が行われません。

f が求まったところで、それをリストにいれるとともに n div ffactorize に渡します。
あとは同じことが繰り返され、n div f1 になったところで終了です。

(「``」がはてなブログと干渉してしまったので、moddiv を囲う記号は説明文では省いてあります。)

素因数分解の結果、[71,839,1471,6857] が出力され、正解でした。


Problem 4 -Largest Palindrome Product-

回文数はどちらから読んでも同じである。
2桁の数の積として表せる最大の回文数は 9009 = 91 × 99 である。
3桁の数の積として表せる最大の回文数は何か。


main = print $ maximum [x | x <- [a * b | a <- [100..999], b <- [100..999]], show x == reverse (show x)]


[a * b | a <- [100..999], b <- [100..999]] で総当たりで積を計算し、show x == reverse (show x)回文数かどうか判定します。
回文数になったものだけをリストにいれ、その中から最大のものを出力します。
答えとして 906609 が出力され、正解でした。

ここで、show は数値を文字列に変換する関数、reverse は順序を逆にする関数です。
(ちなみに、文字列を数値に変換するときは read 文字列 :: Int のようにします。)


reversemaximum は以下のように定義できます。

reverse' [] = []
reverse' (x:xs) = reverse' xs ++ [x]
maximum' [] = []
maximum' (x:xs) = max' x (maximum' xs)
  where max' x y
          | x > y = x
          | otherwise = y


Problem 5 -Smallest Multiple-

2520 は 1 から 10 の数で割り切ることができる最小の数である。
1 から 20 の数で割り切ることができる自然数の中で最小のものは何か。


main = print $ foldr lcm 1 [1..20]


Python で解いたときにも書いていますが、1~20 で割り切れるということは $20!$ かというとそうではなく、最小公倍数を考える必要があります。

引数に渡した2つの数の最小公倍数を求める関数 lcm がありますので、それを利用します。

fn [] = 1
fn (x:xs) = lcm x (fn xs)

main = print $ fn [1..20]

と書けば良いのですが、fn と似た機能を持った関数が既にあります。
それが foldr です。

foldr は以下のような定義ですので、flcmv1 に対応していることになります。

foldr' f v [] = v
foldr' f v (x:xs) = f x (foldr' f v xs)


プログラムを実行すると、答えとして 232792560 が出力され、正解でした。


ちなみに、lcm は以下のように定義できます。

gcd' x y
  | y == 0 = x 
  | otherwise = gcd' y (x `mod` y)

lcm' x y = x * y `div` gcd' x y

ここで gcd' は最大公約数を求める関数で、ユークリッドの互除法 (ユークリッドの互除法 - Wikipedia) を使っています。
定義をそのまま書いたような感じですがうまく動きました。
そして、$\mathrm{gcd} (x, y) \times \mathrm{lcm} (x, y) = a \times b$ と表せるので、$\displaystyle \mathrm{lcm} (x, y) = \frac{a \times b}{\mathrm{gcd} (x, y)}$ で最小公倍数が求められます。


また、foldr の親戚に foldr1 という関数があります。
これは空のリストが渡されるとエラーを吐くようになっており、その一歩手前、すなわち配列の最後の要素を v として計算してくれる foldr です。

foldr1' f [v] = v
foldr1' f (x:xs) = f x (foldr1' f xs)

これを使えば、この問題ではわざわざ v を指定しなくても良くなります。

main = print $ foldr1 lcm [1..20]


Problem 6 -Sum Square Difference-

1 から 10 までの自然数の2乗の和は 385、和の2乗は 3025 である。
よって、この2つの差は 3025 - 385 = 2640 となる。
同じことを 1 から 100 までの自然数で行うと答えはどうなるか。


main = print $ (sum [1..100])^2 - sum [x^2 | x <- [1..100]]


単純に計算すれば良いだけです。
実行すると 25164150 と出力され、正解でした。


Problem 7 -10001st Prime-

初めの6つの素数を並べると 2 3 5 7 11 13 となり、6番目の素数は 13 だとわかる。
10001番目の素数は何か。


sieve [] = []
sieve l@(x:xs) = x : sieve [a | a <- l, a `mod` x /= 0]

main = print $ (sieve [2..]) !! 10000


sieve では 2 以上の整数の無限リストを渡すと、その中から素数だけを返してくれます。
いわゆる「エラトステネスのふるい」を使っており、計算自体は Wikipedia (エラトステネスの篩 - Wikipedia) にあるのと同様です。
受け取ったリストの先頭の数の倍数を除いて、その結果をまた sieve に渡す、という感じです。

l@(x:xs) は、受け取ったリストを変数に束縛しています。
(x:xs) はリストの先頭を x に、残りを xs に入れることを意味しており、@ を挟んで何か変数 (ここでは l) をつければその変数にリスト全体を入れることができます。

答えとして 104743 が出力され、正解でした。


Problem 8 -Largest Product in a Series-

問題文にある1000桁の数の中で隣接した4個の数の積の最大値は 9×9×8×9 = 5832 である。
隣接する13個の数の積の最大値はいくつか。


num = "7316717653133062491922511967442657474235534919493496983520312774506326239578318016984801869478851843858615607891129494954595017379583319528532088055111254069874715852386305071569329096329522744304355766896648950445244523161731856403098711121722383113622298934233803081353362766142828064444866452387493035890729629049156044077239071381051585930796086670172427121883998797908792274921901699720888093776657273330010533678812202354218097512545405947522435258490771167055601360483958644670632441572215539753697817977846174064955149290862569321978468622482839722413756570560574902614079729686524145351004748216637048440319989000889524345065854122758866688116427171479924442928230863465674813919123162824586178664583591245665294765456828489128831426076900422421902267105562632111110937054421750694165896040807198403850962455444362981230987879927244284909188845801561660979191338754992005240636899125607176060588611646710940507754100225698315520005593572972571636269561882670428252483600823257530420752963450"
digit = 13

products = [product (separate x) | x <- adjacents num digit]
  where separate l = [read [l !! i] :: Int | i <- [0..(length l)-1]]
        adjacents l n = [extract l i n | i <- [0..(length l)-n]]
        extract l i n = [l !! (i+x) | x <- [0..n-1], i+x < length l]

main = print $ maximum products


なかなか煩雑になってしまいました。

extractl の中の i 番目の要素から n 個分取り出してくる関数です。
adjacentsl の中から n 個取り出したときに得られる数列全てを返す関数です。
separatel から取り出した数列を1文字ずつに区切り、さらに数値に変換します。
products は取り出した数の各桁の積を計算し、その結果を全て入れておくリストです。
最後に products の中から最大値を探し、出力します。

答えとして 23514624000 が出力され、正解でした。


余談ですが、read 文字列 :: Int の文字列の部分は String 型でないといけないようです。
l !! i だけだと一文字に相当する Char 型として扱われてしまうので [] で囲うことでリスト化し、String 型に変換しています。


Problem 9 -Special Pythagorean Triplet-

ピタゴラス数は以下の条件を満たす3つの数の組み合わせである。
$$ a^{2} + b^{2} = c^{2}, ~ a < b < c $$ 例えば、$3^{2} + 4^{2} = 9 + 16 = 25 = 5^{2}$ となる。
$a + b + c = 1000$ となるピタゴラス数が1組存在するが、その積 $abc$ は何か。


triplets l = [[a, b, c] | m <- [1..l], n <- [1..m], let a = m^2-n^2, let b = 2*m*n, let c = m^2+n^2, a + b + c == l]
main = print $ product $ (triplets 1000) !! 0


Wikipedia (ピタゴラス数 - Wikipedia) から、自然数 $m$ と $n$ について、$m > n$、$(a, b, c) = (m^{2}-n^{2}, 2mn, m^{2}+n^{2})$ を満たせば良いとわかるので、それをそのまま書いたような感じです。
また、各辺の和が 1000 になるという条件もあるので、$a + b + c = 1000$ も追加しています。

[375,200,425] が見つかり、積を計算して 31875000 が出力されます。正解でした。


Problem 10 -Summation of Primes-

10以下の素数の和は 2 + 3 + 5 + 7 = 17 である。
200万以下の素数の和はいくらになるか。


primes = 2 : [x | x <- [3,5..], isPrime x]
  where isPrime n = all (\p -> n `mod` p /= 0) (takeWhile (\p -> p*p <= n) primes)

main = print $ sum $ takeWhile (< 2000000) primes


Problem 7 でつくったエラトステネスのふるいが役に立ちます。
のはずでしたが、10001番目の出力でも若干時間がかかっていたこともあってか、

sieve [] = []
sieve l@(x:xs) = x : sieve [a | a <- l, a `mod` x /= 0]

main = print $ sum $ takeWhile (< 2000000) (sieve [2..])

としても一向に答えが出力されませんでした。


というわけで、初めに載せたような解法を考えました。

試し割り法でもエラトステネスのふるいでも、考える数の最大値の平方根まで試せば良いので、判定する数列は takeWhile (\p -> p*p <= n) primes としています。
ここで、\p -> p*p <= n のような式は「ラムダ式」とよばれる関数で、この式は p を受け取って p*p <= n の真偽を返す関数となっています。
(ラムダ式に関しては記事を作成中です。(いつに公開できるようになるかわかりませんが。))

さらに、2 以外の偶数は全て合成数のはずなので、primes には先に 2 を入れておき、素数かどうかの判定 isPrime は奇数 [3,5..] のみを考えています。

isPrime では、takeWhile (\p -> p*p <= n) primes として取ってきたリスト全体に対して、\p -> nmodp /= 0 が真であった場合に True を返します。

4秒程度で 142913828922 と出力され、正解でした。


ちなみに、all は以下のように定義できます。

all' c [] = True
all' c (x:xs)
  | c x = all' c xs
  | otherwise = False


まとめ

以上、Project Euler の問題を Haskell で解いてみました。

書いてみたらコードゴルフでもしているのかというくらい短くなっておもしろいですね。
でもまだまだ勉強が足りず、検索や ChatGPT に頼りまくってました。

冒頭でも書きましたがやっぱり私はこの言語が好きかもしれません。
続編をお楽しみに。


最後まで読んでいただいてありがとうございました。また次回。

プログラミング言語「Chef」を触ってみる

こんにちは。
今回は、プログラミング言語「Chef」を紹介します。


一風変わったプログラミング言語シリーズ (?) の4つ目です。

プログラミング言語「Piet」を使ってみる - プログラミングの備忘録

プログラミング言語「Uiua」を使ってみる - プログラミングの備忘録

プログラミング言語「Shakespeare」を触ってみる - プログラミングの備忘録


目次


「Chef」とは

Chef (DM's Esoteric Programming Languages - Chef) はプログラミング言語のひとつです。
これは全くの偶然なのですが、Chef の開発者と以前紹介した Piet の開発者は同じ方のようです。

名前の通り料理に関係しており、「コードがレシピに見えるように」というコンセプトがあるそうです。
(実際に「Hello World!」を出力するコードをつくり、そのコード (レシピ) 通りに「Hello Worldケーキ」をつくった方 (Baking a Hello World Cake | Products of Mike's Mind) もいます。)


仕様

DM's Esoteric Programming Languages - Chef の内容を要約していきます。

Chef の "レシピ" には以下の要素が登場します。(一部は無くても良いです。)
各要素はここに示した順で、間に1行の空白行を挟んで書いていきます。

レシピタイトル

プログラムが何をするものなのかを記述します。
1行目でピリオドまでがタイトルになります。

コメント

何かあれば書いてください。

材料リスト

プログラム内で使う変数を材料として示し、各材料は量として数値を指定します。
初期値 単位 材料名 の形で羅列していきます。
(初期値や単位は無く材料名だけを宣言しても良いようです。)

単位と、固体・液体の関係
固体: gkgpinch[es] (つまみ)、heaped (山盛りの)、level (一杯の)
液体: mlldash[es] (少量)
未特定: cup[s]teaspoon[s]tablespoon[s]

Ingredients.
101 eggs
108 g lard
1 cup white sugar
119 ml water

出力においては、液体は Unicode で対応する文字として、固体 or 未特定の材料は数値として出力されます。

調理時間

記載は任意です。
単位は hour[s]minute[s] です。
(コードの作成時間でも書いておけば良いでしょうか。)

Cooking time: 30 minutes.

オーブンの温度

記載は任意ですが、焼く工程があるときは予熱をしておく必要があるでしょう。

degrees Celsius は摂氏温度 [℃]、gas mark はイギリスなどで使われるオーブンの温度単位 (Gas mark - Wikipedia) のようです。
(gas mark は書かなくても良いです。)

Pre-heat oven to 200 degrees Celsius (gas mark 6).

手順

調理手順を記載します。
プログラムの本文にあたります。

ボウルと耐熱皿

Chef にはスタックのような役割を持つボウルと耐熱皿が存在します。

ボウルと耐熱皿は材料を入れることができ、パンケーキを積み重ねるように順番に入っていきます。
材料に対応する値が変わったとしてもこれらの容器の中では材料の値は変わりません。

容器が複数あっても序数 (1st, 2nd, 3rd, 4th, ...) で区別可能です。

命令

以下、命令とその意味です。

  • Take 材料名 from refrigerator.: 入力を受け取り、指定した材料に代入する

  • Put 材料名 into mixing bowl.: ボウルに材料を入れる

  • Fold 材料名 into mixing bowl.: ボウルから一番上の値を取り出し、指定した材料に代入する

  • Add 材料名 [to mixing bowl].: ボウルの一番上の値に指定した材料の値を足す

  • Remove 材料名 [from mixing bowl].: ボウルの一番上の値から指定した材料の値を引く

  • Combine 材料名 [into mixing bowl].: ボウルの一番上の値と指定した材料の値をかける

  • Divide 材料名 [into mixing bowl].: 指定した材料の値をボウルの一番上の値で割る

  • Add dry ingredients [to mixing bowl].: 固体の材料の値を全て足し、ボウルに入れる

  • Liquefy 材料名.: 指定した材料を液体にする

  • Liquefy contents of the mixing bowl. ボウルの中の全ての材料を液体にする
    (液体化は Unicode で対応する文字として出力するためなどに使う)

  • Stir [the mixing bowl] for 数値 minutes.: ボウルの一番上の値を数値の分だけ "ロール" する
    (ロール: 一番上の値を 数値 個下に動かし、それより上の値を上に動かす)

  • Stir 材料名 into the mixing bowl.: ボウルの中の指定した材料を、その値分だけロールする

  • Mix [the mixing bowl] well.: ボウルの中の材料をランダムに並べ替える

  • Clean mixing bowl.: ボウルから全ての材料を消す

  • Pour contents of the mixing bowl into the baking dish.: ボウルの中の全ての材料を耐熱皿にコピーする

  • 動詞 the 材料名.: ループを始める
    ループでは以下を実行する
    1) 材料の値をチェック
    2-1) 0でなければ、"until" に達するまで処理が続く
    2-2) 0であれば、ループを抜けて "until" の後の命令を実行する

  • 動詞 [the 材料名] until 動詞(過去形?).: ループ区間の終わりを示す
    過去形 (?) の動詞は、ループ開始時の動詞と同じものを使う
    材料名が指定された場合、これが実行されるたびに材料の値を -1 する

  • Set aside.: 最も内側のループが終了し、"until" の後の命令が実行される

  • Serve with 補助レシピ.: 副料理長を呼び、補助レシピ (後述) を実行する

  • Refrigerate [for 数値 hours].: 処理を終了する
    補助レシピ内なら副料理長からボウルを受け取る
    時間が指定された場合は、耐熱皿の値を出力する

Method.
Sift the flour.
Put flour into mixing bowl.
Serve with caramel sauce.
Stir for 2 minutes.
Remove egg.
Rub the flour until sifted.
Stir for 2 minutes.
Fold the butter into the mixing bowl.
Pour contents of the mixing bowl into the baking dish.

提供

全ての耐熱皿が空くまで、一番上の値を取り出しては出力、を繰り返します。

記載は任意ですが、出力をしたい場合は必須です。

Serves 数値.

補助レシピ

特殊な材料 (ソースなど) を得るために必要な小さなレシピです。
補助レシピはメインのレシピの後に続けて書きます。

メインレシピのどこかで例えばソースを呼び出したとき、副料理長が料理長のボウルや耐熱皿の中身をコピーしてソースをつくり始めます。
副料理長の仕事は料理長のボウルなどには反映されませんが、調理が終わると1つ目のボウルの中身を料理長に渡します。


実行環境

Python でもいけるようなことが書いてありましたが、やり方わからず。

ブラウザ上で動かせるものがあったのでそちらを利用すると良いでしょう。

Chef | Esolang Park


コーディング

ここからは、実際にいろいろつくってみます。

Hello World!

Hello World Cake.

Tutorial of Chef.

Ingredients.
32 g butter
87 g flour
111 ml water
114 g egg
108 ml milk
100 g sugar
33 g salt

Method.
Put salt into the mixing bowl.
Put sugar into the mixing bowl.
Put milk into the mixing bowl.
Put egg into the mixing bowl.
Put water into the mixing bowl.
Put flour into the mixing bowl.
Put butter into the mixing bowl.
Serve with toppings.
Liquefy contents of the mixing bowl.
Pour contents of the mixing bowl into the baking dish.

Serves 1.

toppings.

Ingredients.
72 g cherry
101 g strawberry
108 g orange
108 g melon
111 g grape

Method.
Clean the mixing bowl.
Put grape into the mixing bowl.
Put melon into the mixing bowl.
Put orange into the mixing bowl.
Put strawberry into the mixing bowl.
Put cherry into the mixing bowl.
Refrigerate.

ケーキ本体とトッピングで分け、それぞれ「 World!」と「Hello」を出力するようにしてみました。
ここで、「Hello World Cake」はメインレシピ、「toppings」は補助レシピにあたります。

プログラム自体は文字に対応する数をそのまま材料の数量として、ひたすらボウルに入れた後に出力する、という単純なものです。


フィボナッチ数列

Fibonacci Chocolate.

Ingredients.
10 ml milk
1 chocolate

Method.
Put milk into the mixing bowl.
Put milk into the 2nd mixing bowl.
Remove chocolate from the mixing bowl.
Remove chocolate from the mixing bowl.
Fold milk into the mixing bowl.
Put chocolate into the mixing bowl.
Put chocolate into the mixing bowl.
Shake the milk.
Serve with butter.
Shake the milk until shake.
Fold milk into the 2nd mixing bowl.
Transfer the milk.
Fold chocolate into the mixing bowl.
Put chocolate into the 2nd mixing bowl.
Transfer the milk until transfer.
Pour contents of the 2nd mixing bowl into the baking dish.

Serves 1.

butter.

Ingredients.
1 ml milk
1 g salt

Method.
Fold milk into the mixing bowl.
Fold salt into the mixing bowl.
Clean the mixing bowl.
Put milk into the mixing bowl.
Add salt to the mixing bowl.

チョコレートのバター乗せみたいな変なものになりましたが、同時に milk 項分のフィボナッチ数列の出力ができるようになりました。
(初めの構想ではチョコレートが増殖する感じにしたかったのですが、結局バターが増殖するようになりました。)

流れ
初めにボウルに chocolate を2つ入れます。
ここで副料理長にバトンタッチし、牛乳を振りながらボウルから上2つを取り出しては足し、を繰り返して料理長に渡します。
これでフィボナッチチョコレートはほぼできているのですが、提供の順番は小さいものからにしたいので、2つ目のボウルを使って順番を逆にしています。

先程紹介したサイトではプログラムを段階的に進めることもできますので、どこで何が起こっているかわかりやすいかと思います。


まとめ

以上、Chef を触ってみました。

一風変わった言語シリーズの中では単純な方なのではないでしょうか。
ですが、if文がないようなのでできることは限られるかもしれません。
(こちら (If in esoteric programming language "chef") を見るとできなくはなさそうですが、どう実装すれば良いかわからず。)


「chef プログラミング言語」と調べるとサーバー構築 (?) かなんかの別の「Chef」がヒットしてしまい日本語の記事は見つけられなかったので、Chef やりたいけど英語やだという方はこの記事が助けになれば幸いです。
(英語の記事はたくさんありましたので問題ない方はそちらの方がいいかもしれません。)


最後まで読んでいただいてありがとうございました。また次回。