以下の内容はhttps://taq.hatenadiary.jp/より取得しました。


「A=B」をやってみる

こんにちは。
今回は「A=B」というゲームをやってみます。


ゲーム紹介シリーズです。
Steamのおすすめに載っていて、説明欄を読んでみると「命令が1つしかないプログラミングゲームです」とあったので興味をひきました。
もともと580円ですが、セールで406円だったので買ってみました。


「A=B」とは

「A=B」は string1 = string2 (最も左にある string1string2 に置き換える) という命令しかないプログラミング言語を用いて、様々な課題を解いていくパズルゲームです。

このゲームにおける実行フローは、
1. 入力文字列の読み込み
2. 実行可能な最初の命令を上から下に向かって探していく
2.1 実行可能な命令がある場合、現在の文字列を置き換える
2.2 実行可能な命令がない場合、実行を終了して現在の文字列を出力する
となっているようです。

百聞は一見に如かずということで、実際に見てみましょう。


実際に遊んでみる

ゲームを起動するとこんな感じです。

1-1

最初の課題「1-1 AをBに」を開くと、

左上に課題の説明があります。
今回は、すべての ab に置き換えれば良いようです。

右上は入力文字列と、目標とする文字列が表示されています。

右下はコードを書く部分です。
すでに a=b と書かれています。


では、実行してみましょう。

左下にはログが表示され、abca=b が適用され、bbc となったことがわかります。
そして、実行可能な命令が無くなったので、bbc が出力されています。


テストケースがいくつかあるようで、実行し続けていくと「Accepted.」と表示されました。


1-2

2つ目の課題では、すべての文字を大文字に置き換えます。

コードは以下で良さそうです。

a=A
b=B
c=C

実行すると、「Accepted.」となりました。


初めに説明した実行フローをもう少し具体的に言うと、コードの1行1行について入力文字列をループし、一通り終わったら次の行へ、と進んでいく、ということです。
つまり、例えばテストケースが abcabc であった場合、まず a=A を適用しながらループするので AbcAbc となります。
続いて b=B を適用するので ABcABc、そして c=CABCABC となり、実行できる命令が無くなったので終了、という感じです。


以降は悩んだ課題や新要素を取り上げて紹介します。


1-4

課題1-4では、連続した a を削除します。

入力文字列は最大で7文字とあるので、ごり押しで…

aaaaaaa=
aaaaaa=
aaaaa=
aaaa=
aaa=
aa=

一応クリアはできますが、チャレンジ要素として「追加の目的」があり、今回はコードを2行以下に収める必要があるようです。


考えてみましょう。
最短の連続は aa なので、aa= で無に置き換える操作は必要そうです。
これだけでは、偶数個の連続は処理できますが、奇数個の連続は a が余ってしまいます。
そこで、aaa=aa とすることによって、aaaaaaaaa のように偶数個の連続に変換することができます。
文字列には偶数個の連続しか残っていないので、最後に aa= を実行すれば解決です。

aaa=aa
aa=

クリアできました。


1-6

課題1-6では、文字列中の ab の数を比較して、多い方を出力します。

この記事には載せていませんがひとつ前の課題でソートを扱ったので、使うと予想できます。

一旦、ソートのために ba=ab を入れておきましょう。
数が多い方の文字を残したいので、ab=ab対消滅させていきます。
最後に、1個だけ残すように aa=abb=b で数を減らしていけば、完成です。

ba=ab
ab=
aa=a
bb=b

クリアです。追加の目的もクリアできてました。

この辺りから難しくなってきますが、同時に「A=B」という命令の可能性を感じてきました。


2-1

一応のストーリーとして、講義の課題としてこのプログラミング言語を提出しようとしているようですが、教授から「「helloworld」を出力できないならプログラミング言語ではない」と言われてしまいます。
そこで、新たな命令が追加されました。

string1=(return)string2string1 にマッチした場合、プログラムを直ちに終了し、string2 を出力する

つまり、a というテストケースに対して a=(return)helloworld とすることで helloworld を (少々無理矢理にも感じますが) 出力できる、ということです。


2-3

課題2-3では、入力文字列が3文字か判定します。

文字数の判定において ab の区別があるのは扱いづらいので、b=ac=aa に統一することにします。
こうしてしまえば、4文字以上2文字以下で false、3文字で true を出力するようにすれば良いだけです。

ちなみに、左辺を無にすることによりどんな場合でも実行するようにできます。

b=a
c=a
aaaa=(return)false
aaa=(return)true
=(return)false


2-6

課題2-6では、両隣の文字と異なる文字が1文字だけの場合に true、そうでなければ false を出力します。
日本語が難しいですが、ある文字について、その両隣を見たときにその文字と同じかどうか判定し、入力文字列の各文字すべてで見たときに、「異なる」と判定されたものが1文字であったら true を出力します。

具体的に言うと…
aabcc の場合、1文字目の a は隣が a なので「同じ」、2文字目の a は隣が ab なので「同じ」となりますが、3文字目の b は隣が ac なので「異なる」と判定されます。4、5文字目は「同じ」判定になるので、「異なる」判定の文字が1つしかないことなります。よって、このテストケースでは true です。
babbcc の場合、1文字目の b と2文字目の a が「異なる」判定になるので、「異なる」判定が2文字あり false になります。

さらに言ってしまえば、2個以上連続している文字が「同じ」判定になり、孤立した文字が「異なる」判定になるので…
孤立した文字が1文字だけのとき true、そうれなければ false を出力すれば良い
ということになります。


それではこれをどう判定するか考えます。
まずは、2個以上連続した文字は消せるので、1-4 でやったように、

aaa=aa
aa=

他の文字についても然りです。

しかし、例えば ccaaac のような場合、aaa を消してしまうと最後に孤立している c が降りてきて、ccc となってしまいます。
こうなると、ccc も消せるので最終的に無になり、本来 true を出力すべきものが false と判定されてしまいます。

これを回避するために、aa=o のように別の文字を挟み、後ろの文字と連続しないようにします。

最後に o= を実行して挟んだ文字を消すことにより、孤立した文字だけを残すことができます。


続いて孤立した文字が1文字だけかどうか判定しますが、これは 2-3 でやったやり方を参考にすれば良いです。

aa=(return)false
a=(return)true
=(return)false

のようにすれば1文字の時だけ true を出力できますが、aa=o と干渉してしまいます。
そこで、a=xb=xc=x のように変換しておけば、

xx=(return)false
x=(return)true
=(return)false

と書けるので干渉しなくなります。


以上で完成です。
まとめると、

aaa=aa
aa=o
bbb=bb
bb=o
ccc=cc
cc=o
a=x
b=x
c=x
o=
xx=(return)false
x=(return)true
=(return)false

クリアです。追加の目的も達成してました。


まとめ

以上、「A=B」の紹介をしました。

ぶつ切りで申し訳ありませんが、だんだん難しくなってきたので一旦ここまでとします。
かなり骨太なパズルゲームなので、長いこと遊べるのではないかと思います。
以前に紹介した「ABI-DOS」をやっているときの感覚を思い出しました。

今ならワンコインで買えますので、皆さんもぜひやってみてください。


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

自分でコードを書く時代は終わった? -VSCodeのCopilotを使ってみる-

最近の記事はゲームばかりで、コードの書き方を忘れてしまうのではないかと思っていたのですが、それに拍車がかかってしまうようなできことが。


Processingのコミュニティで「VScodeにProcessingの拡張機能が登場した」という書き込みがありました。

note.com

そして、Processing純正(?)のIDEでコードを書くことが多かったので気づいていなかったのですが、VSCodeでAIを活用しやすくなっていたようです。

code.visualstudio.com

つまり、VSCode上でAIにProcessingを書かせることができる、ということです。


そんな訳で、どんなものか使ってみました。


実際にやってみた

導入方法の詳細は他に譲るとして、諸々入れて開いてみるとこんな感じです。


Processing界の「Hello World」こと(?)、円を描くプログラムを作ってみます。

右側のチャット欄で「円を描くプログラムを書いてください」と入力してみると…

void setup() {
  size(600, 400);
  smooth();
  noLoop();
}

void draw() {
  background(255);
  fill(100, 150, 255);
  stroke(0);
  strokeWeight(2);
  // 中心に半径100の円(直径200)を描く
  ellipse(width/2, height/2, 200, 200);
}

ellipse() だけでなく、色や線幅など変えられるようになっています。
smooth()noLoop()、コメントもつけてくれました。

AIに裏で実行してもらいながら、自分は別のウィンドウを開いて作業をしていたところ、作成が完了した旨のポップアップメッセージが表示されました。便利です。


中央右上にある再生ボタンを押すと、実行できます。


「画像として保存できるようにしてください」とお願いしてみると、

void setup() {
  size(600, 400);
  smooth();
  noLoop();
}

void draw() {
  background(255);
  fill(100, 150, 255);
  stroke(0);
  strokeWeight(2);
  // 中心に半径100の円(直径200)を描く
  ellipse(width/2, height/2, 200, 200);
  
  // 画像として保存
  save("circle.png");
}

save() をつけてくれました。


ちょっと冒険

円を描くくらいはできてくれないと困りますよね。
もう少しレベルを上げてみます。


「ボールが落下してバウンドする様子を再現してください」
ついでにGIFファイルで録画できるようにとお願いすると、ライブラリ「gifAnimation」を使って実装してくれました。

import gifAnimation.*;

float x, y;
float vx, vy;
float r = 40;
float gravity = 0.8;
float restitution = 0.75;

GifMaker gif;
boolean recording = false;
int delayMs = 16; // フレーム遅延(ms)

void setup() {
  size(600, 400);
  smooth();
  frameRate(60);
  resetBall();
}

void draw() {
  background(255);

  // 床のライン
  stroke(200);
  line(0, height-1, width, height-1);

  // 物理更新
  vy += gravity;
  x += vx;
  y += vy;

  // 床との衝突
  if (y + r > height) {
    y = height - r;
    vy *= -restitution;
    if (abs(vy) < 0.5) {
      vy = 0;
    }
  }

  // 左右の壁で反射
  if (x - r < 0) {
    x = r;
    vx *= -restitution;
  } else if (x + r > width) {
    x = width - r;
    vx *= -restitution;
  }

  // 描画
  fill(100, 150, 255);
  stroke(0);
  strokeWeight(2);
  ellipse(x, y, r*2, r*2);

  // 録画中はフレームを GIF に追加
  if (recording && gif != null) {
    gif.addFrame();
  }
}

// クリックでボールをその位置にリセット
void mousePressed() {
  x = constrain(mouseX, r, width - r);
  y = constrain(mouseY, r, height - r);
  vx = random(-4, 4);
  vy = 0;
}

void keyPressed() {
  // sキー:現在フレームを1枚保存 (PNG)
  if (key == 's' || key == 'S') {
    saveFrame("single-####.png");
    println("Saved single frame");
  }

  // rキー:録画開始/停止(GIF)
  if (key == 'r' || key == 'R') {
    if (!recording) {
      String name = "bounce-" + year() + nf(month(),2) + nf(day(),2) + "-" + nf(hour(),2) + nf(minute(),2) + nf(second(),2) + ".gif";
      gif = new GifMaker(this, name);
      gif.setRepeat(0); // 0 = 無限ループ
      gif.setDelay(delayMs);
      recording = true;
      println("Recording GIF -> " + name);
    } else {
      gif.finish();
      gif = null;
      recording = false;
      println("Recording finished");
    }
  }

  // cキー:録画キャンセル(未finishならfinishして破棄)
  if (key == 'c' || key == 'C') {
    if (gif != null) {
      gif.finish();
      gif = null;
    }
    recording = false;
    println("Recording cancelled");
  }
}

void resetBall() {
  x = width / 2;
  y = r;
  vx = 2;
  vy = 0;
}

ちゃんとできています。
ご丁寧に床まで描画してくれました。


もう少しいけそうです。

「ボールを5個にして、それぞれの衝突も組み込んでください」

import gifAnimation.*;
import processing.core.PVector;

final int N_BALLS = 5;
float gravity = 0.8;
float globalRestitution = 0.85;

GifMaker gif;
boolean recording = false;
int delayMs = 16; // GIF フレーム遅延 (ms)

Ball[] balls = new Ball[N_BALLS];

void setup() {
  size(600, 400);
  smooth();
  frameRate(60);
  initBalls();
}

void draw() {
  background(255);

  // 床のライン
  stroke(200);
  line(0, height-1, width, height-1);

  // 物理更新
  for (int i = 0; i < N_BALLS; i++) {
    balls[i].applyGravity();
    balls[i].update();
    balls[i].checkWalls();
  }

  // ペアごとの衝突判定と解決
  for (int i = 0; i < N_BALLS; i++) {
    for (int j = i + 1; j < N_BALLS; j++) {
      resolveCollision(balls[i], balls[j]);
    }
  }

  // 描画
  for (int i = 0; i < N_BALLS; i++) {
    balls[i].display();
  }

  // 録画中はフレームを GIF に追加
  if (recording && gif != null) {
    gif.addFrame();
  }
}

void mousePressed() {
  // クリック位置に最も近いボールをリセット(簡易)
  int idx = 0;
  float best = 1e9;
  for (int i = 0; i < N_BALLS; i++) {
    float d = dist(mouseX, mouseY, balls[i].pos.x, balls[i].pos.y);
    if (d < best) { best = d; idx = i; }
  }
  balls[idx].pos.x = constrain(mouseX, balls[idx].r, width - balls[idx].r);
  balls[idx].pos.y = constrain(mouseY, balls[idx].r, height - balls[idx].r);
  balls[idx].vel.set(random(-3, 3), random(-2, 0));
}

void keyPressed() {
  if (key == 's' || key == 'S') {
    saveFrame("single-####.png");
    println("Saved single frame");
  }
  if (key == 'r' || key == 'R') {
    if (!recording) {
      String name = "bounce-" + year() + nf(month(),2) + nf(day(),2) + "-" + nf(hour(),2) + nf(minute(),2) + nf(second(),2) + ".gif";
      gif = new GifMaker(this, name);
      gif.setRepeat(0);
      gif.setDelay(delayMs);
      recording = true;
      println("Recording GIF -> " + name);
    } else {
      gif.finish();
      gif = null;
      recording = false;
      println("Recording finished");
    }
  }
  if (key == 'c' || key == 'C') {
    if (gif != null) {
      gif.finish();
      gif = null;
    }
    recording = false;
    println("Recording cancelled");
  }
}

void initBalls() {
  // 異なるサイズ・色で5個生成。重ならないように位置決め。
  int attemptsLimit = 1000;
  for (int i = 0; i < N_BALLS; i++) {
    float r = random(20, 40);
    color c = color(random(50,200), random(50,200), random(50,255));
    Ball b = new Ball(random(r, width - r), random(r, height/2), random(-2,2), 0, r, c);
    int attempts = 0;
    while (overlapsAny(b, i) && attempts < attemptsLimit) {
      b.pos.x = random(r, width - r);
      b.pos.y = random(r, height/2);
      attempts++;
    }
    balls[i] = b;
  }
}

boolean overlapsAny(Ball b, int uptoIndex) {
  for (int j = 0; j < uptoIndex; j++) {
    if (balls[j] != null) {
      if (PVector.dist(b.pos, balls[j].pos) < b.r + balls[j].r) return true;
    }
  }
  return false;
}

void resolveCollision(Ball a, Ball b) {
  PVector n = PVector.sub(b.pos, a.pos);
  float dist = n.mag();
  float minDist = a.r + b.r;
  if (dist <= 0.0001) {
    // 完全に重なった場合ごくわずかにずらす
    n.set(random(0.01f,0.02f), 0.01f);
    dist = n.mag();
  }
  if (dist < minDist) {
    // 位置補正(penetration resolution)
    float penetration = minDist - dist;
    PVector un = n.copy().normalize();
    float totalInvMass = 1.0f / a.m + 1.0f / b.m;
    // それぞれの移動量は反比例(質量に応じる)
    PVector correctionA = un.copy().mult(-penetration * (1.0f / a.m) / totalInvMass * 0.5f);
    PVector correctionB = un.copy().mult(penetration * (1.0f / b.m) / totalInvMass * 0.5f);
    a.pos.add(correctionA);
    b.pos.add(correctionB);

    // 相対速度
    PVector rv = PVector.sub(b.vel, a.vel);
    float velAlongNormal = rv.dot(un);
    if (velAlongNormal > 0) return; // 既に離れている

    // 反発係数は2つの平均で簡易化
    float e = (globalRestitution);

    float j = -(1 + e) * velAlongNormal / totalInvMass;
    PVector impulse = un.copy().mult(j);
    a.vel.sub(PVector.mult(impulse, 1.0f / a.m));
    b.vel.add(PVector.mult(impulse, 1.0f / b.m));
  }
}

class Ball {
  PVector pos;
  PVector vel;
  float r;
  float m;
  color col;

  Ball(float x, float y, float vx, float vy, float r_, color c) {
    pos = new PVector(x, y);
    vel = new PVector(vx, vy);
    r = r_;
    col = c;
    // 質量は面積比例 (r^2) の簡易化
    m = r * r;
  }

  void applyGravity() {
    vel.y += gravity;
  }

  void update() {
    pos.add(vel);
  }

  void checkWalls() {
    // 床
    if (pos.y + r > height) {
      pos.y = height - r;
      vel.y *= -globalRestitution;
      if (abs(vel.y) < 0.5) vel.y = 0;
    }
    // 天井
    if (pos.y - r < 0) {
      pos.y = r;
      vel.y *= -globalRestitution;
    }
    // 左右
    if (pos.x - r < 0) {
      pos.x = r;
      vel.x *= -globalRestitution;
    } else if (pos.x + r > width) {
      pos.x = width - r;
      vel.x *= -globalRestitution;
    }
  }

  void display() {
    fill(col);
    stroke(0);
    strokeWeight(2);
    ellipse(pos.x, pos.y, r*2, r*2);
  }
}

まじですか。
クラスも使えてます。


こいつはできる…。

「ボールを小さくして100個に増やし、マウスカーソルと反発するようにしてください」

import gifAnimation.*;
import processing.core.PVector;

final int N_BALLS = 100;
float gravity = 0.6;
float globalRestitution = 0.88;

GifMaker gif;
boolean recording = false;
int delayMs = 16; // GIF フレーム遅延 (ms)

Ball[] balls = new Ball[N_BALLS];

// マウス反発設定
boolean repelOn = true;
float repelRadius = 80;
float repelStrength = 200; // 調整して強さを変える

void setup() {
  size(600, 400);
  smooth();
  frameRate(60);
  initBalls();
}

void draw() {
  background(255);

  // 床のライン
  stroke(220);
  line(0, height-1, width, height-1);

  // 力の適用(重力 + マウス反発)
  for (int i = 0; i < N_BALLS; i++) {
    balls[i].applyGravity();
    if (repelOn) applyMouseRepel(balls[i]);
    balls[i].update();
    balls[i].checkWalls();
  }

  // 衝突判定・解決(N^2)
  for (int i = 0; i < N_BALLS; i++) {
    for (int j = i + 1; j < N_BALLS; j++) {
      resolveCollision(balls[i], balls[j]);
    }
  }

  // 描画
  for (int i = 0; i < N_BALLS; i++) {
    balls[i].display();
  }

  // 録画中はフレームを GIF に追加
  if (recording && gif != null) {
    gif.addFrame();
  }
}

void applyMouseRepel(Ball b) {
  PVector mouse = new PVector(mouseX, mouseY);
  PVector dir = PVector.sub(b.pos, mouse);
  float d = dir.mag();
  if (d > 0 && d < repelRadius) {
    dir.normalize();
    // 距離に応じて線形に弱くする(0 at r, max at 0)
    float strength = repelStrength * (1.0 - d / repelRadius);
    // 加速度 = Force / mass (質量は r^2)
    PVector accel = dir.mult(strength / b.m);
    b.vel.add(accel);
  }
}

void mousePressed() {
  // クリック位置に最も近いボールをリセット
  int idx = 0;
  float best = 1e9;
  for (int i = 0; i < N_BALLS; i++) {
    float dista = dist(mouseX, mouseY, balls[i].pos.x, balls[i].pos.y);
    if (dista < best) { best = dista; idx = i; }
  }
  balls[idx].pos.x = constrain(mouseX, balls[idx].r, width - balls[idx].r);
  balls[idx].pos.y = constrain(mouseY, balls[idx].r, height - balls[idx].r);
  balls[idx].vel.set(random(-2, 2), random(-2, 0));
}

void keyPressed() {
  if (key == 's' || key == 'S') {
    saveFrame("single-####.png");
    println("Saved single frame");
  }
  if (key == 'r' || key == 'R') {
    if (!recording) {
      String name = "bounce-" + year() + nf(month(),2) + nf(day(),2) + "-" + nf(hour(),2) + nf(minute(),2) + nf(second(),2) + ".gif";
      gif = new GifMaker(this, name);
      gif.setRepeat(0);
      gif.setDelay(delayMs);
      recording = true;
      println("Recording GIF -> " + name);
    } else {
      gif.finish();
      gif = null;
      recording = false;
      println("Recording finished");
    }
  }
  if (key == 'c' || key == 'C') {
    if (gif != null) {
      gif.finish();
      gif = null;
    }
    recording = false;
    println("Recording cancelled");
  }
  if (key == 'm' || key == 'M') {
    repelOn = !repelOn;
    println("Mouse repel: " + (repelOn ? "ON" : "OFF"));
  }
}

void initBalls() {
  int attemptsLimit = 2000;
  for (int i = 0; i < N_BALLS; i++) {
    float r = random(6, 12); // 小さめ
    color c = color(random(80,220), random(80,220), random(100,255), 200);
    Ball b = new Ball(random(r, width - r), random(r, height/3), random(-1.5,1.5), 0, r, c);
    int attempts = 0;
    while (overlapsAny(b, i) && attempts < attemptsLimit) {
      b.pos.x = random(r, width - r);
      b.pos.y = random(r, height/3);
      attempts++;
    }
    balls[i] = b;
  }
}

boolean overlapsAny(Ball b, int uptoIndex) {
  for (int j = 0; j < uptoIndex; j++) {
    if (balls[j] != null) {
      if (PVector.dist(b.pos, balls[j].pos) < b.r + balls[j].r) return true;
    }
  }
  return false;
}

void resolveCollision(Ball a, Ball b) {
  PVector n = PVector.sub(b.pos, a.pos);
  float dist = n.mag();
  float minDist = a.r + b.r;
  if (dist <= 0.0001) {
    n.set(random(0.01f,0.02f), 0.01f);
    dist = n.mag();
  }
  if (dist < minDist) {
    float penetration = minDist - dist;
    PVector un = n.copy().normalize();
    float totalInvMass = 1.0f / a.m + 1.0f / b.m;
    PVector correctionA = un.copy().mult(-penetration * (1.0f / a.m) / totalInvMass * 0.5f);
    PVector correctionB = un.copy().mult(penetration * (1.0f / b.m) / totalInvMass * 0.5f);
    a.pos.add(correctionA);
    b.pos.add(correctionB);

    PVector rv = PVector.sub(b.vel, a.vel);
    float velAlongNormal = rv.dot(un);
    if (velAlongNormal > 0) return;

    float e = globalRestitution;
    float j = -(1 + e) * velAlongNormal / totalInvMass;
    PVector impulse = un.copy().mult(j);
    a.vel.sub(PVector.mult(impulse, 1.0f / a.m));
    b.vel.add(PVector.mult(impulse, 1.0f / b.m));
  }
}

class Ball {
  PVector pos;
  PVector vel;
  float r;
  float m;
  color col;

  Ball(float x, float y, float vx, float vy, float r_, color c) {
    pos = new PVector(x, y);
    vel = new PVector(vx, vy);
    r = r_;
    col = c;
    m = r * r; // 質量は面積比例
  }

  void applyGravity() {
    vel.y += gravity;
  }

  void update() {
    pos.add(vel);
  }

  void checkWalls() {
    if (pos.y + r > height) {
      pos.y = height - r;
      vel.y *= -globalRestitution;
      if (abs(vel.y) < 0.4) vel.y = 0;
    }
    if (pos.y - r < 0) {
      pos.y = r;
      vel.y *= -globalRestitution;
    }
    if (pos.x - r < 0) {
      pos.x = r;
      vel.x *= -globalRestitution;
    } else if (pos.x + r > width) {
      pos.x = width - r;
      vel.x *= -globalRestitution;
    }
  }

  void display() {
    fill(col);
    stroke(0, 120);
    strokeWeight(1);
    ellipse(pos.x, pos.y, r*2, r*2);
  }
}

思わず笑ってしまいました。


できてしまいそうだったのでレナード・ジョーンズポテンシャルを導入してみてもらったのですが、それっぽい描画はされたものの、いろいろお願いしているうちに使用回数の上限が来てしまいました…。

徐々にボール同士が近づいていくのが見える…と思います。
ファイルサイズの関係でここに載せられないのですが、温度 (ボールの速さ) に対応する値を上げていくとボールの動きが激しくなり、下げていくとボールの動きがおとなしくなり徐々にまとまっていく動きが見えました。


まとめ

以上、VSCodeとAIのタッグで遊んでみました。

実は他にも、ライフゲームチューリングパターン、スペクトログラムやなんかも試してみていたのですが、どれもそれっぽく動くものができていてびっくりしました。
正直なめていました。少し前は自分でデバッグしないとまともに動かないものばかり吐き出していたのに…。


「自分でコードを書く時代は終わった?」なんてタイトルに書いていますが、私の動機は「モノの仕組みを知りたい」という部分にありますので、AIに全部お任せ、ということにはならないと思います。
(元々、AIの言っていることを鵜呑みにするスタンスでは無いので、コードを生成できたとしてもどう動いているかとか確認しながら使うことになると思いますが)


ちなみに、今回は利用回数の制限が来てしまいましたが、「GitHub Copilot Pro」に登録すれば月10ドル、年100ドルで無制限で使えます (他にもいろいろ特典があるようです)。
30日間の無料お試しもあるみたいですので、興味があればぜひ。


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

「農家は Replace() されました」をやろう

こんにちは。
今回は、「農家は Replace() されました」というゲームを紹介します。


直近の数記事はほぼこれになってしまっていますが、ゲーム紹介シリーズです。
今回はちゃんとプログラミングが関わりますので許してください。

夜勤用の生活リズムにするために少し夜更かしする必要があったのですが、何か集中できそうなゲームをと思っていたところちょうどこのゲームの紹介記事を見たことを思い出し、買ってしまいました。
(結局、裏で農業しつつ Apex をやる、という形に落ち着いてしまいましたが…)


「農家は Replace() されました」とは、ずばり農業自動化ゲームです。

ドローンにプログラミングし、植えて収穫を繰り返します。
ゲームを進めていくと作物の種類や、水や肥料といった農業的な要素を解放できるようになりますが、while演算子、配列といったプログラミング的な要素も解放され、より効率的な農業にできます。
時には、密集しすぎていると成長が遅くなってしまうとか、昇順に並んでいると収穫量が増えるといった条件がある作物もあるので、そのあたりも考慮しつつコードを書く必要があります。

使われている言語は "Pythonっぽい" もので、Python で使えてもこのゲームでは使えない関数などがあります。
また、VSCode を使ったりできるようですが、ゲーム内のエディタでもサジェストを出してくれるので結構使いやすいです。

地味に、グラフィックや効果音も好きな点のひとつです。


後半では、長くなっていくヘビのゲームみたいなものや、迷路を探索して宝箱を見つけたりといった、農業関係ない別ゲーも始まります。

一般的なプログラミング能力を鍛えられている感じがして、これはこれで面白いです。


最終的には完全自動化・効率化を追求していくことになろうかと思いますが、「リーダーボード」機能もあるのでモチベになります。

試しに、ドローン1機で8×8の迷路を解く速さを競う「Maze_Single」をやってみましたが、シンプルに右手法でもランキングに載りました。

特に迷路なんかはプログラミングのベタ問だと思いますので、アルゴリズムの勉強にもなりそうです。


まとめ

以上、「農家は Replace() されました」というゲームを紹介しました。

リリースされたばかりですので、この後もどんどん要素が増えていくのではないでしょうか。

現在 (2025/10/03)、リリース記念セールで20%オフの960円で売っていますので、ぜひ。

store.steampowered.com

「shapez 2」で全自動工場をつくる

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


以前、shapez 2 を紹介しました。
「2」で3Dになったことで「1」とは違った面白さが出て、見た目も良いのでめちゃめちゃハマってしまいました。

taq.hatenadiary.jp

ストーリモード()はすっ飛ばしまして、ランダム図形に対応した全自動工場 (Make Anythig Machine; MAM) をつくります。


目次


はじめに

本記事では、ROS#0、つまり結晶が含まれない方のランダム図形に対応したMAMをつくっていきます。

割と規模が大きなものになりますので、設計図のようなものがあると良いと思います。
なんとなくでもこんな配置にして、各工程はこんな形で、など。

遠くにつくって、後で近くに移転させてもいいかもしれません。
ゲームモードによってはブループリントポイントが不要なので、コピペし放題です。

また、ここに紹介する工場が正解というわけでは無いので、「もっとこうした方が良い」などあれば、皆さんの環境で改良してみてください。


全自動工場の流れ

「1」のときとだいたい同じです。

taq.hatenadiary.jp


1. 素材を集める
2. 必要な図形を取り出す
3. 着色する
4. 1層分を組み合わせる
5. 積層させる
6. 完成


参考までに、私が建てた工場はこんな感じになっています。
列車が走りすぎていますが、気にしないでください。

「1」のときとひとつ違うとすれば、色の素材が液体になったので、必要な色の判断と着色を同時にやるようになっています。


素材を集める

素材は適当なところから掘り出して、列車で運んできます。

図形は丸、四角、星、扇、そして「2」で追加された「ピン」の5つです。
右上4分の1のみを素材として使うつくりにしたので、前4つについてはどこからでも持ってこれると思います。

「2」で追加されたピンですが、通常の図形として取り扱うことができるので、図形の種類が1個増えたと思ってもらうくらいで問題ないです。
ピンのみの図形は、作り方はいろいろありますが、私はこんな感じで作っています。

適当な図形に4回ピンを押すと、積層の上限が4層なので5層目が消え、ピンの山ができます。
これを適当な色で結晶化すると、全体が結晶になります。
さらにピンを押すと、上限の4層を超えてしまうので、5層目が消えます。
ここで、結晶はその仕様上、一部が欠けると全体が壊れてしまうので、最後に押したピンのみが残ります。
あとはこれを右上4分の1のみにすれば、今回の工場で扱える形になります。


色についても同様に集めます。
混色は事前に済ませておいた方がいいのではないかと思って、RGB以外も揃えておきました。


必要な図形を取り出す

ここが肝です。

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


ランダム図形は、「オペレーター信号レシーバー」から受け取ることができます。
今回は「#0」の方に合わせておきます。

また、「グローバル信号送信機」と「グローバル信号レシーバー」があり (日本語訳統一して欲しいけど)、同じチャンネルで情報のやりとりができます。
ここでは、ランダム図形を1層ごとに分け、下から1、2、3、4のチャンネルに割り振っておきます。


「図形分析器」などシミュレーション設備を使って各層の4分の1ずつの情報を取得し、必要な素材を取り出せるようにします。
この辺りは「1」のときと似たような感じです。


着色する

着色の処理は「1」のときとは少し違い、各色について塗るべき色ならば着色機へ、違う色であれば次の色へ、という感じになっています。

下の画像では、左側は無着色の判定なので着色機には入らず次に進んでいますが、右側は黄色を塗るべきだと判定されて着色機に送られています。


1層分を組み合わせる

ここまで来れば1層分の図形が揃っていますので、これを積層して1層をつくります。

基本は、各図形を対応する位置まで回転させて、積層機でくっつけるだけです。
あまりきれいではないのは、違う図形だったら捨てる、とか、空白があれば飛ばす、とかいった処理を詰め込んだためです。


積層させる

要求される図形が複数層から成る場合は、同様に積層が必要です。


完成

以上で完成です。
列車に乗せて、そのまま納品しましょう。


ここでつくったのは、1レーンのみの出力です。
しかし、これだけではかなり遅いです。要求量は、初めは1000個と少なめですが、レベルが上がっていけば増えていきますので、もっとたくさんつくりたいですね。

土台にある口は1ヶ所あたり4×3の12レーンありますので、12レーンで出力できるMAMを目指しましょう。


まとめ

以上、1レーン出力のMAMをつくってみました。
とりあえず動きはしますが、切り替えが遅かったり、出力が少なかったりと改善の余地はあります。
加えて、今回は「#0」のランダム図形に対応したMAMでしたが、「#1」の「結晶」に対応したものもつくりたいです。

また、全自動工場の建設だけでなく、3Dの利点を活かした効率化・小型化など他の遊び方もあると思いますので、まだまだ楽しめそうですね。

ちなみにですが、現時点で4レーン出力、12レーン出力の「#0」対応MAMは一応つくれました。(切り替えに何十分もかかりますが…)
なので、そのうち記事が出ると思います。お楽しみに。


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

「RecoveryFox AI」を使ってみる

こんにちは。
今回は「RecoveryFox AI」というソフトを使ってみます。


目次


RecoveryFox AI とは

RecoveryFox AI は、「Recovery」とあることからもわかるように、データ復元ソフトです。
そして「AI」とついていますので、AIの技術が組み込まれているそうです。時代ですね。

最先端のAI技術を駆使して各ファイル形式の特徴を識別し、データの欠片から完全なファイルを再構築することで、長時間削除したファイル、部分的に上書きされたファイルでも高い成功率で復元できます。様々なデータ紛失シーンに対応し、各種のストレージデバイスをサポートします。

とのことです。

www.wonderfox.jp


なぜ急に復元ソフトを?と思われるかもしれません。
実は、WonderFox Soft という会社の方から、このブログで「RecoveryFox AI」を紹介してほしいという旨のメールをいただきました。
なので、今回はPRということになります。

私自身、消えたら消えたでまた欲しければつくればいいか、と思っていたり、そもそもデータが消えたという現象にあまり出会わなかったりと、この手のソフトには縁が無かったのですが、思いついたことをいろいろ試してみたいと思います。


ちなみに、料金プランは4種類で、1週間ライセンスが7,980円、1ヶ月で9,980円、1年間で11,980円、そして永久ライセンス (=買い切り) が15,980円 (すべて税込) となっています。

無料版もあるようですが、データのスキャンとプレビューのみで、復元まではしてくれないようです。
本当に復元できるのか、試しに使ってみる分には良いと思います。


まずはインストール

さっそく、インストールしてみます。
ホームページ (RecoveryFox AI – データ復元をもっと安全、もっとスマートに!) の「無料ダウンロード」から始まります。

インストール画面にも書いてくれていますが、復元したいディスクにインストールしてしまうとデータが上書きされて復元できなくなってしまうそうです。注意しましょう。


開いてみると、シンプルなUIです。

ひとつだけ気になる点を挙げるとすれば、ウィンドウのサイズを変えられないことですね。
画面がいっぱいになってしまい、なんなら少しはみ出していたのでウィンドウを小さくしたかったのですが、マウスを端に持っていっても両矢印の形になりませんでした。
(自分の環境だけかもしれませんが)


試しにスキャン

空のゴミ箱をスキャンしてみました。
ウィンドウに表示されるアイコンをクリックするだけで、スキャンは5分ほどで完了し、以下のように表示されました。

フォルダを開き、見たいファイルの上で右クリックすればプレビューすることができます。

左下の「データ復元」ボタンで復元されるはずですが、無料版ではスキャンまででおしまいです。


本記事を書くにあたって永久ライセンスコードをいただいてしまいましたので、復元機能を解放して使ってみます。


復元してみる

ゴミ箱

先程スキャンした空のゴミ箱の中身を復元してみましたが、ファイルとしては破損しているものが多い印象でした。
テストとして画像とテキストをゴミ箱に入れ、その後空にしてみましたが、復元した画像ファイルは開こうとすると「サポートされていない形式のようです」と表示されたり、開けても真っ黒でした。
しかし、テキストファイルは問題なく復元されていました。

Cドライブは読み書きが頻繁に行われていますので、もしかしたらデータが上書きされてしまったのかもしれません。
時々、保存したはずのないYouTubeの動画のスクショ(?)のようなものが復元されたりしたので、裏でいろいろデータのやりとりがあるのでしょうか。
(筆者はほぼ常にYouTubeで動画を流しています。)


HDD

つづいて、フォーマットしたHDDをスキャンしてみました。

スキャンを始めると、まずクイックスキャンが行われ、その後にAIスキャンが行われました。
クイックスキャンは1秒かからないほどで終了しましたが、AIスキャンはかなり時間がかかりました。

クイックスキャン終了後にこのようなウィンドウが表示されます。


AIスキャンを待っていたら何時間もかかってしまいそうだったので、一旦停止して中身を見てみました。
すると、テストとして消しておいた画像ファイルが復元されていました。
プレビュー時には画質が悪く見えていましたが、復元すると削除前と同等の画質で見ることができました。

プレビュー時

復元後


テキストファイルも、プレビューのときは文字化けしていて怪しい感じがしたのですが、復元すると普通に表示されてました。

プレビュー時

復元後


ちなみに、インストール時と同様、復元したファイルを復元したいディスクに入れてしまうと上書きされてしまう可能性があるので、別のディスクに復元しましょう。


他、いつ消したのかどこで使うのかすらもわからないようなファイルの数々が。
容量が大きいことと、自分が全く把握していないファイルまでもスキャンされるので、目的のファイルだけ探してくれるようにできればありがたいですね。難しいかもしれませんけど。


USBメモリ

ちょうど、全然使っていないUSBメモリが家に転がっていましたのでこれでも試してみました。

スキャンの時間に関してはHDDのときと似たような感じでした。
が、ひとつ違うことがありました。AIスキャンの途中、スカスカだからこれ以上スキャンしても意味ないよ、と表示が出ました。

「OK」を押すと、スキャンが終了しました。


復元した結果、テストで消してみた画像とテキストが問題なく表示されました。
クイックスキャンのみでも全て復元することができました。


AI必要か?と思ってしまいましたが、いろいろ試してみたところ、単に削除しただけの場合はクイックスキャンで対応できるようですが、フォーマットした場合にはAIスキャンが力を発揮するようです。それでも完全には復元できないっぽいですね。


その他

上書きしてしまったファイルも復元できるそうです。
(私の環境ではOfficeソフトのライセンスが無いので試せませんでした…)

suikamelo.hatenablog.com


ホームページにも様々なシチュエーションで操作方法が説明されています。

www.wonderfox.jp


おまけ: そもそも「削除」とは

良い機会でしたので、「削除」とはどのような処理なのか調べてみました。


データは、データ本体と管理情報から成るそうです。
そしてデータを削除したときは、「削除した」という情報が記録され、データ本体は残ったまま。
残ったままではありますが、これらは管理上無いものになっているため保存領域として再利用できる、という仕様のようです。


「復元」はこの残ったままのデータを読み取ることであり、だからこそ、復元したいディスクに新たにデータを書き込む行為は、復元したいデータの領域を上書きしてしまう可能性があるため、控えた方が良いということなのでした。

逆に、復元できないように消したければ、別のデータで上書きしてしまえば良いということになります。
(世の中には残留磁気を読み取るとかいう装置もあるようですが…)

もちろん、物理的に壊してしまうのも手ではあります。


参考


まとめ

以上、「RecoveryFox AI」を使ってみました。

消してしまったら、早く対応するに越したことはありません。
USBメモリのような、データの移動に使うだけといった運用をしているものに対してであれば、かなり頼りになるのではないかと思います。

システムファイルも入っているようなディスクでは、余計なものまで拾い上げられてしまう印象でした。
難しいと思いますが、何日前までのを、とか、このファイル形式のを、みたいに指定できると良いのですけど…。

また、削除だけならおそらく復元できると思いますが、フォーマットまでしてしまうとダメ元で試してみる、無いよりはマシ、という感じになりそうです。


「RecoveryFox AI」、もしものときには試してみてください。


最後まで読んでいただきありがとうございました。
そして、WonderFox Soft の担当の方、PRのご依頼ありがとうございました。(ご期待に添えていたでしょうか…)

「shapez 2」をやろう

こんにちは。
今回は「shapez 2」を紹介します。


「shapez 2」は言わずもがな、あの「shapez」の「2」です。

たまたまですが、ちょうど1年前くらいに紹介しました。

taq.hatenadiary.jp

セールで30%OFFであったことに加え、3連休でがっつりやれそうと思い、買ってしまいました。


遊んでみての感想

序盤は shapez と同じような感じですが、大きな違いは3Dであるということです。

画面中央辺りにあるものは「積層機」なのですが、実際に重ねられていく様子を見ることができます。

そういう動きを見ることができるという意味では面白いですが、コンパクトにしようとすると立体的に考えなければなりませんので、頭を使います。


他、図形の他に「ピン」や「結晶」というものが追加され、自動工場をつくるのにひと工夫要りそうです。
図形や液剤を運ぶ「列車」も追加され、速く大量にものを運ぶことができるようになりました。


ゲームの流れとしては前作同様、決められたお題に沿って図形をつくっていき、いろいろ解放していくストーリーモード (と勝手によんでいる) が終わると、要求される図形がランダムになります。
ランダム図形としては、shapez 2 で追加された「ピン」や「結晶」が必要なものと、旧 shapez と同様のものの2つがあるっぽいです。


現時点での進み具合としては、旧 shapez の方のランダム図形に対して自動工場を建設中という感じです。
今はこんな具合にぐちゃぐちゃなので、いろいろまとまったらまた記事を書こうと思います。


追記 (2025/09/14)

書きました。

ひとつ認識が違うことがありました。
「ピン」は通常の図形と同様に扱えるため、ここでいう旧shapezのランダム図形の方にも登場するようです。
(といっても、図形の種類がひとつ増えるくらいの影響しかありませんでしたが。)

taq.hatenadiary.jp

以上


ちなみにゲームモードはいくつかあり、「オペレーターバッジ」はチュートリアル、これが終わると「レギュラー」となり、「クレイジー」では積層が5層までになったり、「ヘキサゴナル」では図形が6分割されるようです。


まとめ

以上、「shapez 2」を遊んでみました。
前作をやってハマったという方は買って損は無いと思います。

まだ早期アクセスとのことなので、この先も楽しみですね。

また次回。

「ABI-DOS」をやろう

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


すっかりゲーム紹介シリーズみたいなものができてしまいました。
プログラミングが直接関わるタイプのゲームではないのでタイトルには合わないかもしれませんが、shapez のようにアルゴリズムを考える系のゲームで、かなり気に入ったので紹介します。

昨年末くらいに1200円でリリースされ、その2ヶ月後くらいに無料になったそうで、試しに始めてみたらかなり面白くてどんどん進めてしまいました。
元々1200円で売っていただけあってかなり歯ごたえがあって、一通り遊んでから記事を書こうかと思っていたのですが結局途中ながら書くことにしました。


ABI-DOS とは

「ABI-DOS」は論理パズルゲームです。
一定の操作をするブロックとワイヤを組合せ、入力として送られてくるシグナル (UBITS) をうまく変換していき、要求通りに出力させます。
変換操作として加減乗除や比較、遅延など様々あります。

store.steampowered.com

デフォルトでは英語とスペイン語のみに対応していますが、一応日本語化もできるようです。


実際にはこんな感じ

はじめの4問を例にしてみます。


SIGNAL DUPLICATOR

問題を開くと、こんな画面になります。

ここで、画面中央辺りが回路を置く部分 (メインボード) で、下のブロックからやりたい操作を選びます。
(色が薄くなっているブロックは今後解放される要素です。使うことはできますが「NOT RECOMMENDED」と表示されています。)

ALTキーやホイールクリックを押すことで、ワイヤをひくモードに切り替えることができます。

また、右上にインプット、右下にアウトプットのシグナルが表示されており、メインボード上の左側の三角形のブロックからインプットシグナルが出て、右側の四角形のブロックにアウトプットシグナルを流していきます。

下中央辺りにある「TXT」ボタンを押すと説明が出てきます。


1問目ではインプットシグナルをそのままアウトプットすれば良いので、ワイヤで繋げば良いことになります。

しかし仕様上、1つ目の UBITS は開始と同時にインプットから出てきますが、2つ目以降は「INPUT C」というブロックを機能させなければなりません。
「INPUT C」は、その上を UBITS が通ることで機能し、次のインプットシグナルを出させます。

左下の「START」ボタンかスペースキーを押すと実行されます。
ステップ毎に実行することや早送りすることもできます。

問題なさそうであれば、「SPEED × 999」のボタンを押して最後まで飛ばしましょう。
クリアできれば、以下のような表示が出ます。

「GIF」ボタンで実行した様子をGIFファイルとして保存することができます。

「EXIT」ボタンでステージセレクト画面に戻ります。


ちなみに、下に並んだブロックの右の方にある旗のマークを押すと「CHALLENGE MODE」をオンにすることができ、ブロックやワイヤを置ける範囲が制限された状態で解くことになります。
(このモードでは (おそらく) 色が薄くなっているブロックも使わなければ解けないので、ある程度進んでどんなブロックがあるかわかってから取り組んだ方が良いかもしれません。)


ADDING CIRCUIT

2問目は S0 と S1 の UBITS を足して S5 に出力します。
仕様として、UBITS 同士がぶつかると各々が持つ値が足されてひとつの UBITS になるので、これを足し算として使います。
新たに解放された矢印のブロックは、入ってきた UBITS の進行方向をその矢印の方向に変えます。

本問題で説明されるワイヤの分岐の仕様を使えば、矢印のブロックを使わなくても解けます。


SEIZER

3問目では、S0 よりも S1 の方が大きければ足した結果を S5 に流し、S0 よりも S1 の方が小さければ捨てる、という操作が求められています。
比較演算ができないと解けないと思われるかもしれませんが、ワイヤの仕様で2つの UBITS がぶつかった際は値の大きい方の移動方向が優先されるので、条件分岐が可能になります。


AGGRIGATOR

4問目は、S0 のシグナルを累積して S5 に流します。
それまでの累積和の結果を保存しておく必要があります。
そこで「DUPLICATOR」の登場です。
このブロックは UBITS を複製することができるので、出力用と保存用の2つの UBITS に分けることができます。
保存用はインプット側に戻し、次のインプットシグナルと足し合わせて複製、を繰り返せば良さそうです。


おまけ

ここまで紹介した問題は割と単純でしたので、もう少し複雑なものも紹介します。
現時点で順番通り解いて18問目までクリアできているので、18問目を紹介します。

18問目は「UNWINDER」という題で、ランレングス法で圧縮されたようなデータを復元する問題です。

ここまで来ると1問解くのに数時間かかってしまい、もはや再現不可能なので細かい説明はできません。
が、やっていることとしては個数に相当する方を1ずつ減らしていき、記号に相当する方をその個数分だけ複製して出力するような感じです。


まとめ

以上、「ABI-DOS」の紹介をしてみました。
だんだんしっかり悩むタイプのパズルゲームになってきますので、かなり長く楽しめると思います。

この面白さを伝えられたかどうか怪しいですが、無料なので興味を持ってもらえたらぜひやってみてほしいです。




以上の内容はhttps://taq.hatenadiary.jp/より取得しました。
このページはhttp://font.textar.tv/のウェブフォントを使用してます

不具合報告/要望等はこちらへお願いします。
モバイルやる夫Viewer Ver0.14