gotutiyan’s blog

プログラミング関連の話題を中心に

【入門】processingにおけるシーン遷移を実現する一般的なテクニック

はじめに

ここでは,processingのシーン遷移について解説します.
シーン遷移を知っていれば,様々な方面に活用できます.具体的には,スタート画面,クリア画面,操作説明の画面などを簡単に作ることができます.他にも,ステージクリア制のゲーム制作,アート作品のポートフォリオアプリの制作などにも応用が利きます.

この記事では,まず基本アイデアとして,ステージ遷移の考え方について書きます.
その後,その実装方法を簡略化して3通り述べます.
最後に,単純なゲームを作成してみて,実践的な使い方を解説します.

基本アイデア

まずは基本アイデアの概要です.前準備として,存在するであろうシーンに番号を振ります.その後,今度は番号を指定することでシーンを呼び出すという感じです.

例えば,スタートシーンに0番,ゲームシーンに1番,クリアシーンに2番,というようにあらかじめ番号を振ります.
その後,スタートシーンを呼び出したいと思ったら0を指定する,クリアシーンを呼び出したければ2を指定する,という具合です.

次からは,上記の具体例を元に実装例を見ていきます.
この例では,以下のような流れを想定しています.

  • スタートシーンには,スタートボタンがあり,押すとゲームシーンに移る.
  • ゲームをクリアすれば,クリアシーンに移る.

実装例

ここでは実装例を書きます.
なお,processingのテンプレである以下の形から付け加えていくことにします.

void setup(){
  // 最初の1回だけ実行される
}

void draw(){
  // 何回も実行される
}

1. draw()に直書き

一番愚直な方法です.ある意味わかりやすいかもしれません.

int scene = 0;  // シーンを指定する番号
void setup(){

}

void draw(){
  if(scene == 0){
    // スタートシーンの処理
    if(スタートボタンを押したら) scene = 1;
  }else if(scene == 1){
    // ゲームシーンの処理
    if(クリアしたら) scene = 2;
  }else if(scene == 2){
    // クリアシーンの処理
  }
}

各シーンについて,次のシーンに移れるようにsceneの値を書き換える処理を入れています.

また,この例では,全ての処理をdraw()に直書きします.一見分かりやすそうですが,例えば,ゲームシーンの処理が200行とかになった場合,プログラム上で処理の流れが追いにくくなります.

2. 関数を呼び出す

int scene = 0;  // シーンを指定する番号
void setup(){

}

void draw(){
  if(scene == 0){
    init();
  }else if(scene == 1){
    game();
  }else if(scene == 2){
    finish();
  }
}

void init(){
  // スタートシーンの処理
  if(スタートボタンを押したら) scene = 1;
}

void game(){
  // ゲームシーンの処理
  if(クリアしたら) scene = 2;
}

void finish(){
  // クリアシーンの処理
}

各シーンでの処理を,関数によって記述しました.ここでは,具体的な処理は全てinit(), game(), finish()に記述します.

この実装で嬉しいことは,各シーンの処理によらず,draw()の中身は変わらないことです.とりあえずdraw()さえ見ればシーンと番号の対応が一目で分かるので,コードの可読性がぐっと上がります.

ちなみに,シーンによらず常に実行したい処理もあるかもしれません.例えば,background()や,BGMに関する処理です.このような時は,もう一つ専用の関数を作って,if文の影響を受けないところで呼び出します.

int scene = 0;
void setup(){

}

void draw(){
  common();  // if文の影響を受けないところで呼ぶ
  if(scene == 0){
    init();
  }else if(scene == 1){
    game();
  }else if(scene == 2){
    finish();
  }
}

void common(){
  // 共通の処理
}

void init(){
}

void game(){
}

void finish(){
}

3. シーンを文字列で指定する

1.および2.では,シーンを番号で指定していました.でもこの方法は,「0番はスタート」という情報を自分が覚えておく必要があります.これでは,より複雑なゲームでは,何番が何のシーンだったかを忘れるかもしれません.「キャラ選択画面は5で,魔法使いの能力確認画面は49で..」みたいなのは,できるだけやりたくないです.

このような時,シーンの指定に文字列を使えば良いかもしれません.例えば,"start"はスタートシーン,"select_character"はキャラ選択画面, "magic_ability"が魔法使いの能力確認画面,みたいな要領です.これだと,非常に直感的に記述できます.

String scene = "start";  // シーンを指定する番号
void setup(){

}

void draw(){
  if(scene == "start"){
    init();
  }else if(scene == "game"){
    game();
  }else if(scene == "clear"){
    finish();
  }
}

void init(){
  // スタートシーンの処理
  if(スタートボタンを押したら) scene = "game";
}

void game(){
  // ゲームシーンの処理
  if(クリアしたら) scene = "clear";
}

void finish(){
  // クリアシーンの処理
}

文字列はString型で扱うことができます.数字で指定していた時よりも,少しわかりやすくなった気がしますね.

単純なゲーム制作で理解するシーン遷移

最後に,具体的なサンプルを見て,実践的な感覚を掴みましょう.ここでは,3.の方法を用いて,文字列でシーンを制御します.
ここでは単純な例として,赤い四角をクリックしたらクリアできるようなゲームを考えましょう.
もちろん,スタート画面とクリア画面も作ります.

f:id:gotutiyan:20200116225907g:plain

まずはテンプレです.

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

void draw(){

}

共通部分の追加

まずは共通部分です.今回は,画面更新のたびに画面を塗りつぶすことくらいです.

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

void draw(){
  common();  // 呼び出し
}

// 共通部分
void common(){
  background(255); 
}

スタートシーンの追加

スタートシーンを作ります.これに伴って,シーンを指定する文字列も作成します. この文字列には,最初"start"を代入しているので,最初はスタートシーンが表示されます.

また,何かしらのキーをクリックしたらゲームが始まるようにします.これはvoid keyPressedの中で,sceneの文字列を書き換えれば良いです.

String scene = "start";
void setup(){
  size(500, 500);
  textSize(50);
}

void draw(){
  if(scene == "start") init(); //呼び出し
}

void common(){
  background(255); 
}

// スタートシーンの処理
void init(){
  fill(0);
  text("Start", width/5, height/2);
  text("Press any key", width/5, height/2+60);
}

void mousePressed(){ 
  // ゲームシーンへ遷移
  if(scene == "start") scene = "game"; 
}

ゲームシーンの追加

次にゲームシーンを作成します.
ゲームの内容的には,赤い四角が描画できて,クリックしたらシーン遷移すれば良いです.

String scene = "start";
void setup(){
  size(500, 500);
  textSize(50);
}

void draw(){
  common();
  if(scene == "start") init();
  else if(scene == "game") game();
}

void common(){
  background(255); 
}

void init(){
  fill(0);
  text("Start", width/5, height/2);
  text("Press any key", width/5, height/2+60);
}

// ゲームシーンの処理
void game(){
  fill(255,0,0);
  rect(30, 50, 70, 90);
}

void mousePressed(){
  if(scene == "start")scene = "game";
  else if(scene == "game"){
     // クリックできたらクリア画面に遷移
     if(get(mouseX,mouseY) == color(255,0,0)){
        scene = "clear"; 
     }
  }
}

get(x,y)は点(x,y)の色を取得する関数で,get(mouseX, mouseY)でマウス座標の色を取得できます.color(255,0,0)は赤色を表します.

クリアシーンの追加

最後にクリア画面を追加します.ここでは単純に,Clearの文字を出すことにします.

String scene = "start";
void setup(){
  size(500, 500);
  textSize(50);
}

void draw(){
  common();
  if(scene == "start") init();
  else if(scene == "game") game();
  else if(scene == "clear") finish();  // 呼び出し
}

void common(){
  background(255); 
}

void init(){
  fill(0);
  text("Start", width/5, height/2);
  text("Please click", width/5, height/2+60);
}

void game(){
  fill(255,0,0);
  rect(30, 50, 70, 90);
}

// クリアシーンの処理
void finish(){
  fill(0);
  text("Clear", width/5, height/2);
}

void mousePressed(){
  if(scene == "start")scene = "game";
  else if(scene == "game"){
     if(get(mouseX,mouseY) == color(255,0,0)){
        scene = "clear"; 
     }
  }
}

このようにして,スタートシーン→ゲームシーン→クリアシーンの遷移を実装できました.

おわりに

今回は,processingにおけるシーン遷移について説明し,具体的なサンプルと共に使い方を説明しました.いろいろなシーンで使えるので,ぜひ使って見てください.

一つお断りなのですが,今回,主にinit(), game(), finish() を使用しました.本来,start(), game(), clear()としたかったのですが,start()とclear()は既にprocessingの標準関数として用意されていたため,ユーザー定義の関数として作成できませんでした.なので,苦し紛れにinit()とfinish()で代用しています.

以上です.ありがとうございました.

【入門】processingで最小限の一筆書きゲームを作る

本記事は,processingで一筆書きのゲームを作る記事です.
早速ですが,完成版は画像のようになると思います.マウスを使ってマス目をなぞり,なぞったところは赤くなります.全てのマス目をなぞることができればクリアです.

f:id:gotutiyan:20200114001632g:plain
完成予想図

まだprocessingをよく知らない方でも完成までたどり着けるように、詳しく書いていきたいと思います。プログラムは60行です. 出てくるプログラムの知識は int型, float型、変数、if、配列、for程度です。どれも事前知識があると嬉しいですが、全て解説は入れているので、問題ないのではないかなと思います。

はじめに

座標系

processingを使うと、自由に図形を描くことができます。図形を描画する際に必ず指定するのが座標なのですが、processingの場合は座標系における軸の取り方がいつもとは違います。 数学における座標系は、上に行くほどy座標が増えて、右に行くほどx座標が増えます。また、原点は左下です。 対して、processingにおける座標系は、下に行くほどy座標が増えて、右に行くほどx座標が増えます。また、原点は左上です。

f:id:gotutiyan:20200102190010p:plain
座標系の違い(左:数学 右:processing)
最初は戸惑うかもしれませんが、徐々に慣れていきましょう。

setup()とdraw()

ゲームを作るに欠かせないのは「パラパラ漫画」の機構です。ページを素早くめくるように、ゲームは高速に画面が更新されています。テトリスを想像してみてください。ブロックが落ちていくだけでも、ゲーム画面は非常に高速に更新されています。この更新の速さを「フレームレート」と呼んだりしますが、フレームレートが低いことは、いわゆる「カクつく」「処理が重い」と感じる原因です。パラパラ漫画をめくる速度が遅ければ、当然絵もカクつきますね。

さて、processingにはこの「高速に画面を更新する」仕組みがあります。それがsetup()draw()です。これらはプログラミングの中では非常に重要な「関数」というものですが、今は割愛します。 setup()は、ゲームが始まった瞬間に一度だけ処理が行われます。 draw()は、ゲームを遊んでいる間、裏で何回も何回もひたすら処理が行われます。draw()が何回も実行されて、画面がその度に更新されることによって、パラパラ漫画はめくられます.ブロックは落ちるし、キャラは動きます。

具体的には、以下のようなテンプレを元に作成することになります。

void setup(){
  //1回だけ実行される処理
}

void draw(){
  //何回も実行される処理
}

初期設定

今回ゲームを作るにあたって、初期設定をしましょう。 具体的には,実行画面の大きさです。ゲームをプレイする画面を小さくしたり、大きくしたりできます。 これはsize()によって指定することができます。サイズは一度設定すればそれ以降固定されるので、setup()に書きます。 サイズは何でも良いですが、本記事では500x500にすることにします。

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

void draw(){
  //何回も実行される処理
}

実行ボタンを押すと、ある程度の大きさのウィンドウが表示されます。

フィールドを作成する

マス目の一辺の長さの決定と変数

一筆書きを行うためのフィールドを作成します.本記事でのフィールドとは,マス目の集合だとしましょう.一筆書きのゲームは,必ずマス目が用意されていなければなりません.今回は5x5マスによる一筆書きを実現します.目標はこんな感じです.

f:id:gotutiyan:20200111101104p:plain

いま,フィールド全体を見たときに,縦や横に並ぶマス目の数は5個です.また,フィールドの縦や横の長さは,「初期設定」のところで500x500と決めたので,マス目一つあたりの一辺の長さは自然に決まります.つまり,500/5=100です.これを,プログラムでは変数を用いて書いてみます.

変数とは,何かしらの値を格納できるものです.変数1つにつき1つの値しか格納できませんが,一度格納したものを後で上書きしたり,四則演算を行ったりできます.さらに,processingでは,変数にはというものがあります.型は変数に格納できる値の種類を表すもので,int型は,その変数には整数だけ格納できることを表します.

さて,これらを駆使して,マス目の個数と,マス目の一辺の長さは以下のように書けます.

int number_of_squares = 5;  //一辺に並べるマス目の数
int side_length = 0; //マス目の一辺の長さ
void setup(){
    size(500,500);
    side_length = width / number_of_squares; // 500/5=100
}

void draw(){
    //何回も実行される処理
}

width, heightは,変数の中でもシステム変数と呼ばれるものです.システム変数は,processingが勝手に用意してくれている変数で,公式のエディタでは桃色で表示されます.
widthは実行画面の横の長さを,heightは実行画面の縦の長さを表します.本記事であれば,width=height=500です.

フィールドの作成とfor

ここまでで,マス目の一辺の長さが決まったので,あとはそれを良い感じに並べるだけです.とは言いつつ,説明することは多いので順番にやっていきましょう.

まずは四角形の描画方法です.
rect(左上の頂点x, 左上の頂点y, 横の長さ, 縦の長さ)で書けます.

rect(0,0,100,50); // ((x,y)=(0,0)を左上の点とし,横100,縦50の四角形)

というわけで,まずは左上のマス目を書いてみます.

int number_of_squares = 5;  //一辺に並べるマス目の数
int side_length = 0; //マス目の一辺の長さ
void setup(){
    size(500,500);
    side_length = width / number_of_squares; // 500/5=100
}

void draw(){
    rect(0, 0, side_length, side_length);  //左上のマス目

}

f:id:gotutiyan:20200111002013p:plain

1つだけマス目を書けました! どんどん増やして並べていきましょう..と言いたいところですが,5x5=25個のマス目をこのように手打ちするのは,骨が折れます.できるんですけどね.

今やろうとしていることは,マス目を並べることです.もっと言えば,等間隔で並べようとしています.このようなとき,for文を用いると便利です.for文の一般的な文法はこうです.

for(int 変数名 = 初期値; 変数名 < 上限値; 変数名 += 加算値){
    //処理を書く
}

for文の変数名には,慣習的にi, j, kあたりがよく使われます.これを使えば,繰り返しの処理が簡単に記述できます.今回のゲーム制作で言えば,マス目の左上の座標を指定するのに役立ってきます.

まずは以下のコードを見てみましょう.

int number_of_squares = 5;  //一辺に並べるマス目の数
int side_length = 0; //マス目の一辺の長さ
void setup(){
  size(500,500);
  side_length = width / number_of_squares; // 500/5=100
}

void draw(){
  // for文を使って,5つのマス目を書いた
  for(int i = 0; i < number_of_squares; i++){
    rect(i*side_length, 0, side_length, side_length);
  }
}

f:id:gotutiyan:20200111002324p:plain

for文を使うことで,たった3行で5つのマス目を並べることができました.並べる時には,隣同士のマス目はside_lengthだけ離せば,綺麗に敷き詰められることを利用しています.

冗長に書けば,以下のようになります.

// for文を使用
for(int i = 0; i < number_of_squares; i++){
  rect(i*side_length, 0, side_length, side_length);
}
ーーーーーーーーーーーーーーーーーーーーーーー
// for文を未使用
rect(0, 0, side_length, side_length);
rect(side_length, 0, side_length, side_length);
rect(2*side_length, 0, side_length, side_length);
rect(3*side_length, 0, side_length, side_length);
rect(4*side_length, 0, side_length, side_length);

0→1→2→3→4と増えていく変数iが,座標指定にうまくマッチしていることが分かります.

残りの領域も敷き詰めていきます.先ほどは,1つのマス目を横に5個並べました.これと同じ要領で,「横に5個並んだもの」を縦に5個並べることを考えます.こうすれば,5x5のマス目になるはずです.つまり,for文を2つ重ねることをします.

int number_of_squares = 5;  
int side_length = 0; 
void setup(){
  size(500,500);
  side_length = width / number_of_squares; 
}

void draw(){
  // 「横に5つ並べた」ものを縦に5つ並べた
  for(int j = 0; j < number_of_squares; j++){
    // 1つのマス目を横に5つ並べた
    for(int i = 0; i < number_of_squares; i++){
      // iとjに注意
      rect(i*side_length, j*side_length, side_length, side_length);
    }
  }
}

f:id:gotutiyan:20200111101104p:plain

for文の中にfor文を入れることを,「入れ子にする」,もしくは「ネストする」などと言います.外側のfor文の変数名は j で,四角のy座標に用いています.内側のfor文の変数名は i で,四角のx座標に用いています.

以上で,フィールドの作成ができました.

なぞれるようにする

一筆書きのゲームは,マス目をなぞっていくことでプレイするものです.前章でマス目の作成は済んでいるので,次はこれをなぞれるような機構を考えます.このため,以下のような要件を満たすようにしましょう.

  • プレイヤーが最後になぞったマス目から,次に移動できるのは上下左右だけ
  • なぞったマス目は赤色になる
  • 始点はこちらで指定する

なぞったマスの覚え方と配列, if

なぞり方に関するルールは定めましたが,まずはあるマスについて,そのマスが既になぞられたかどうかを覚える必要があります.これの実現には,あるマスについて,1なら既になぞられた,0なら まだなぞられていない,となるような変数を作成します.この変数はマスの数だけ作成することが望ましいので,5x5=25個の変数を作りたいです.でも,手打ちでするのはめんどくさいですね.

// 頑張って書いた例
int visited1 = 0, visited2 = 0, ..... , visited25 = 0;

これをもっと簡単に書くために,配列を使うことにします.配列は,大量の変数を一度に作成できるものです.上記を配列で書き換えると以下のようになります.

int visited[] = new int[25];   //25個のint型変数を作成

このようにして作られた配列は,visited[0], visited[1], ...., visited[24]として,値を代入したり,値を参照したりできます.このときの0~24を添字(index)と呼びます.0から始まることに注意してください.

さらに,今回は実装上の理由から,この配列を2次元にして使います.一応1次元でもできるのですが,2次元の方が後々分かりやすくなります.5x5個のマスに対しての変数なので,配列も5x5の2次元に揃えます.

int visited[][] = new int[number_of_squares][number_of_squares];

では,これをプログラムに組み込みましょう.組み込むに当たって,新たにif文を導入します.if文は「もし〜なら」という機構を実現するものです.文法の一例は以下の通りです.

if(条件式) {
  // 条件式がTrueの時の処理
} else {
  // そうでない時の処理
}

条件式もいろいろ書けますが,この段階では==を紹介します.==は,その左側も右側も同じであれば,True,そうでなければFalseを返します.

if(5 == 5) {
  // 5と5は等しいので実行される
}

if(4 == 5){
  // 4と5は異なるので実行されない
}

次では,このif文を赤に塗るか塗らないかを決定することに使います.

int number_of_squares = 5;  
int side_length = 0; 
int visited[][] = new int[number_of_squares][number_of_squares]; // 塗ったかどうかの0 or 1
void setup(){
  size(500,500);
  side_length = width / number_of_squares;
  // v[2][2] = 1; //試しに真ん中を赤くする用
}

void draw(){
  for(int j = 0; j < number_of_squares; j++){
    for(int i = 0; i < number_of_squares; i++){
      if(visited[i][j] == 1)fill(255,0,0);  // 赤に塗る
      else fill(255);  // 白に塗る
      rect(i*side_length, j*side_length, side_length, side_length);
    }
  }
}

if文を用いれば,visited[ ][ ]が1か0かで,赤く塗るかを決められます.ここでfill()は図形を塗る色を決められる関数です.基本的にRGBなので,
fill(赤の濃さ, 緑の濃さ, 青の濃さ)
で指定します.それぞれの値は0(最小)~255(最大)です.ここでは赤を塗りたいので,fill(255, 0, 0);です.

ただ,visited[ ][ ]は初期値が全て0なので,全て白に塗られます.なので特に実行結果に変化はありません.なので,setup()中にコメントアウトされている行を削除すれば,中心のマスだけ赤くなるでしょう.

f:id:gotutiyan:20200111201227p:plain

これで,塗るかどうかを記憶するために配列を作って,if文によって塗る色を決めることができました.

なぞったマスを赤くする

一筆書きのゲームはマウスを使ってプレイします.つまり,マウスの座標から,どのマスを塗っているのかを判定する必要があります.もっと具体的にいえば,どのようなときにvisited[ ][ ]を1にするかについて詳しく書きます.ここではその方法の紹介と,実際に塗ったところが赤くなる様子まで見れるようにします.

まずは,マウスの座標を取得する方法です.これはmouseX, mouseYで取得できます.これはwidthなどと同じシステム変数です.

また,マウスをクリック&ドラッグしている間だけ,マス目を塗れるようにしましょう.これには,void mouseDragged()という関数を用います.これはマウスをドラッグしている間だけ実行されるものです.関数については中級的な内容ですが,とりあえずサンプルを見てもらえればと思います.

int number_of_squares = 5;  
int side_length = 0; 
int visited[][] = new int[number_of_squares][number_of_squares]; 
void setup(){
  size(500,500);
  side_length = width / number_of_squares; 
}

void draw(){
  for(int j = 0; j < number_of_squares; j++){
    for(int i = 0; i < number_of_squares; i++){
      if(visited[i][j] == 1)fill(255,0,0);  
      else fill(255);  
      rect(i*side_length, j*side_length, side_length, side_length);
    }
  }
}

void mouseDragged(){
  // マウスをドラッグしている間だけ実行
}

この節ではmouseDraggedに処理を記述することになります.

さて,やっと本題です.マウス座標から,今塗っているマス目を特定するにはどうすれば良いでしょうか.今やりたいことは,マウスの座標を,なんとかvisitedの添え字である0~4の値に変換することです.
また,mouseX, mouseYは,画面内に収まるとは限りません.画面サイズは500と決めていても,マウスが実行画面の外に行けば,mouseX,Yは600にも700にもなります.

このような時,constrain()を使います.これは値の下限と上限を決めて,その範囲に収めてくれる関数で,constrain(値, 下限, 上限)として使います.今回はmouseX, mouseYに対して使います.

void mouseDragged(){
  int mx = constrain(mouseX, 0, width-1);  // 0~499になるようにする
  int x = mx / side_length; //添え字に変換
  
  int my = constrain(mouseY, 0, height-1);  // 0~499になるようにする
  int y = my / side_length;  //添え字に変換
  visited[x][y] = 1;  //該当マスを赤く塗る
}

constraintsを使って,マウス座標を画面内に収まるようにします.width-1やheight-1のように1引いているのは,500の時だけ5になってしまい,添え字として使うには都合が悪いからです.499なら,499/100=4(小数切り捨て)で,大丈夫です.
こうして得た添え字を使って,visited[x][y]に1を入れたら,マスを赤く塗る機構の完成です.

やり直し機構

ゲームはトライ&エラーを繰り返してクリアするものです.プレイヤーは,時に失敗することがあります.このような時,すぐにやり直せることができれば,プレイヤーのモチベーションは続きます.

この考えに基づき,このゲームではマウスを離したら(=クリックをやめたら),塗った形跡を全て消すことにします.マウスを離した時の処理は,mouseReleased()に記述します.

int number_of_squares = 5;  
int side_length = 0; 
int visited[][] = new int[number_of_squares][number_of_squares]; 
void setup(){
  size(500,500);
  side_length = width / number_of_squares; 
}

void draw(){
  for(int j = 0; j < number_of_squares; j++){
    for(int i = 0; i < number_of_squares; i++){
      if(visited[i][j] == 1)fill(255,0,0);  
      else fill(255);  
      rect(i*side_length, j*side_length, side_length, side_length);
    }
  }
}

void mouseDragged(){
  int mx = constrain(mouseX, 0, width-1);
  int x = mx / side_length; 
  
  int my = constrain(mouseY, 0, height-1);
  int y = my / side_length; 
  visited[x][y] = 1;
}

void mouseReleased(){
  //マウスを離した時の処理 
}

次に,どうすれば塗った形跡を消せるかです.これは,visited[ ][ ]の全てに0を入れたら良いです.これはfor文を入れ子にして,以下のように書けます.

int number_of_squares = 5;  
int side_length = 0; 
int visited[][] = new int[number_of_squares][number_of_squares]; 
void setup(){
  size(500,500);
  side_length = width / number_of_squares; 
}

void draw(){
  for(int j = 0; j < number_of_squares; j++){
    for(int i = 0; i < number_of_squares; i++){
      if(visited[i][j] == 1)fill(255,0,0);  
      else fill(255);  
      rect(i*side_length, j*side_length, side_length, side_length);
    }
  }
}

void mouseDragged(){
  int mx = constrain(mouseX, 0, width-1);
  int x = mx / side_length; 
  
  int my = constrain(mouseY, 0, height-1);
  int y = my / side_length; 
  visited[x][y] = 1;
}

void mouseReleased(){
  // 全ての変数を0に
  for(int i = 0;i < number_of_squares; i++){
   for(int j = 0; j < number_of_squares; j++){
     visited[i][j] = 0;
   }
  }
}

これで,快適にトライ&エラーを繰り返すことができます.

始点を決める

一筆書きのゲームをプレイしたことがある人なら分かると思いますが,一般的には始点が決められています.複雑なステージであれば,始点を適切に指定しないとクリアできないステージとなってしまいます(別途解説しています).ここではその機構を作ります.

まずは,始点のための変数を作成します. 始点はsetup(),およびmouseDragged()で決めます.どちらも,プレイヤーがプレイを始める前には決めておくべきだからです.まずは適当に2を入れておきます.

int number_of_squares = 5;  
int side_length = 0; 
int visited[][] = new int[number_of_squares][number_of_squares];
int start_x=2, start_y=2; // 始点用変数
void setup(){
  .......

次に,ここで始点としたマス目は,実際に赤く塗らないと行けません.この処理も,setup(),およびmouseDragged()に書きます.いずれにせよ,ステージの初期化と同時に始点も塗るということです.

int number_of_squares = 5;  
int side_length = 0; 
int visited[][] = new int[number_of_squares][number_of_squares];
int start_x=3, start_y=2; // 始点用変数
void setup(){
  size(500,500);
  side_length = width / number_of_squares;
  visited[start_x][start_y] = 1; //始点を塗る
}

void draw(){
  for(int j = 0; j < number_of_squares; j++){
    for(int i = 0; i < number_of_squares; i++){
      if(visited[i][j] == 1)fill(255,0,0);  
      else fill(255);  
      rect(i*side_length, j*side_length, side_length, side_length);
    }
  }
}

void mouseDragged(){
  int mx = constrain(mouseX, 0, width-1);
  int x = mx / side_length; 
  
  int my = constrain(mouseY, 0, height-1);
  int y = my / side_length; 
  visited[x][y] = 1;
}

void mouseReleased(){
  for(int i = 0;i < number_of_squares; i++){
   for(int j = 0; j < number_of_squares; j++){
     visited[i][j] = 0;
   }
  }
  visited[start_x][start_y] = 1; //始点を塗る
}

壁マスの作成

壁マスの作成

ここまでの一筆書きのゲームは,全てのマスを塗ることができます.しかし,これは流石に面白みに欠けていると思うので,「壁のマス」は必須だと思います.壁マスは,塗ることができないマスです.

壁マスの実現にも,やはりvisited[ ][ ]を用います.今,この値は0(塗ってない),もしくは1(塗った)でした.これに,-1(壁マス)であることを追加します.今,試しに一番左上のマスを壁マスにすることを考えます.壁マスの要件はこんな感じです.

  • 壁マスは,塗ることができない
  • 壁マスは,あらかじめ灰色に塗っておく

まずは灰色に塗るところをやります.壁マスにはvisited[ ][ ]に−1を入れたら良いです.これはsetup()に書きます.また,「やり直し機構」のところで,mouseReleased()でもフィールドの塗り直しを行なっているので,ここでも忘れずに壁マスの情報を入れます.

最後に,-1である時には灰色で塗ることを書きましょう.色を塗る情報はdraw()で書いているので,灰色で塗ることもdraw()で書きます.灰色はfill(150);で実現できます.

int number_of_squares = 5;  
int side_length = 0; 
int visited[][] = new int[number_of_squares][number_of_squares];
int start_x=2, start_y=2;
int wall_x = 0, wall_y = 0; // 壁マスの添え字
void setup(){
  size(500,500);
  side_length = width / number_of_squares;
  visited[start_x][start_y] = 1;
  visited[wall_x][wall_y] = -1;  // -1を入れる
}

void draw(){
  for(int j = 0; j < number_of_squares; j++){
    for(int i = 0; i < number_of_squares; i++){
      if(visited[i][j] == 1)fill(255,0,0); 
      else if(visited[i][j] == -1)fill(150);  // 灰色に塗る
      else fill(255);  
      rect(i*side_length, j*side_length, side_length, side_length);
    }
  }
}

........

void mouseReleased(){
  for(int i = 0;i < number_of_squares; i++){
   for(int j = 0; j < number_of_squares; j++){
     visited[i][j] = 0;
   }
  }
  visited[start_x][start_y] = 1;
  visited[wall_x][wall_y] = -1;  // -1を入れる
}

次に,灰色のマスは塗れないようにします.現状では,灰色のマスにマウスをかざせば,容赦無く赤に塗りつぶしてしまいます.
これが起こる原因は,マスの状態(壁,塗ってない,塗った)を見ずに,visited[ ][ ]に1を代入していることです.よって,塗る前にマスの状態を確認して,「塗っていない」マスに対してだけ塗るようにしましょう.

void mouseDragged(){
  int mx = constrain(mouseX, 0, width-1);
  int x = mx / number_of_squares; 
  
  int my = constrain(mouseY, 0, height-1);
  int y = my / number_of_squares; 
  if(visited[x][y] == 0)visited[x][y] = 1;  // 「塗ってない」マスなら塗る
}

壁マスを作ったことによる問題

さて,前節で壁マスを導入しました.しかし,これにより一つ問題が起こります.次の画像を見てください.

f:id:gotutiyan:20200112111953g:plain

このように,壁マスの色は変わりませんが,壁マスを貫通して一筆書きができてしまいます.この問題を解決します.

結論から言うと,「最後に塗ったマスから上下左右のマスしか塗らないようにする」という制約をつけることが解決策になります.この制約の元では,壁マスから上下左右には塗ることができません(壁マスは塗れないマスなので).不思議かもしれませんが,これだけで問題は解決できます.

さらにもう一点だけです.「最後に塗ったマス」というのは,最初に塗るときには存在しません.よって,最初は,始点としていたマスを最後に塗ったマスとみなすことにします.

実装としては,最後に塗ったマスをprev_x, prev_yとして持っておけば良いです. また,(x1, y1)と(x2, y2)の2つのマスがあると仮定したとき,それらが上下左右に位置する関係かどうかを判定する方法は,以下のように書けます.

abs(x1 - x2) + abs(y1 - y2) == 1

ここで,abs( )は絶対値を返す関数です.マンハッタン距離が1であれば良い,と聞いてわかる人はそれで十分です.わからない人は,とにかく「x座標,もしくはy座標が1だけ違えばいい」と解釈すれば,分かりやすいかなと思います.

int number_of_squares = 5;  
int side_length = 0; 
int visited[][] = new int[number_of_squares][number_of_squares];
int wall_x = 0, wall_y = 0; 
int start_x=2, start_y=2;
int prev_x, prev_y; // 最後に塗ったマスの添え字
void setup(){
  size(500,500);
  side_length = width / number_of_squares;
  visited[start_x][start_y] = 1;
  visited[wall_x][wall_y] = -1;
  // 最初は,始点を最後に塗ったマスとしておく
  prev_x = start_x;
  prev_y = start_y;
}

void draw(){
  for(int j = 0; j < number_of_squares; j++){
    for(int i = 0; i < number_of_squares; i++){
      if(visited[i][j] == 1)fill(255,0,0); 
      else if(visited[i][j] == -1)fill(150);
      else fill(255);  
      rect(i*side_length, j*side_length, side_length, side_length);
    }
  }
}

void mouseDragged(){
  int mx = constrain(mouseX, 0, width-1);
  int x = mx / number_of_squares; 
  
  int my = constrain(mouseY, 0, height-1);
  int y = my / number_of_squares; 
  // 最後に塗ったマスの上下左右であれば塗るという条件を追加
  if(visited[x][y] ==0
     && (abs(prev_x - x) + abs(prev_y - y) == 1)){
       visited[x][y] = 1; //塗る
       prev_x = x;  // 最後に塗ったマスを更新(x)
       prev_y = y;  // 最後に塗ったマスを更新(y)
     }
}

void mouseReleased(){
  for(int i = 0;i < number_of_squares; i++){
   for(int j = 0; j < number_of_squares; j++){
     visited[i][j] = 0;
   }
  }
  visited[start_x][start_y] = 1;
  visited[wall_x][wall_y] = -1;
  prev_x = start_x; // 忘れず初期化
  prev_y = start_y; // 忘れず初期化
}

以上で,壁マスの設置が完了し,それによって起こる問題も解決できました.

クリア判定とテキスト表示

ここまで,フィールドを塗っていくことができるようになりました.壁マスも自由に設置できるようになったので,ステージ作りもできるようになりました.残すはクリアしたかどうかの判定のみです.ここでは,最小限という名目なので,"Clear!"という文字を画面に表示することにしましょう.

結論から言えば,visited[ ][ ]の全部の値が-1か1になっていればクリアです.
解釈として,「(壁マスを除いて)全てのマスが塗られた」,もしくは,「塗られていないマスがない」と考えれば,visited[ ][ ]が-1と1だけになれば良いというのは納得がいくでしょう.

まずは実装を見てみましょう.クリア判定は常に監視するべきもので,かつマウスの状態には関係ない処理なので,draw( )で書くことにします.

int number_of_squares = 5;  
int side_length = 0; 
int visited[][] = new int[number_of_squares][number_of_squares];
int wall_x = 0, wall_y = 0; 
int start_x=2, start_y=2;
int prev_x, prev_y;
void setup(){
  size(500,500);
  side_length = width / number_of_squares;
  visited[start_x][start_y] = 1;
  visited[wall_x][wall_y] = -1;
  prev_x = start_x;
  prev_y = start_y;
  // テキストのサイズを大きめに
  textSize(50);
}

void draw(){
  for(int j = 0; j < number_of_squares; j++){
    for(int i = 0; i < number_of_squares; i++){
      if(visited[i][j] == 1)fill(255,0,0); 
      else if(visited[i][j] == -1)fill(150);
      else fill(255);  
      rect(i*side_length, j*side_length, side_length, side_length);
    }
  }
  // クリア判定
  boolean is_clear = true;
  for(int i = 0; i < number_of_squares; i++){
    for(int j = 0; j < number_of_squares; j++){
      if(visited[i][j] == 0) is_clear = false;
    }
  }
  // クリア条件を満たしていれば,テキスト表示
  fill(0);
  if(is_clear)text("Clear!", width/3, height/2);
}

void mouseDragged(){
  int mx = constrain(mouseX, 0, width-1);
  int x = mx / number_of_squares; 
  
  int my = constrain(mouseY, 0, height-1);
  int y = my / number_of_squares; 
  if(visited[x][y] ==0
     && (abs(prev_x - x) + abs(prev_y - y) == 1)){
       visited[x][y] = 1;
       prev_x = x;
       prev_y = y;
     }
}

void mouseReleased(){
  for(int i = 0;i < number_of_squares; i++){
   for(int j = 0; j < number_of_squares; j++){
     visited[i][j] = 0;
   }
  }
  visited[start_x][start_y] = 1;
  visited[wall_x][wall_y] = -1;
  prev_x = start_x;
  prev_y = start_y;
}

クリア判定の実装は少しアルゴリズミカルなところが強いです.
まずはじめに,クリア判定用のboolean型変数を作ります.boolean型は,trueかfalseのどちらかしか取らない型です.trueかfalseしかないので,if文の条件式に直接使用できます.今回はソースコードに習って,この変数のことをis_clearと呼ぶことにします.

クリアを判定するアルゴリズムはこうです.最初,仮にクリアできているものと考えます.よって,is_clearにtrueを入れます.
次に,visited[ ][ ]の値を全部見ていき,0であればまだ塗り残しがあってクリアしていないので,is_clearにfalseを入れます.
visited[ ][ ]を全て見終わった後,まだis_clearがtrueであれば,クリアしていることが分かります.

また,テキストの表示にはtext( )を使います.具体的には,文字列と座標をtext(文字列, x座標, y座標)で指定します.今回は"Clear!"を中央あたりに表示させたいので,text("Clear!", width/3, height/2)としています.
また,テキストには文字サイズがあります.これはtextSize( )で指定できます.今回は文字サイズを50くらいにすれば良い感じなので,textSize(50)とします.注意点として,textSize( )は少し処理が重いので,draw( )で何度も実行するとゲーム自体が重くなることがあります.よってこれはsetup( )に記述することにします.

重要な注意点

とても一般的な話をしますが,今回のゲームに限らず,一筆書きというのは,どうやってもできない場合があります.この辺りの話は数学の分野の1つであるグラフ理論に関わってきます.特にオイラーグラフ(厳密には半オイラーグラフ)に密接していますが,難しいので割愛します.

今回のゲームで言えば,クリアできるかできないかは,実は壁マスの位置と始点のマスの位置で決まります.どちらのマスも,ゲームの製作者が自由に決めることができてしまうので,作ったステージがちゃんとクリアできるものかは確認する必要があります.

おわりに

以上で,最小限の一筆書きゲームが完成しました!
たった60行のプログラムですが,それなりにプログラミングの知識を必要とし,簡単なアルゴリズムも学べる良い題材だったと思います.

最後にもう一度,コードの完成図を掲載します.

int number_of_squares = 5;  
int side_length = 0; 
int visited[][] = new int[number_of_squares][number_of_squares];
int wall_x = 0, wall_y = 0; 
int start_x=2, start_y=2;
int prev_x, prev_y;
void setup(){
  size(500,500);
  side_length = width / number_of_squares;
  visited[start_x][start_y] = 1;
  visited[wall_x][wall_y] = -1;
  prev_x = start_x;
  prev_y = start_y;
  textSize(50);
}

void draw(){
  for(int j = 0; j < number_of_squares; j++){
    for(int i = 0; i < number_of_squares; i++){
      if(visited[i][j] == 1)fill(255,0,0); 
      else if(visited[i][j] == -1)fill(150);
      else fill(255);  
      rect(i*side_length, j*side_length, side_length, side_length);
    }
  }
  boolean is_clear = true;
  for(int i = 0; i < number_of_squares; i++){
    for(int j = 0; j < number_of_squares; j++){
      if(visited[i][j] == 0) is_clear = false;
    }
  }
  fill(0);
  if(is_clear)text("Clear!", width/3, height/2);
}

void mouseDragged(){
  int mx = constrain(mouseX, 0, width-1);
  int x = mx / side_length; 
  
  int my = constrain(mouseY, 0, height-1);
  int y = my / side_length; 
  if(visited[x][y] ==0
     && (abs(prev_x - x) + abs(prev_y - y) == 1)){
       visited[x][y] = 1;
       prev_x = x;
       prev_y = y;
     }
}

void mouseReleased(){
  for(int i = 0;i < number_of_squares; i++){
   for(int j = 0; j < number_of_squares; j++){
     visited[i][j] = 0;
   }
  }
  visited[start_x][start_y] = 1;
  visited[wall_x][wall_y] = -1;
  prev_x = start_x;
  prev_y = start_y;
}

これは宣伝なんですが,この記事と同じくらいの丁寧さでシューティングゲームを作るものも公開しているので,気になる方はご覧ください.

gotutiyan.hatenablog.com

ありがとうございました.

今後の展開

ここでは,このゲームをさらに拡張するためのアイデアを提案します.実装はしないのでコード例はありませんが,ご了承ください.

  • マスの数を変える
    マスの数は5とは言わず,もっと多くしても面白いでしょう.今回作成したプログラムは,number_of_squaresを変えるだけで,マス目の数を変えてプレイできるようになっています.

  • 一マス戻れるようにする
    あるマスを塗ったけど,なんか違う気がするので,一歩戻りたい時があると思います.そういう機能は今はないので,つければより遊びやすくなります.

  • 壁マスを複数つけられるようにする
    今は壁マスを1つしか作成できませんが,やはり複数作成できたほうが,ステージの難易度も調整できます.具体的には,wall_xとwall_yを配列にすれば良いです.

  • クリア時の演出をつける
    今は一瞬だけClear!の字がでるだけです.もっと派手な何かを考えると面白いです.

  • ステージ制にする
    ステージクリア制のゲームにすれば,もっと長く遊んでもらえます.各ステージについて壁マスの位置を設定するなどが考えられます.ステージに応じてマス目の数を変えれば,面白いかもしれません.

  • BGMをつける
    これは優先順位が低いかもしれませんが,やはりBGMの有無では雰囲気が変わってきます.

  • 塗ったマスに透明度を与える
    透明度を設定すれば,若干マス目が透けることになります.背景として何かの絵みたいなものを置いておけば,塗れば塗るほど後ろの絵が浮かび上がってくるような演出が可能になります.
    透明度の実装は,fill( )の4つ目に値を設定します.

他にもいろいろあると思うので,考えてみてください!
ありがとうございました.

HackerRankでコンテストを主催する方法

競技プログラミングの世界では,様々なサイトでコンテストが開かれています.具体的には,AtCoderCodeForcesなどです.その中でも,HackerRank(ハッカーランク)というサイトは,自分でコンテストを開けることが特徴です.今回はその具体的な方法について説明します.

なお,ハッカーランクへの登録は済んでいるものとします.

www.hackerrank.com

目次

大まかな流れ

コンテストの作成にあたっては,以下の4つのステップで行えます.
1. コンテストを作成する
開始時間など,コンテストの詳細を決めます.
2. 問題を作成する
実際に解かれる問題を作成します.
3. コンテストに問題を登録する
1.で作成したコンテストに,2.で作成した問題を登録します.
4. 参加URLを配布する
コンテストの参加者に,参加用のURLを配布します.

コンテストを作成する

まずは新しいコンテストを作成します.自分のユーザーIDからプルダウンされるメニューから,Administrationを選びます.

f:id:gotutiyan:20200103224923p:plain

次に,緑のボタンのCreate Contestを選びます.
f:id:gotutiyan:20200103225026p:plain

次の画面ではコンテストの情報を入力します.これらの情報は,後からでも変更できます.
* Contest Name: コンテストの名前です.
* Start Time: コンテストの開始時間です.日をカレンダーから,時間を15分刻みで設定できます.
* End Time: コンテストの終了時間です.Start Timeと同じ要領で,時間を設定します.
* Organization Type: 組織の形式を選びます.よく分からなければ,otherで良いでしょう.
* Organazation Name: 組織の名前を入力します.

これで,コンテストの作成は完了です.

問題を作成する

問題情報の作成

ここでは,実際にコンテストに出題する問題を作成します.

まずは,コンテストの作成と同じようにAdministrationを選びます.

f:id:gotutiyan:20200103224923p:plain

次に,Manage Challengeのタブを選び,緑のボタンのCreate Challengeを選びます.

f:id:gotutiyan:20200103183350p:plain

その後,問題の情報を入力します.全ての欄を埋めないと完了できないので注意してください.また,ほとんどの欄では,$\frac{a}{b}$などのTex形式の数式表記をサポートしています.
* Challenge Name: 問題名です.
* Description: 問題の説明ですが,特に公開されないので,自分向けの説明を書きます.
* Problem Statement: 問題文です.
* Input Format: 入力形式です.
* Constraints: 入力の制約です.
* Output Format: 出力形式です.
* Tags: 問題のタグです.1D-arrayなど,主に解法に関するものです.

以上で,コンテスト情報の作成は終了です.

テストケースの作成

次にテストケースの作成に入ります.これはTest Casesのタブで行えます.

f:id:gotutiyan:20200103185413p:plain

テストケースの追加は,ブラウザ上で行う方法と,ファイルアップロードによる方法の2通りの方法で行えます. また,これより先では,既にテストケースとなる入出力のセットは作成済みであるものとしています.

1. ブラウザ上で行う方法
この方法は,ブラウザ上で行えるので直感的に分かりやすい方法です.しかし,あまり大きなデータはこの方法ではアップロードできないため,その場合は2.の方法を用いる必要があります.

まず,緑色のボタンのAdd Test Case を選びます.

f:id:gotutiyan:20200103225227p:plain

そして,inputとoutputの欄に,用意した入出力のセットをそれぞれ貼り付けます.

f:id:gotutiyan:20200103233029p:plain

また,用意したテストケースのうち少なくとも1つは,サンプルとして設定しなければなりません.これは,上部に表示されるSampleのチェックボタンをクリックすることで行えます.サンプルに設定したテストケースは問題文中に表示されるため,できるだけ簡単な(=サイズの小さい)データを用いたテストケースが望ましいです.

以上の操作を,用意したテストケースの数だけ行います.

2. ファイルアップロードによる方法
この方法の利点は,複数のテストケースを一度にアップロードできる点や,データのサイズが非常に大きくなってもアップロードできる点です.

まずは,Upload Zipを選びます.

f:id:gotutiyan:20200103234239p:plain

次に,この方法を使えば,既にアップロードされているテストケースは消滅しますがよろしいですか?という内容のダイアログが出てくるので,Continueを選びます.

その後,zipファイルを選択すれば,作業は終了です.

f:id:gotutiyan:20200103234957p:plain

このときのzipファイルは,inputフォルダとoutputフォルダを中身に持つものでないと受け付けてくれません.さらに,inputのフォルダには必ず"input+数字"で始まるファイル(input01.txt など),outputのフォルダには"output+数字"で始まるファイル(output01.txt など)を置く必要があります.この時,input01.txtとoutput01.txtのように,同じ数字を含むファイル同士が入出力のセットとして扱われます.

f:id:gotutiyan:20200104000413p:plain

以上で,問題の作成は完了です.

コンテストに問題を登録する

いよいよ,先ほど作った問題をコンテストで出題する問題として設定します.

再びAdministrationから,対象のコンテストの管理画面に行きます.そして,Challengesのタブに移動します.

f:id:gotutiyan:20200104001501p:plain

次にAdd Challengeのボタンを押して,Nameのところに問題のタイトルを打ちます.そうすれば,問題の名前が推薦されるので,それを選びます.

f:id:gotutiyan:20200104001636p:plain

この作業を,出題する問題の数だけ繰り返します.これにより,コンテストに問題を登録できました.

参加URLを配布する

作ったコンテストに参加して,問題を解いてもらうためには,コンテストのURLを配布する必要があります.参加URLは,コンテストの管理画面に表示されているURLのことです.これを参加予定の人たちに配布し,コンテストの開始を待ってもらってください.

f:id:gotutiyan:20200104002137p:plain

このURLを踏めば,コンテストの開始時間までのカウントダウンがされているような画面に飛ばされます.参加者には,この画面で待機してもらってください.開始時間になれば,自動的に問題を解くことができるようになります.

以上で,コンテストの開催方法についての説明は終わりです.

発展的な設定

ここからは発展ですが,コンテストに関する詳細な設定についてです.

リーダーボードにおける順位づけについて

この設定は,コンテスト管理画面のAdvanced Settingsタブより行えます.
リーダーボードとはつまり順位表です.順位づけの方式は,以下の2つから選択できます.

  1. ハッカーランクのデフォルトのリーダーボード
  2. ACM形式のリーダーボード

筆者が2.しか使ったことがないので1.については分かりませんが,2.はICPC形式で,解いた問題数が多いほど勝利,問題数が同じ時,回答時間が早い方が勝利,というものになります.誤答した時のペナルティや,順位表の凍結に関する設定もできます.

言語ごとの実行時間制限の編集

実行時間制限は,問題ごとに行えます.よってこの設定は,問題の管理画面の,Languagesタブより行えます.

各言語について時間とメモリの制限をシークバーにより入力し,必ず右上の保存のボタンを押して終了してください.各パラメータは上限と下限が決められているので,無限に時間をかけても良い,というような設定はできません.

f:id:gotutiyan:20200104161139p:plain
時間とメモリの設定(四角囲み)と,保存ボタン(矢印)

モデレータの説明とその追加

コンテストの開催にあたっては,出題予定の問題を本当に解けるかどうかを確かめる必要があります.この作業には作問者だけではなくて,少なくとも1人は別の人を呼んできて行うのが望ましいです.モデレータの機能を使えば,このような問題の確認作業が簡単に行えます.

モデレータは,コンテストと問題の両方でつけることができます.具体的には,コンテスト管理画面,もしくは問題の管理画面のModeratorsタブで,モデレータにしたい人のIDを入力します.

f:id:gotutiyan:20200104164745p:plain

問題に対してモデレータに登録された人は,その問題の編集権,およびソースコードの提出が行えます.また,コンテストに対してモデレータに登録された人は,そのコンテスト情報の編集権,およびコンテストに登録された各問題に対してソースコードの提出が行えます.これらはもちろん,コンテストの開始時間の前でも行えます.

終わりに

今回は,HackerRankでコンテストを開催するための方法について解説しました.

www.hackerrank.com

カメさんぽ攻略Tips集

はじめに

カメさんぽを知らない人はこの記事にたどり着いていない気がしますが、これはカメさんぽの記事です。カメさんぽはカービィで有名なハル研究所のゲームで、歩数によってカメ達と仲良くなるゲームになっています。

本記事では、ゲームを進める上で知っていると有利になることや、知っていると面白いことなどを中心に、Tips集みたいなのを目指してみます。特に,戦略系(=知っていると有利にゲームを進められるもの)と,面白系(特にゲームの進みに関係ないが,知っていると楽しめるもの)に分けています.

記事の信頼性のために筆者の進行度を書くと,図鑑は全て埋まり,仲良し度は全てMAX,さんぽレベル39,勝利回数209です.(2020/01/02時点)

記事の中には一部ネタバレが存在しますのでご注意ください。
また,執筆時期の関係で,句読点がコロコロ変わっていますが,ご容赦ください.

この記事の執筆にあたっては,研究室の先輩にも結構な話題の提供をしていただきました.この場を借りてお礼をさせていただきます.

1. 戦略系

1-1. ブースト vs 釣竿

散歩に勝つために欠かせないのが、ブーストと釣竿です。ここでは,特に序盤について,どちらを強化するべきかを検討します.これらは,どちらもコインを用いて強化することができて、ブーストの最大レベルは30、釣竿の最大レベルは未知(40レベルを超えることは確認)です。最終的にはどちらも最後まで強化するので問題ないのですが、特に序盤はどちらを強化するか悩みどころです。両者の特徴を以下に挙げます。

釣竿:瞬間的に大幅な歩数を稼ぐことがメリット(相手の歩数を減らす=自分の歩数を増やすと考えて問題ないため)。散歩開始直後に使っても意味がないところがデメリット。レベル30で1350歩。

ブースト:1時間もの間、歩数を大幅に増やせることがメリット。発動には広告を見る必要があることと、得られる効果が,結局は自分の歩き次第で変わるところがデメリット。レベル30で200%。

どちらが強い効果なのかを検討するに当たっては,強化に必要なコインが判断材料になります.ゲームの開発陣からすれば,より強い機能ほど解放させたくないので,必要コインは高くなるはずです. ここで,レベル29を30にするのに必要なコインは,ブーストが445(多分),釣竿が490コインです.よって,釣り竿の方がより強いということが言えそうです.

ですが、この記事ではあえて,ブーストをお勧めしたいと考えています。

この理由は,ロケットブーストは広告によるブーストとの兼ね合いができることがあります.広告ブーストは歩数を1.5倍~2.0倍にできるものです.ロケットブーストはレベル最大で200%,そこから広告により最大で2倍にできるので,合わせると4倍になります.個人的には,これは釣竿よりも高い効果があると考えています.

一方で,普段から本当に歩かない人は,釣竿をお勧めします.歩かないことを前提としたときでは,ジェムを使いまくってでも釣竿を連打する他ないからです.

広告ブーストが発生する歩数

アプリを落とした状態で歩くと、起動時に広告による倍率がかかります。この倍率は、1.5倍をデフォルト値として、1回の散歩中であれば上昇して行き、1.5, 1.6, 1.7, 1.8, 1.9,最後には2.0倍まで上がります。この発生条件の話です。

上記の「アプリを落とした状態で稼いだ歩数」を「貯めた歩数」と呼ぶことにすると、貯めた歩数が極端に少ない場合は広告ブーストが発生しません。この広告ボーナスが発生する歩数の境目はまだ特定できていません。というのも、277歩で発生し、295歩で発生しないケースが確認できたので、一概に境目を特定できなくなりました。ただし、300歩を超えれば確実に発生するということは言えそうです。

広告ブーストの蓄積

広告ブーストについてもう一件です。貯めた歩数は累積できるという話です。
いま、「貯めた歩数」(広告ブーストが発生する歩数を参照)が500歩であるとします。ここで、ゲーム内の選択肢としては「そのまま」、もしくは「1.5~2.0倍」というどちらかが与えられます。しかし、ここでタスクキルをすれば、その500歩は「貯めた歩数」として維持できるということです。ここからさらに500歩歩けば、貯めた歩数は1000歩になり、広告ボーナスも1000歩に対してかかってきます。

これを応用すると、ギリギリを攻めすぎて広告ブーストが発生しない歩数(250歩とか)だった時、そこで諦めるのではなくてタスクキルをしてから+αだけ歩くことで、効率的に広告ブーストを拾っていけるということが言えます。

降参による代走時間の短縮

仲良し度は、散歩をして勝利するか、チケットと引き換えに代走を頼むことで上げることができます。前者ならハート一個分、後者はハート1/4に相当します。対戦中のカメと同じカメを代走させることはできません。

さて、このうち代走による仲良し度の上昇についてです。まず、代走の終了条件には以下の3種類あります。
1. 指定の時間経過する
2. プレイヤーが強制的に終了させる
3. 相手のカメの降参を受け入れる
実は、このうち1と3について仲良し度が上昇します。つまり、対戦中のカメが降参した瞬間に代走を頼み、その後すぐに降参を受け入れます。こうすることで、代走時間をかけることなく,カメの仲良し度を1/4上昇させることができます。

仲良し度のために引き直しをしない

図鑑を全て埋めれば,後は仲良し度を上げることとなります.また,ほいほいの引き直しでも,仲良し度がMAXではないカメが優先して出てくるようになります.ここでは,仲良し度を上げるための,特にジェム効率に特化した内容を紹介します.

実はチケットというのはジェムを使って買うことができます。交換レートは1対1です。5枚のチケットが欲しければ5ジェム必要です。普段は買うことはできませんが、所持数以上のチケットを使おうとした時にジェムを消費して交換することができます。

さて、いま、「ほいほいして仲良し度を上げること」と、「代走によりチケットを上げること」を比較してみます。以下では、チャレンジカメは考慮していません。

ほいほい:ジェム20個(引き直せば50個)を使って、仲良し度をハート1つ分増やす。 代走:チケット最大5枚使って、仲良し度をハート1/4分増やす。

ここで重要なのが、レア3のカメ(=代走チケット5枚)としても、両者のチケット効率が等しくなることです。ほいほいして,20個のジェムを消費し1つのハートを得ることと、ジェム20個をチケット20枚に変えて代走させて、1/4のハートを4回得ることは同じです。よりレア度の低いカメを対象にしたときや,引き直して結局50ジェム消費することを考えれば,よりたくさんの利点があることがわかります.

この手法のデメリットは、代走に費やす時間が非常にかかることです。前述した降参による代走時間の短縮を用いても難しい部分があります。が、それにより得られるジェム効率としては目を見張るものがあると考えています。仲良し度の上昇を目的とした、ほいほいを引き直す時代は終わりです。(図鑑埋めのためにはどんどん弾き直しましょう。)

バリケードで減る目安歩数

バリケードは1時間相手を足止めし、これによって若干目安歩数が下がります。相手のカメの(元々の)目安歩数は24時間で到達する最大値なので、1時間足止めすれば、目安歩数の24分の1を減らすことができます。例えば,目安歩数が24000歩のカメなら,バリケードを使えば目安歩数を1000歩減らすことができます.

ハルエッグを持ったウラシマ

ウラシマと対戦すると,たまにハルエッグを持ったウラシマと戦うことができます。このウラシマに勝つといつもより多めに報酬がもらえます.特別なウラシマに対する報酬は以下の通りです.

ジェム コイン チケット
勝ち 30 80 8
負け 15 30 4
デフォルト(比較用) 10 30 3

このウラシマが出現する条件は良く分からなくて、それなりに出る人もいれば、40回程度戦ってもでない人もいます。○回に1回出るとかそういうのではなくて、確率が低いのだと思います。

確実にハルエッグなウラシマを拾う方法

上述したように,ハルエッグを持ったウラシマの利益はハンパないです.実は,ウラシマがハルエッグを持っているか確認して,持っていなければ ほいほい をする,持っていれば そのままウラシマと歩く,というようなことができます.

この原理は簡単です.このゲームは,右上の歯車のマークから,オプションを開くことができて,その一番下に「競争を中止」というボタンがあります.このボタンはその名の通り,競争を中止できます.これを利用し,まずは一旦ウラシマに対戦を挑んで,ハルエッグを持っていなければ競争を中止して, ほいほいをすれば良いのです.おそらくウラシマがハルエッグを持っているかどうかは確率なので,試行回数が増えるだけでも効果は大きいと考えています.
(これはまだ未確定ですが,あるカメとの対戦を終了した時点で,次のウラシマがハルエッグを持つかどうかが決まっているような気がしています.なので,連続して,「ウラシマに挑む→競争を中止する」を繰り返すことは,おそらく無駄なことです.)

2. 面白系

タップしまくると何かが起こるところ
  • 散歩画面の人間と相手のカメ
  • 右下にある歩数を書いた看板
時間帯でセリフが変わる

散歩相手のカメは色々セリフを言いますが、実は時間帯を意識したセリフになっています。朝なら「眠そうだね」(意訳)、夜なら「こんな時間まで散歩してるのか?」(意訳)みたいなことを言われるときがあります。

目安歩数の表示

目安歩数は,釣竿を使ったりバリケードを置くことで減少します.ここに表示される歩数は,実際の目安歩数の10の位を四捨五入したものになっています.
例えば,目安歩数が5000歩のカメに1050歩の釣り竿を使用した時,1回目は
5000-1050=3950 で,四捨五入して4000歩が表示されます.もう一度使うと,
3950-1050=2900 で,四捨五入して2900歩が表示されます.
このように,見た目上は5000→4000→2900で,途中で減らせた歩数が変わったように見えますが,上記のように説明がつきます.

報酬受け取りの時のイナバ

カメに勝っても負けても,イナバから報酬をもらえます.ここで,イナバをタップすると色々な反応をするので可愛いのです.触るところによっていくつか反応に種類があるので,紹介します.
・耳:左右で若干変わります
・目(額)
・ほっぺ:左右で若干変わります
・口
・手:左右それぞれ反応します

3. ガラクタデータベース

ラクタは1時間に1度出現し、5個貯めるとダイヤが2個もらえます。
ラクタは、カメさんぽに登場するいずれかのカメに由来するものとなっています。ここではそれを見ていきましょう。

ラクタ名 誰のものか 備考(「誰のものか」の根拠など)
バリケード 誰のでもない 目安歩数を1/24減らします
はっぱが生えたタマゴ ウラシマ いわゆるハルエッグ
つぶれたヨーヨー ウラシマ いつも手に持ってますね,たまに卵になるけど
タツムリのカラ カメツムリ カラを捨てて甲羅にしたっぽい
電動二輪の取扱説明書 カメーイ 電動二輪=セグウェイ
ピンクの花かざり フラーラ いつも頭につけてます
チューンガムの包み紙 ブルー 口から風船ガム出してます
ペロペロキャンディの棒 カメノリ いつも食べてます
タテガミのようなカツラ サバンナ サバンナのキャラ説明の最後の文がそれです
はずかしい歌詞カード カメケロ 図鑑の説明に歌関係のことが書いてます
三輪車のサドル ヤンカメ 乗ってます
御朱印帳の落し物 おへんろさん 神社を巡る「お遍路」では,各神社の到達記念に御朱印をいただけるのです
真っ赤なゼリー スッポン 図鑑画面でタップするとゼリーを食べます
鼻をかんだティッシュ スーぽん 図鑑でタップすると鼻をかんでます
ミカンの皮 おこた こたつの上にみかん乗ってます
こわれたプロペラのハネ カメコプター お尻につけてます
こげた食パン トータスー 焦がしちゃったのね
サメの歯のような石 シャーくん それは石ではなく歯なのでは...?
みずみずしいサボテン カメぞう いつも手押し一輪車に乗せてます
学生服の第二ボタン カメバアチャン おぶってる優しい少年のボタン
バターのかけら カメープル 頭に乗ってます
折れた自撮り棒 カメメ 図鑑でタップすると自撮り棒を出します
派手なフリルのカサ カメリーヌ 持ってます
オフィスチェアのタイヤ カメヤマ課長 座ってます
でろでろのおしゃぶり ちびかめちゃん ちびかめちゃん,実はおしゃぶり咥えてないんですよね
カメサイズのダンボール箱 ハコスキー この子しかいない
ジェントルなパイプ ジェントルさん いつも口に加えてますね
使用済みのストロー カメ? 図鑑でタップするとジュースを飲みます
ポリスっぽいバッチ ジョン 当然こいつしかいない
脱ぎ捨てられた囚人服 ホワイト 囚人ってバレるもんね
小さなカメ人形 パペットル 操ってます
古びたサーカスのポスター Mr.ショーマン サーカス関係なので
穴のあいた虫アミ カメレオン 獲物逃げるもんな
賞味期限切れドッグフード ポチ 自分のエサですね
謎の任務が書かれたメモ エージェントG どんな任務なんだろう
ネコジャラシ ニャカメ ずっと持ってます
飲みかけスポーツドリンク 亀田川あゆみ 一番飲んでそう
ツノみがき用のワックス トリカメトプス ツノといえばこの子しかいないような
この星のものではないネジ カメリアン ネジ取れてますよ
またがるウマのおもちゃ フィリップ これ無くして大丈夫なのか
秘伝の書 カメまる 忍者っぽいアイテムなので
オモチャのバトン マーチブラザーズ オモチャだったのか
友カメ募集のチラシ K. レックス 図鑑の説明に友カメの記述があります
おさいせんばこ ロングイ 手に持ってますね
車のキー 亀井沢こうへい 車乗ってるのは こうへい しか居ない
ありがたい短歌 かめのもとひろまろ ありがたい 短歌の中身 気になるな
高級そうなティアラ クイーンマリア かぶってます

2019振り返り

2019年も終わりです.結構,年が変わったとて特に何かあるわけではないと思っているタイプですが,せっかくなので振り返りをします.時系列は全く考慮していません.

競プロ

競技プログラミングは,やはり今年はそれなりにやったと思います.10月ごろからは研究が忙しくなり,思うようにできませんでしたが,普通に楽しんでできているので,本当に良い趣味?だと思います.今年は

AtCoder: 891 -> 1142(緑)

CodeForces: 1067 -> 1462(水色)

という感じです.僕は問題を解きたいだけなのであまり色は気にしていませんが,色が上がればそりゃ嬉しいので,また いろいろな知識をつけていきたいと思います.

ICPCの国内予選に出ました

7月くらいにICPC国内予選に出ました.僕は3年で,4年とM1の先輩と3人で出ましたが,残念ながら予選突破とはなりませんでした.C問題は解くべきだったのですが,本番中は考察が追いつかず,解けませんでした.次は頑張りたいと思います.

yukicoderに問題を投稿しました

yukicoderにはたまに問題を投稿しています. 今年コンテストに出題された問題がどれか忘れましたが,

No.893 お客様を誘導せよ - yukicoder

これなんかは今年だったような気がします.

学内プロコンを主催しました

ハッカーランクで独自のコンテストが開けると知ったので,学内のICPCチームで,学内コンテストをやろうという提案を先生方にしていました.これが案外サクッと受け入れられて,無事開催に至りました.作問の流れはyukicoderで出題している経験からある程度わかっていたので,スムーズでした.が,各問題の計算量の見積もりが少し甘かったかなという感じです.でも,良いコンテストになりました.

自然言語処理研究室に配属されました

2年次で受けた とある授業から,その研究室にはお世話になっていました.そしてそのまま,配属になりました.早速研究を行っており,楽しくやっています.勉強会も盛んに行われているので,しっかり勉強して行きたいです.

言語処理学会2019に参加しました.

上述したように,既に2年次の早い段階から研究室に関わりを持っていたこともあり,言語処理学会に参加しました.流石に発表することはないので,聴講です.初めて学会というものに参加して,研究とはこういうものなのか,ということを知りました.名古屋大学は食堂でラーメンのサイズが選べたのでびっくりしました.

NLP2020若手の会第14回シンポジウムに参加しました

北海道で開催された

NLP若手の会 (YANS) 第14回シンポジウム (2019) - NLP 若手の会

に参加しました.2泊3日くらいで,言語処理を研究する若手研究者の発表が聴ける会です.ここで初めてポスター発表というものをしました.これは学会ほど厳しいものではないので,完璧に終わっていない研究でも発表することができます.僕が発表した研究は萌芽研究賞に選ばれて,表彰状をもらいました.ありがとうございました.

部活動もしていました

部活動は,KSWLという部活で,情報系の部活に入っていました.特に今年は役員になっており,会計としてもやっていました.文化祭では一筆書きのゲームを作り,楽しんでプレイしてもらえたような気がしています.このとき使用した知識はグラフ理論(特にオイラーグラフとか)と幅優先探索アルゴリズムで,競プロが大変役に立ちました.Gitリポジトリはこちら

github.com

他にも,監査委員会を煽ったら常任委員会に注意されるなど やんちゃしていましたが,まあこんなことできるのも部活ならではだろう,と思っています().

成績優秀者になりました

学部の上位10%程度が選ばれるらしい成績優秀者に選ばれました.なんか表彰されて,図書カードをもらいました.さらに,この中から2人が選ばれる大金がもらえる賞があるのですが,それには残念ならが選ばれませんでした.

終わりに

なんか他にもあった気がしますが,パッと思い当たるのはこの辺です.

来年もよろしくお願いします!

CodinGame「UNLEASH THE GEEK」参加記

CodinGameというサイトの10日間コンテスト、「UNLEASH THE GEEK」に参加しました。Bronzeリーグ到達、世界1354/2162、日本52/62の成績でした。 www.codingame.com

はじめに

これはCodinGameのコンテストに初めて参加した人の感想です。技術的な攻略はあまり参考にならないかもしれません。

ゲームの概要(ざっくり)

二人対戦のゲームです。各プレイヤーの目標は、5台の車を動かして、盤面に埋められた鉱石をできるだけ多く回収することです。盤面は30x15の固定で、ある程度のマスには鉱石(amadeus)が埋められています。車には「移動」「掘削」「レーダーの要求」「トラップの要求」の指示を出すことができ、いずれの操作も1ターンを消費します。レーダーは設置すれば、設置したチームはその周辺に埋めれられている鉱石の座標と量を知ることができます。また、トラップが設置されているマスを掘削しようとすると、その周辺で爆発が起こり、巻き込まれた車は失われます。

コンテストの様子

Wood Bossとの死闘

最初はWoodBossと戦いました(まだリーグの存在を知りませんでした。)

CodinGameのこの手のコンテストでは、Wood<Bronze<Silver<Gold<Legendという5つのリーグがあり、コンテスト期間の後半になるにつれて上位のリーグが解放されていく仕組みです。各リーグにはBossが存在し、Bossよりも高い評価を獲得すれば上のリーグに上がれます。よって、誰しも最初はWoodリーグなわけです。

WoodBossは必ず1つだけ鉱石を持ち帰るようでした。これなら勝てると思って実装しましたが、どこの入力を使えば鉱石の位置を取得できて、かつ車を向かわせられるかというところに少し苦労しました。データの持ち方にも少し悩みました。でもなんとか実装してWoodBossを撃破しました。

Bronze Bossとの死闘

Woodの次はBronzeリーグです。なんかいきなり強くなってびっくりしました。レーダーめっちゃ置くし、トラップめっちゃ置くし。でも目標は高いほうが良いです、頑張ります。

Woodの時点では、「とにかくランダムに動きランダムに掘る。偶然鉱石を掘れたら一直線に左端に帰る」を実装していました。これでは足りないことは明らかでした。

考えると、盤面は450マスしかないので、適当にループを回しても1ターンあたりの思考時間である50msは余裕で下回ります。また、レーダーさえ置けば鉱石の場所はわかるので、「自分に一番近い鉱石に向かって移動し、掘る」ことは簡単にできそうでした。これを実装すると、スコアが大きく伸びました。楽しい。

でもBronzeはトラップを置きまくります。見ていると、結構な確率で自分の車が2,3台死んでしまいます。なのでトラップを避けることを考えました。
トラップを埋められた時点で、そのマスには穴が空きます。なので、穴が空いているマスを掘らなければ絶対にトラップは引きません。相手が爆破して巻き込まれるケースは消せませんが。しかしこれを書くと、ore==3の時しか掘ってくれず効率がダメダメです。

そこで、「自分が過去に掘った座標を最新20件保存しておき、そこに含まれる座標も掘る」ことにしました。最近自分が掘ってトラップが置かれていなければ、ちょっとやそっとの時間では新たにトラップが置かれることはないだろう、という根拠のない考えに基づいています。これをやると、また結構スコアが伸びました。楽しい。ついでにレーダーの位置を決め打ちもしました。

他の人のを見る

でもBronze Bossは強いです。なんか強いです。何が強いのかはよくわかりませんでした。この辺で、自分では発想がなくなったのでリーダーボードから他の人の対戦の様子を見ることにしました。この頃にはシルバーあたりが解放されていましたが、Bronzeの人たちは案外トラップガン無視で、とにかく掘ることに専念している人が多い印象でした。この人たちがトラップ避けの対策をしているのかは分かりませんでした。この頃から、「トラップを意識すればするほど、鉱石入手量は下がる」ということは分かっていました。でも少なくともBronze Bossには勝てないといけない(ほんまか?)ので、トラップを避けることは続けました。

トップを走る上位陣の様子も見ましたが、あまり分かりませんでした。

終わり

そのまま特に策がないまま、コンテストは終了しました。

感想

とにかくどんな手法が効くか分からないので、思いついたものは悪そうでもやって見るのは大事そうでした。あと、入力としてどんな情報を知ることができるのか、というのは問題文をよく見返すのも大事そうでした。 他の人のを見ても今回は良く分かりませんでしたが、機能として公開されている以上、利用する価値が十分にありそうでした。

さらに、コンテスト中の思考回路的に、どんどん付け足す感じでコードが出来上がって行きます。少し試すつもりでベタ書きしたコードが良い性能を発揮したとき、流れでそのまま置いてしまうことはよくありました。結果、特に部品化されていないコードがmain関数内に溜まるので、非常に見栄えが悪いです。これはバグの元にもなり、反省点でした。

総合的には結果はどうであれ、(自分のAIを作るのも、観戦して他の日本勢を見ているのも)楽しかったので良かったです!また参加したいと思います。

requestsとbeautifulSoup4でスクレイピングをしてみた

スクレイピングとは

webサイトから必要な情報を抜き出してくることです。

requestsとは

pythonのモジュールの一つで、httpの通信ができます

pip install requests

beautifulSoupとは

pythonのモジュールの一つで、webサイトのデータを入力して、それを解析し、良い感じに表示してくれるものです。 今回はbeautifulSoup4を使います。インストール時には、bs4で指定します。

pip install bs4

今回は、はてなブログのトップページを例に、webサイトの見出しとかリンクとかの文章を取ってきたいと思います。

いざ、スクレイピング

スクレイピングをするときには、requestsを用いてwebサイトのhtmlデータを取って来て、それをbeautifulSoupに投げることで、必要箇所を良い感じに抜き出します。

import requests
from bs4 import BeautifulSoup

res=requests.get("https://hatenablog.com")
res.encoding='utf-8'
bf=BeautifulSoup(res.text,'html.parser')

2つのモジュールをimportし、まずはwebサイトのhtmlを取ってきます。これはrequests.get(サイトのURL)で行えます。今回ははてなブログのトップページのURLを入れます。
これの返り値resはResponseオブジェクトと言って、url先のwebサイトの色々な情報が入ったものになります。

次にこれをbeutifulSoupに解析させます。res.textとすると、webサイトのhtmlがそのまま得られるので、これを入力にします。また、これに合わせて第2引数をhtml.parserにします。

これで、解析結果がbfの変数に入りました。bfにも、様々な情報が入っています。 bf.find_all(タグ名)を使うと、htmlタグを指定して、当てはまるものを全て閲覧できます。試しに、aタグを指定してみます。

for e in bf.find_all('a'):
    print(e)

結果は以下のようになります。

<a href="https://hatenablog.com/guide/pro?plus_via=service_top_nav">はてなブログPro</a>
<a href="https://blog.hyouhon.com/entry/2019/09/09/143003">タコの丸ごとピザを焼きました</a>
<a href="https://blog.hyouhon.com/entry/2019/09/09/143003">私的標本:捕まえて食べる</a>
<a href="https://blog.hyouhon.com/entry/2019/09/09/143003">// またタコ釣りに行ってきました。 楽しかったです。 ということで、タコのピザを焼いてみます。 タコは生のまま冷…</a>
......
<a href="https://itunes.apple.com/jp/app/hatenablog/id583299321" target="_blank"><img alt="Download on the App Store" src="https://cdn.blog.st-hatena.com/images/banner/Download_on_the_App_Store_Badge_US-UK_135x40.svg?version=5eb1a238a24f928783bfdf3e8b093e1b38aebe88&amp;env=production"/></a>
<a href="https://play.google.com/store/apps/details?id=jp.ne.hatena.blog" target="_blank"><img alt="Get it on Google Play" height="40px" src="https://cdn.blog.st-hatena.com/images/banner/google-play-badge.png?version=4576ad6d91e1b649ca7282cb6fffa97a00d3f9c4&amp;env=production"/></a>

aタグであるような部分が全て取れていることがわかります。最後にはgoogle playへのリンクまでありました。

でも、これではまだ見にくいので、getText()の関数を使います。

for e in bf.find_all('a'):
    print(e.getText())

結果は以下です。

はてなブログとは
はてなブログPro
『ぼくたちは勉強ができない』126話 感想、小美浪あすみにとって唯我成幸とは...?
ふわふわな日記
ぼく勉 問126 感想「先人はかの日に備え[x]を蓄積する」 『ぼくたちは勉強ができない』 最新話 感想 ネタバレ注意 今…






タコの丸ごとピザを焼きました
私的標本:捕まえて食べる
.....
カラースター
はてなダイアリー
日本語
English

htmlの性質上、<a>タグの中には単なるテキストだけではなくて、画像が入ることもあります。(<a><img src="hoge"></a>のようなパターン。)この時は、空行になります(多分)。

これは適当にif文でマシにすることはできます。(getText() != '' みたいな処理を挟めばいいので)

最後にコード全体です。

import requests
from bs4 import BeautifulSoup

res = requests.get("https://hatenablog.com")
res.encoding = 'utf-8'
bf = BeautifulSoup(res.text, 'html.parser')
for e in bf.find_all('a'):
    if e.getText() != '':
        print(e.getText())

他にも適当にURLを変えたり、all_find('h1')、all_find('p')など、色々変えて見ると面白いかもしれません。