Help us understand the problem. What is going on with this article?

【じゃんけん(っぽい)ゲームで入門】Fortran

1. はじめに

自作した じゃんけん(っぽい)ゲーム を題材にして Fortran の文法や仕様を確認していきます。

1-1. ゲームの仕様

ルールは一般的なじゃんけんのルールに準じます。ただし、プレイヤーとCOMには5点の体力が用意されており、じゃんけんに勝利すると相手に1点のダメージを与えることができます。先に相手の体力を0点以下に減らしたほうの勝利となります。引き分けは発生せず、プレイヤーかCOMのどちらかが勝利するまで処理を続行します。

処理内容はコンソールに表示し、コンソールに表示された内容はログファイルにも吐き出すようにします。

1-2. Fortranとは?

Fortranは世界初の高級言語です。1953年、低級言語に代わるものとして開発がスタート。それから4年後の1957年にコンパイラが発表されました。言語仕様は今日に至るまで改良が続けられており、現在最新版となるのは Fortran 2018 です。当記事に掲載しているコードもFortran 2018に準拠しています。

発表当初は、低級言語で書くよりも命令数が少なくて済む点が評価され、数学・計算機科学分野を中心に普及しました。発表から70年近く経った今日においても シミュレーター業界大学および研究機関(物理学・工学・気象学・量子力学など)スーパーコンピューター分野 などではC/C++などと併せて使われ続けているようです。

普及している分野を見ても分かるようにFortranは数値計算に向いた言語です。処理速度もC/C++と同等以上と聞くので、そういったプログラムを組む場合は使ってみると面白いかもしれません。 文字列操作やオブジェクト指向は勘弁な!

文法や言語仕様に目を向けると、さすが世界初の高級言語というだけあってか、後発の言語ではみられない独特な機能や記法が散見されます。とはいえ、やっていること自体は他言語と変わらない――いや、むしろ最近の言語よりもできることは少ないのでFortran特有の要素に慣れてしまえば、そこからの学習・習得は容易なほうでしょう。 なお、書き易くはない。

1-3. 開発環境構築手順

私は MinGW (Minimalist GNU for Windows) というアプリを使って、Fortranの開発環境を構築しました。正直よく分かっていないのですが MinGWは 色々なプログラミング言語の開発環境を構築するためのアプリ のようです。開発経緯を調べたところ、元々はUNIXやLinuxなどで使われていたプログラム開発ツール群 GNUツールチェーン をWindowsでも使えるように移植した Cygwin というアプリから派生したのがMinGWらしいです。

仔細はともかく、ここからはMinGWを使ってFortranの開発環境を構築する手順を説明します。

※Windows 10 64bit版向けの解説です。他環境での動作は確認していません。

1-3a. MinGWのインストーラーをダウンロード

まずはMinGWのインストーラーをダウンロードしましょう。MinGWのサイトに行きます。

MinGW | Minimalist GNU for Windows

当該サイトの左側メニューのなかに Navigation というカテゴリがあり、そのなかに Downloads というリンクが貼ってあるのが確認できるかと思います。そこからMinGWのダウンロードページに飛びます。

Download File List - MinGW - Minimalist GNU for Windows - OSDN

ダウンロードページのページ下部に Download Package list という見出しとともに、10個近くのリンクが貼ってあることが確認できるかと思います。これらがMinGWに関連するパッケージ群となります。このなかにMinGWのインストーラーがあるのですが少し深い階層にあるので、ここは手っ取り早くダウンロードする手順を採ります。

先の Download Package list という見出しのすぐ上に Windowsのロゴが書かれたボタン があるかと思います。ボタンの右に mingw-get-setup.exe と書かれているのを確認してください。そのボタンをクリックすればMinGWのインストーラーがダウンロードされます。

1-3b. MinGWのインストーラーを使ってMinGWをインストール

ダウンロードできましたら、インストーラーを起動してください。

インストーラーが立ち上がりましたら、ウィンドウが表示されますので、ウィンドウ下部の Install をクリックしてください。

次の画面では「どこにインストールしますか?」ということを聞かれますので任意のパスを指定してください。初期設定では C:\MinGW となっているはずです。特に問題がないなら初期設定のままでよいでしょう。インストール場所の設定後、画面下の Continue をクリックしてください。

これでMinGWに必要なファイルのダウンロード・インストールが始まりますので気長に待ちます。我が家のクソ雑魚Wi-Fi回線で5分ほどかかりましたので、平均的なWi-Fi回線や有線であればもっと早く終わるはずです。

ダウンロード・インストール処理が終わりましたら、先ほどまでは押せなかったウィンドウ下部の Continue が押せるようになっているはずなのでクリックしてください。インストーラーが閉じ、MinGWが起動します。

ここで誤ってウィンドウを閉じてしまった場合は直接MinGWを起動してください。インストール時の設定でインストール場所を C:\MinGW から変更していないならば C:\MinGW\libexec\mingw-get\guimain.exe にあるはずです。

1-3c. Fortranの開発環境を構築する

ここからはMinGWを使って、Fortranの開発環境を構築していきます。

まず、ウィンドウ左上にある Installation のなかから Update Catalogue を選択します。するとアップデート状況を知らせるウィンドウが表示されますのでアップデート完了を待ちます。当該ウィンドウ右下の Close が押せるようになったらアップデート完了です。 Close をクリックしてウィンドウを閉じてください。

再びMinGWウィンドウに戻り、ウィンドウ左側の項目リストのなかにある Basic Setup をクリックしてください。その後、ウィンドウ上半分にある導入可能なパッケージ一覧リストのなかから mingw32-gcc-fortran-bin を右クリックし Mark forInstallation を選択してください。当該パッケージに黄色の矢印マークが付いたらOKです。

最後に、ウィンドウ左上にある Installation から Apply Changes を選択してください。これでMinGWはFortranの開発に必要なファイルのダウンロード・インストールを始めてくれます。

しばらくするとインストール完了した旨と、インストールしたファイルの一覧が表示されたウィンドウが表示されます。ウィンドウ右上の Close をクリックして閉じてください。これでFortranでの開発に必要なパッケージの導入が完了しました。MinGWは閉じてしまって大丈夫です。

1-3d. 正常にセットアップできたか確認する

正常にセットアップできたか確認します(できていないことが稀によくあります)。

MinGWのインストールディレクトリ(標準では C:\MinGW )のなかの bin というディレクトリのなかに gfortran.exe があることを確認してください。これがFortranのコンパイラとなります。

続いて 同ディレクトリにPathを通してください。 理由はよくわかりませんが Pathを通さないと正常に動作しません。

Pathを通したらgfortran.exeを使って動作確認しましょう。まずは以下コマンドを打ち、FORTRANのバージョン情報が表示されることを確認してください。

こんな感じに表示されたらOK
C:\Users\AGadget> gfortran -v
Using built-in specs.
COLLECT_GCC=gfortran
COLLECT_LTO_WRAPPER=c:/mingw/bin/../libexec/gcc/mingw32/9.2.0/lto-wrapper.exe
Target: mingw32
Configured with: ../src/gcc-9.2.0/configure --build=x86_64-pc-linux-gnu --host=mingw32 --target=mingw32 --disable-win32-registry --with-arch=i586 --with-tune=generic --enable-static --enable-shared --enable-threads --enable-languages=c,c++,objc,obj-c++,fortran,ada --with-dwarf2 --disable-sjlj-exceptions --enable-version-specific-runtime-libs --enable-libgomp --disable-libvtv --with-libiconv-prefix=/mingw --with-libintl-prefix=/mingw --enable-libstdcxx-debug --disable-build-format-warnings --prefix=/mingw --with-gmp=/mingw --with-mpfr=/mingw --with-mpc=/mingw --with-isl=/mingw --enable-nls --with-pkgversion='MinGW.org GCC Build-20200227-1'
Thread model: win32
gcc version 9.2.0 (MinGW.org GCC Build-20200227-1)

C:\Users\AGadget>

また、正常にコンパイルできることも確認します。以下コードが正常にコンパイルされ、実行ファイルも正常に動作するか確認してください。

test.f90
program test
  print '(a)', 'Hello world.'
end program
こういう感じでコンパイルして実行
C:\Users\AGadget> gfortran .\test.f90 -o test

C:\Users\AGadget> .\test.exe
Hello world.

C:\Users\AGadget>

ここまでできたらパーフェクトです。お疲れさまでした。

2. 特記事項

基本的な文法などは記事末尾のソースコードを流し読んで頂くとして、ソースコードからは読み取れない事柄や注意すべきことなどを特記します。

2-1. 拡張子

拡張子は .f90 を付けるのが無難かと思います。何故 無難 などという言い回しをするかというと、Fortranに公式な拡張子はなく、慣習的に.f90が使われているためです。そのためコンパイラによって使える拡張子に違いがあるとかないとか……。

何はともあれ、当記事で掲載されている手順で開発環境を構築した場合は.f90で大丈夫です。

2-2. 文字コード

文字コードはUTF-8です。

2-3. コーディング規約

Fortranの公式コーディング規約というのは存在しないようです。ただ、いくつかの団体がコーディング規約を発表していますので、いずれかを参考にするのが良いのではないでしょうか。

なお、当記事では我流で書いていますので、ご注意ください。

2-4. 大文字・小文字を区別しない

Fortranではキーワードや識別子の大文字・小文字を区別しません。

2-5. 1行あたり132文字の制限

Fortranでは原則として1行あたり 132文字まで しかコードを記述することができません。133文字以上書くとコンパイルエラーになります。

1行あたりの文字数は次のような計算で求められます。まずは以下コードをご覧ください。

1行あたりの文字数の求め方.f90
! 前提: インデントは半角空白2つとする
! このコードの1行あたりの文字数の最大は4行目の27文字です
program main
  print '(a)', 'Hello world.'
end program

このコードにおける1行あたりの最大文字数はprint文が書かれた4行目の27文字です。何故27文字か分かりますでしょうか。

Fortranの1行あたりの文字数カウント処理には 文法的に不要な部分はカウントされない という性質があります。例えば、上記コードではprint文の直後や、カンマ記号の直後に半角空白を挿れています。この半角空白は文法的には必須ではなく、コードの可読性を上げるためのものです。このような文法的に必須ではない文字はカウント対象外として無視されるのです。

ただ、インデントを構成する半角空白は何故かカウント対象に入っていますので注意してください。

人間とコンパイラで見え方が違う.f90
! 書いたコード
program main
  print '(a)', 'Hello world.'
end program

! コンパイラからはたぶんこう見えている
program main
  print'(a)','Hello world.'
end program

以上より、例として提示したコードの最大文字数27文字は次のような計算で求められることが分かります。

インデント = 2
printキーワード = 5
編集記述子 = 5
カンマ記号 = 1
出力する文字 = 14

27 = 2 + 5 + 5 + 1 + 14

2-6. 1行あたりの文字数制限への対策

Fortranには原則として1行あたり132文字までしか書けないという制約があります。

この厄介な制約への対応方法を2つ紹介します。

1つ目はアンド記号を使って、1文を複数行に分けて書く方法です。

文字数制限に抗おう.f90
! アンド記号で1文を複数行に分割できる
program main
  print &
    & '(a)', &
    & 'Hello world...?'
end program

! アンド記号は末尾に付けるだけでも問題ない
! ただ、見づらいので先頭にも付けたほうがいい……かも?
program main
  print &
    '(a)', &
    'Hello world...?'
end program

もう1つの方法は、そもそも文字数制限を適用せずにコンパイルすることです。

-ffree-line-length-noneオプションで文字数制限を解除
C:\Users\AGadget> gfortran main.f90 -ffree-line-length-none

2-7. コメント

コメントは感嘆符記号より行末までが範囲となります。ブロックコメントはありません。

コメントアウト.f90
! ここがコメントです
! ブロックコメントは無いので諦めてどうぞ

2-8. エントリポイント

エントリポイントは program文のブロックの先頭 です。

以下コードをご覧ください。

こういう感じ.f90
! Fortranではメインルーチンをprogram文で囲む必要がある
! → PHPかな?
!     → PHPの閉じタグに相当するend programは省略できません
program main

  ! ここから処理が始まります
  print *, '一富士'
  print *, '二鷲'
  print *, '三茄子'
end program

また、program文の書き方はいくつかあります。

バリエーション
! 最も丁寧な書き方
! 色々なところで、この書き方が推奨されている
program main
  ! 色々な処理
end program main

! ちょっと省いた書き方
! 当記事掲載のコードではこの書き方を採用
program main
  ! 色々な処理
end program

! 最も省略するとRubyっぽくなる
program main
  ! 色々な処理
end

2-9. 変数宣言は先頭で

Cの古いバージョンにおいて、変数宣言はブロック先頭でないと駄目でした。

Fortranでは新しいバージョンでもブロック先頭でないと駄目です。変数宣言よりも前に置けるのはimplicit noneやuse文などに限られています。

2-10. コンパイラ取説

gfortranのコマンドを一部紹介します。

最小構成(コンパイルだけ)
C:\Users\AGadget> gfortran main.f90
実行ファイル名を指定
C:\Users\AGadget> gfortran main.f90 -o main
どの規格でコンパイルするかを指定(標準は-std=GNU)
C:\Users\AGadget> gfortran main.f90 -std=GNU

C:\Users\AGadget> gfortran main.f90 -std=legacy

C:\Users\AGadget> gfortran main.f90 -std=f95

C:\Users\AGadget> gfortran main.f90 -std=f2003

C:\Users\AGadget> gfortran main.f90 -std=f2008

C:\Users\AGadget> gfortran main.f90 -std=f2018
コンパイル時の警告を全てエラーにする(キッチリ書きたい人向け)
C:\Users\AGadget> gfortran main.f90 -Werror
オススメコマンドレシピ
C:\Users\AGadget> gfortran main.f90 -o main -std=f2018 -Werror

2-11. implicit none

implicit none は必ず宣言してください。宣言場所はprogram文の直後です。

sample.f90
program main
  implicit none

  ! 以下、処理
end program

implicit noneが宣言されたソースコードには2つの効果が適用されます。1つは変数の宣言を強制させること(VBのOption Explicit文と同様)。そしてもう1つが 暗黙の型宣言 を行わないようにすることです。

暗黙の型宣言とは宣言されていない変数が使われるとき、その変数名に応じてデータ型が 暗黙的に定まる という仕様です。具体的には、変数名が i から n で始まるときは整数型、それ以外であれば実数型の変数と見做されます。

bad_sample.f90
program main

  ! エンジニア「拙者、お前達の中に実数を見た」
  h = 8.88
  i = 8.88

  ! FORTRAN「この世界に絶対はない」
  print *, h
  print *, i
end program
C:\Users\AGadget> gfortran bad_sample.f90 -o bad_sample

C:\Users\AGadget> bad_sample.exe
   8.88000011
           8

C:\Users\AGadget>

2-12. 数値と文字列を連結する処理

Fortranで数値と文字列を連結させるのは若干手間です。以下のような処理を組む必要があります。

main.f90
program main
  implicit none

  ! 連結したい数値
  integer :: number = 1337

  ! 連結したい数値を文字列化したときに保持する変数
  character(4) :: tmp

  ! 変換しつつ代入
  write (tmp, '(i0)') number

  ! 連結出力
  print '(a)', 'leet == ' // tmp
end program
C:\Users\AGadget> gfortran main.f90 -o main

C:\Users\AGadget> main.exe
leet == 1337

C:\Users\AGadget>

Fortranでは数値と文字列を連結させることはできません。また、データ型変換用の関数なども標準では用意されていません。そこで出力対象に変数を指定することができるwrite文を使って変換するわけです。

2-13. 配列の取り扱い

配列の取り扱いは若干ややこしい(?)ので注意してください。

まず、配列宣言時には配列の長さを指定する必要があります。可変長配列も使用できますが、必要になるケースはそう多くないはずです。

宣言.f90
program main
  implicit none

  ! 長さ3の整数型配列
  integer :: array_1(3)

  ! 可変長の整数型配列
  integer, allocatable :: array_2(:)
end program

インデックス値は1から始まります。また、コロン記号を使うことで複数の要素をまとめて取り扱うこともできます。

参照・代入.f90
program main
  implicit none

  integer :: array(3)

  array(1) = 1
  array(2) = 22
  array(3) = 333
  array(2:3) = 114514
  print *, array(1)
end program

配列宣言時に限って、大括弧記号で初期値を指定することもできます。

参照・代入.f90
program main
  implicit none

  integer :: array_1(3) = [1, 22, 333]

  ! 大括弧記号を使わないと全ての要素に同じ値が入ります
  integer :: array_2(3) = 4444
end program

2-14. 可変長文字列

当記事掲載のコードでは使用していませんが、Fortranでも可変長文字列を使用することができます。

可変長文字列.f90
program main
  implicit none

  ! これは固定長文字列
  character(128) :: string_1

  ! これは可変長文字列
  character(:), allocatable :: string_2
end program

2-15. 変数の初期値はリテラルだけ

変数の初期値に指定できるのはリテラルだけです。関数からの戻り値などを指定することはできません。

2-16. サブルーチンはcall文で呼び出す

サブルーチンを呼び出すときは必ずcall文を使用する必要があります。VBと似た(VBがFortranに似ているというほうが正確ですが)制約ですが、VBと違いcallキーワードを省略することはできません。

2-17. 引数・戻り値の定義

サブルーチン・関数の引数は、それぞれの処理のなかで変数のように定義する必要があります。 なんだ、このクソ仕様は!?

また、関数の戻り値の指定方法は4種類存在します。

関数の戻り値.f90
program main
  implicit none
contains

  ! 戻り値が関数名 & 関数内でデータ型を指定
  function sample_1()
    integer :: sample_1
  end function

  ! 戻り値が関数名 & 関数外でデータ型を指定
  integer function sample_1()
  end function

  ! 戻り値が任意 & 関数内でデータ型を指定
  function sample_1() result(r)
    integer :: r
  end function

  ! 戻り値が任意 & 関数外でデータ型を指定
  integer function sample_1() result(r)
  end function
end program

2-18. 引数は原則参照渡し

サブルーチン・関数への引数は原則として参照渡しとなっています。値渡しにするには仮引数を小括弧で囲むことで実現できます。

標準で参照渡しとかマヂ無理.f90
program main
  implicit none
  integer :: var = 1

  ! これは参照渡し
  call test_def(var)

  ! これは値渡し
  call test_def((var))
contains
  subroutine test_def(var)
    integer :: var
    var = var + 1
  end subroutine
end program

3. ソースコード

main.f90
!!
!! じゃんけん(っぽい)ゲームを遊ぶことができるプログラムです
!! Fortran 2018に準拠しています
!!
program main
  use game_character_module
  use output_module
  implicit none

  !! 変数
  type(game_character_type) :: player        ! プレイヤーを表すオブジェクトです
  type(game_character_type) :: com           ! COMを表すオブジェクトです
  integer                   :: turn_count    ! ターン数です
  type(output_type)         :: output        ! コンソール・ログファイルへの出力処理を担当するオブジェクトです

  !! 処理
  call player%constructor(5, 'Qii太郎', .true.)
  call com%constructor(5, 'COM', .false.)
  turn_count = 0
  call output%constructor()
  do while(player%can_game() .and. com%can_game())
    call add_turn_count()
    call output_turn_count_message()
    call player%output_status_message(com%get_name_width(), output)
    call com%output_status_message(player%get_name_width(), output)
    call output%output_empty_line(1)
    call player%choice_action(output)
    call com%choice_action(output)
    call output%output_empty_line(1)
    call output_choiced_action()
    call player%damage_process(com%get_action())
    call com%damage_process(player%get_action())
    call output%output_empty_line(5)
  end do
  call judge_winner()
  call output%output_empty_line(1)
  call pause()
contains

  !!
  !! ターン処理開始に伴い、ターン数を加算するサブルーチンです
  !!
  subroutine add_turn_count()

    !! 処理
    turn_count = turn_count + 1
  end subroutine

  !!
  !! 現在のターン数を知らせるメッセージを出力するサブルーチンです
  !!
  subroutine output_turn_count_message()

    !! 変数
    character(3) :: turn_count_to_string    ! 文字列化したターン数です(長さが3あれば999ターンまで対応できるので十分なはず……)

    !! 処理
    write(turn_count_to_string, '(i0)') turn_count
    call output%output_message('【第' // trim(turn_count_to_string) // 'ターン】')
  end subroutine

  !!
  !! 双方のゲームキャラクターが選択した行動を出力するサブルーチンです
  !!
  subroutine output_choiced_action()
    call output%output_message('> ' // trim(player%get_action()) // ' vs ' // trim(com%get_action()))
  end subroutine

  !!
  !! 勝者を判定し、勝者が誰かを知らせるメッセージを出力するサブルーチンです
  !!
  subroutine judge_winner()

    !! 処理
    if(player%can_game()) then
      if(com%can_game()) then
        error stop 'judge_winner - 1'
      else
        call output%output_message('> ' // trim(player%get_name()) // 'の勝利です!')
      end if
    else
      if(com%can_game()) then
        call output%output_message('> ' // trim(com%get_name()) // 'の勝利です!')
      else
        error stop 'judge_winner - 2'
      end if
    end if
  end subroutine

  !!
  !! Enterキーが押されるまで処理を止めるサブルーチンです
  !!
  subroutine pause()

    !! 処理
    write(*, '(a)', advance='no') 'Enterキーを押してください . . . '
    read *
  end subroutine
end program
game_character_module.f90
!!
!! ゲームキャラクターに関する値や操作をまとめたモジュールです
!! → クラスのように使用してください
!!     → 当モジュールで宣言されている構造型の変数を宣言後、コンストラクタの代替となる手続きを呼び出して初期化してください
!! Fortran 2018に準拠しています
!!
module game_character_module
  use output_module
  implicit none
  type game_character_type
    private
    integer       :: max_life          ! 体力の上限値です
    integer       :: life              ! 体力です
    character(64) :: name              ! 名前です(長さは64もあれば足りるやろ……)
    integer       :: name_width        ! 名前の横幅(バイト数ではない)です
    logical       :: is_playable       ! プレイヤーキャラクターであるかを表すフラグです
    character(9)  :: action_list(3)    ! 行動の一覧です
    character(9)  :: action            ! 選択した行動です
  contains
    procedure          :: constructor              => game_character_constructor                 ! 構造型を初期化します
    procedure, private :: set_name_width           => game_character_set_name_width              ! 名前の横幅を数えてモジュール変数に代入します
    procedure          :: can_game                 => game_character_can_game                    ! ゲームの続行が可能か返答します
    procedure          :: get_name_width           => game_character_get_name_width              ! 名前の横幅を返します
    procedure          :: output_status_message    => game_character_output_status_message       ! ステータスを表すメッセージを返します
    procedure          :: choice_action            => game_character_choice_action               ! 行動選択処理です
    procedure, private :: count_prompt_len         => game_character_count_prompt_len            ! プロンプトの長さをカウントします
    procedure, private :: generate_prompt          => game_character_generate_prompt             ! プロンプトを作成します
    procedure, private :: choice_action_for_player => game_character_choice_action_for_player    ! プレイヤー用の行動選択処理です
    procedure, private :: choice_action_for_com    => game_character_choice_action_for_com       ! COM用の行動選択処理です
    procedure          :: get_action               => game_character_get_action                  ! 選択した行動名を返します
    procedure          :: damage_process           => game_character_damage_process              ! ダメージ処理です
    procedure          :: get_name                 => game_character_get_name                    ! 名前を返します
  end type
contains

  !!
  !! コンストラクタの代替となるサブルーチンです
  !! → 初期化処理を行っているため、当サブルーチンを呼び出すのは1回だけにしてください
  !!
  subroutine game_character_constructor(self, initial_life, name, is_playable)
    class(game_character_type) :: self            ! 自オブジェクトです
    integer                    :: initial_life    ! ゲーム開始時点の体力です
    character(*)               :: name            ! 名前です
    logical                    :: is_playable     ! プレイヤーキャラクターであるかを表すフラグです

    !! 処理
    self%max_life = 5
    if (initial_life < 0) then
      error stop 'game_character_module%constructor - 1'
    end if
    if (initial_life > self%max_life) then
      error stop 'game_character_module%constructor - 2'
    end if
    self%life = initial_life
    self%name = name
    call self%set_name_width()
    self%is_playable = is_playable
    self%action_list(1) = 'グー'
    self%action_list(2) = 'チョキ'
    self%action_list(3) = 'パー'
  end subroutine

  !!
  !! 自身の名前の横幅(バイト数ではない)を数えて返す関数です
  !! 半角文字を1、全角文字を2としてカウントします
  !! → iachar()を使って名前をアスキーコードに変換したとき、全角文字を構成する3バイトの1つ目の数値が128以上である特性を利用して判別しています
  !!
  subroutine game_character_set_name_width(self)
    class(game_character_type) :: self    ! 自オブジェクトです

    !! 変数
    integer :: cycle_stock    ! 下記do文中でcycle文を実行するか判断するフラグ兼cycle文の実行回数です
    integer :: i              ! 名前を1文字ずつ参照するためのカウンタ変数です

    !! 処理
    self%name_width = 0
    cycle_stock = 0
    do i = 1, len_trim(self%name)
      if (cycle_stock > 0) then
        cycle_stock = cycle_stock - 1
        cycle
      end if
      if (iachar(self%name(i:i)) < 128) then
        self%name_width = self%name_width + 1
      else
        self%name_width = self%name_width + 2
        cycle_stock = 2
      end if
    end do
  end subroutine

  !!
  !! ゲームの続行が可能であるか返答する関数です
  !!
  function game_character_can_game(self) result(can_game)
    class(game_character_type) :: self        ! 自オブジェクトです
    logical                    :: can_game    ! ゲームの続行が可能であるかを表す戻り値です

    !! 処理
    if (self%life > 0) then
      can_game = .true.
    else
      can_game = .false.
    end if
  end function

  !!
  !! 名前の横幅を返す関数です
  !!
  function game_character_get_name_width(self) result(name_width)
    class(game_character_type) :: self          ! 自オブジェクトです
    integer                    :: name_width    ! 名前の横幅です

    !! 処理
    name_width = self%name_width
  end function

  !!
  !! ステータスを表すメッセージを作成して返すサブルーチンです
  !! メッセージは名前部分と体力部分からなります
  !! → 名前部分では双方のゲームキャラクターの名前の横幅を揃えて見映えを良くしています
  !!     → 横幅を揃えるために、バイト数ではなく、半角文字を1、全角文字を2としてカウントした特殊な値を使用しています
  !! → 体力部分では記号を使って体力ゲージっぽく見える文字列を使用しています
  !!
  subroutine game_character_output_status_message(self, opponent_name_width, output_object)
    class(game_character_type) :: self                   ! 自オブジェクトです
    integer                    :: opponent_name_width    ! 当サブルーチンにおいて主体となるゲームキャラクターの対戦相手の名前の横幅です
    type(output_type)          :: output_object          ! コンソール・ログファイルへの出力を担当するオブジェクトです

    !! 変数
    character(len_trim(self%name) + max(0, opponent_name_width - self%name_width)) :: name_part    ! メッセージを構成する名前部分です
    character(self%max_life * 3)                                                   :: life_part    ! メッセージを構成する体力部分です
    integer                                                                        :: i            ! 体力ゲージを作成するのに必要なカウンタ変数です

    !! 処理
    name_part = self%name
    life_part = ''
    do i = 1, self%life
      life_part = trim(life_part) // '■'
    end do
    do i = 1, self%max_life - self%life
      life_part = trim(life_part) // '□'
    end do
    call output_object%output_message(name_part // ': ' // life_part)
  end subroutine

  !!
  !! 行動を選択するサブルーチンです
  !! プレイアブルなキャラクターであるかどうかに応じて処理を切り替えます
  !!
  subroutine game_character_choice_action(self, output_object)
    class(game_character_type) :: self             ! 自オブジェクトです
    type(output_type)          :: output_object    ! コンソール・ログファイルへの出力を担当するオブジェクトです

    !! 処理
    if(self%is_playable) then
      call self%choice_action_for_player(self%generate_prompt(self%count_prompt_len()), output_object)
    else
      call self%choice_action_for_com()
    end if
  end subroutine

  !!
  !! 行動選択処理で必要となるプロンプトの長さを返す関数です
  !!
  function game_character_count_prompt_len(self) result(prompt_len)
    class(game_character_type) :: self          ! 自オブジェクトです
    integer                    :: prompt_len    ! プロンプトの長さです

    !! 変数
    integer :: i                    ! プロンプトの長さを測るのに必要なカウンタ変数です
    integer :: option_number_len    ! プロンプトを構成する選択肢番号部分の長さですです
    integer :: option_spacer_len    ! プロンプトを構成する選択肢間を区切る半角空白群の長さです
    integer :: prompt_end_len       ! プロンプトの末尾を表す文字列の長さです

    !! 処理
    option_number_len = 4
    option_spacer_len = 4
    prompt_end_len = 2
    prompt_len = 0
    do i = 1, size(self%action_list)
      prompt_len = prompt_len + option_number_len + len_trim(self%action_list(i)) + option_spacer_len
    end do
    prompt_len = prompt_len + prompt_end_len
  end function

  !!
  !! 行動選択処理で必要となるプロンプトを作成して返す関数です
  !!
  function game_character_generate_prompt(self, prompt_len) result(prompt)
    class(game_character_type) :: self          ! 自オブジェクトです
    integer                    :: prompt_len    ! プロンプトの長さです
    character(prompt_len)      :: prompt        ! プロンプトです

    !! 変数
    integer      :: i                ! カウンタ変数です
    character(1) :: option_number    ! カウンタ変数を文字列化したもの――選択肢番号です
    integer      :: option_len       ! 選択肢番号・選択肢名・選択肢間の区切りからなる文字列の長さです
    integer      :: prompt_head      ! プロンプトとなる文字列のうち、未入力のインデックスのなかで最も若い番号です

    !! 処理
    prompt = ''
    prompt_head = 1
    do i = 1, size(self%action_list)
      write(option_number, '(i0)') i
      option_len = len('[' // option_number // '] ' // trim(self%action_list(i)) // '    ')
      prompt(prompt_head: prompt_head + option_len - 1) = '[' // option_number // '] ' // self%action_list(i) // '    '
      prompt_head = prompt_head + option_len
    end do
    prompt(prompt_head:prompt_head + 2) = ': '
  end function

  !!
  !! プレイヤー用の行動選択処理を実行するサブルーチンです
  !!
  subroutine game_character_choice_action_for_player(self, prompt, output_object)
    class(game_character_type) :: self             ! 自オブジェクトです
    character(*)               :: prompt           ! プロンプトです
    type(output_type)          :: output_object    ! コンソール・ログファイルへの出力を担当するオブジェクトです

    !! 変数
    character(1) :: input          ! 標準入力された値を受け取る変数です
    integer      :: i              ! カウンタ変数です
    character(1) :: i_to_string    ! カウンタ変数を文字列化したものです

    !! 処理
    do
      write(*, '(a)', advance='no') prompt
      read *, input
      call output_object%output_message_only_to_log_file(prompt // input)
      do i = 1, size(self%action_list)
        write(i_to_string, '(i0)') i
        if(input == i_to_string) then
          self%action = self%action_list(i)
          return
        end if
      end do
      call output_object%output_empty_line(1)
      call output_object%output_message('> 再入力してください')
      call output_object%output_empty_line(1)
    end do
  end subroutine

  !!
  !! COM用の行動選択処理を実行するサブルーチンです
  !!
  subroutine game_character_choice_action_for_com(self)
    class(game_character_type) :: self    ! 自オブジェクトです

    !! 変数
    real :: random    ! 行動選択処理で用いる乱数です

    !! 処理
    call random_number(random)
    self%action = self%action_list(int(random * size(self%action_list)) + 1)
  end subroutine

  !!
  !! 選択した行動を返す関数です
  !!
  function game_character_get_action(self) result(action)
    class(game_character_type)  :: self      ! 自オブジェクトです
    character(len(self%action)) :: action    ! 選択した行動です

    !! 処理
    action = self%action
  end function

  !!
  !! ダメージが発生するかを確認し、必要であればダメージを発生――体力を減少させるサブルーチンです
  !!
  subroutine game_character_damage_process(self, opponent_action)
    class(game_character_type)     :: self               ! 自オブジェクトです
    character(len(self%action))    :: opponent_action    ! 対戦相手が選択した行動です

    !! 処理
    if( &
      & (self%action == self%action_list(1) .and. opponent_action == self%action_list(3)) &
      & .or. &
      & (self%action == self%action_list(2) .and. opponent_action == self%action_list(1)) &
      & .or. &
      & (self%action == self%action_list(3) .and. opponent_action == self%action_list(2)) &
      & ) then
      self%life = self%life - 1
    end if
  end subroutine

  !!
  !! 名前を呼び出し元に返す関数です
  !!
  function game_character_get_name(self) result(name)
    class(game_character_type) :: self    ! 自オブジェクトです
    character(len(self%name))  :: name    ! 自身の名前です

    !! 処理
    name = self%name
  end function
end module
output_module.f90
!!
!! コンソール・ログファイルへの出力処理に関する値や操作をまとめたモジュールです
!! → クラスのように使用してください
!!     → 当モジュールで宣言されている構造型の変数を宣言後、コンストラクタの代替となる手続きを呼び出して初期化してください
!! Fortran 2018に準拠しています
!!
module output_module
  implicit none
  type output_type
    private
    character(20) :: log_file_name    ! ログファイル名です
  contains
    procedure          :: constructor                     => output_constructor                        ! 構造型を初期化します
    procedure, private :: set_log_file_name               => output_set_log_file_name                  ! ログファイル名を作成します
    procedure          :: output_message                  => output_output_message                     ! コンソールとログファイルに値を出力します
    procedure          :: output_empty_line               => output_output_empty_line                  ! 任意の数の空行を出力します
    procedure          :: output_message_only_to_log_file => output_output_message_only_to_log_file    ! ログファイルに値を出力します
  end type
contains

  !!
  !! コンストラクタの代替となるサブルーチンです
  !! → 初期化処理を行っているため、当サブルーチンを呼び出すのは1回だけにしてください
  !!
  subroutine output_constructor(self)
    class(output_type) :: self    ! 自オブジェクトです

    !! 処理
    call self%set_log_file_name()
  end subroutine

  !!
  !! ログファイル名を作成するサブルーチンです
  !!
  subroutine output_set_log_file_name(self)
    class(output_type) :: self    ! 自オブジェクトです

    !! 変数
    character(8)  :: current_date          ! ccyymmdd形式の現在日付です
    character(10) :: current_time          ! hhmmss.sss形式の現在時刻です
    character(5)  :: time_difference       ! 現地時刻と世界標準時との時差です    date_and_time()を呼び出すためだけに宣言しています
    integer       :: dates_and_times(8)    ! 現在日時をまとめた配列です    date_and_time()を呼び出すためだけに宣言しています

    !! 処理
    call date_and_time(current_date, current_time, time_difference, dates_and_times)
    self%log_file_name = 'log_' &
      & // current_date(3:4) // current_date(5:6) // current_date(7:8) &
      & // current_time(1:2) // current_time(3:4) // current_time(5:6) &
      & // '.txt'
  end subroutine

  !!
  !! 引数に指定された文字列をコンソールとログファイルに出力するサブルーチンです
  !! 出力される値の末尾には改行が付きます
  !! ログファイルへの書き込みは追記モードで実行します
  !!
  subroutine output_output_message(self, message)
    class(output_type) :: self       ! 自オブジェクトです
    character(*)       :: message    ! 出力する値です

    !! 変数
    integer :: unit    ! ログファイルへの追記時に使用する装置番号です

    !! 処理
    print '(a)', message
    open(newunit = unit, file = self%log_file_name, position = 'append')
    write(unit, '(a)') message
    close(unit)
  end subroutine

  !!
  !! 空行をコンソールとログファイルに出力するサブルーチンです
  !! 出力する空行の数は引数で指定します
  !!
  subroutine output_output_empty_line(self, number_of_line)
    class(output_type) :: self              ! 自オブジェクトです
    integer            :: number_of_line    ! 空行の数です

    !! 変数
    integer :: i    ! 空行出力処理を繰り返し実行するためのカウンタ変数です

    !! 処理
    do i = 1, number_of_line
      call self%output_message('')
    end do
  end subroutine

  !!
  !! 引数に指定された文字列をログファイルにだけ出力するサブルーチンです
  !! → プレイヤー用の行動選択処理などではログファイルへの出力だけが必要になるので当処理を用意しました
  !! 出力される値の末尾には改行が付きます
  !! ログファイルへの書き込みは追記モードで実行します
  !!
  subroutine output_output_message_only_to_log_file(self, message)
    class(output_type) :: self       ! 自オブジェクトです
    character(*)       :: message    ! 出力する値です

    !! 変数
    integer :: unit    ! ログファイルへの追記時に使用する装置番号です

    !! 処理
    open(newunit = unit, file = self%log_file_name, position = 'append')
    write(unit, '(a)') message
    close(unit)
  end subroutine
end module
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした