JMeter WebDriver Plugin を使ってみた

はじめに

http://jmeter.apache.org/index.html

Apache JMeterは主にWebアプリの負荷テストで使用されるツールです。
基本的にはシナリオにしたがってリクエストを投げて、想定どおりのレスポンスが戻るまでの時間を測定し、集計するというツールですが、ユーザーのインタラクティブな操作に対応したテストを行うのは苦手です。例えば、「ユーザーがWebアプリ上である操作を行った時、裏で多数の非同期通信が行われた結果がUI上に反映されるまでの時間を測定する」といったシナリオを書くのは結構面倒なのです。

そこで、JMeterのシナリオ上からブラウザを Selenium WebDriverを通じて呼び出してしまえというWebDriverプラグインの出番になります。

jmeter-plugins.org

JMeterシナリオ上でブラウザへの操作を記述し、リクエストはJMeterではなくあくまでブラウザから行い、レスポンス(とDOMへの反映)もブラウザが受け取ります。ブラウザを起動する必要が有るため、例えば大規模ユーザの同時アクセスを想定しているような、JMeterが得意な負荷テストには向きません。しかし、ブラウザを使用することで、ユーザの操作に近い形でのテストシナリオを記述する事が可能になります。

jMeterとWebDriver APIそれぞれについてある程度知識のある人向けに、JMeter WebDriver Plugin を使って Mozilla Firefox によるテストシナリオを作成してみます。

インストール方法

1. plugins-managerの導入

jmeter-plugins.org

から plugins-managerのjarファイルを入手し、以下のフォルダへコピーします。

${JMeterインストールフォルダ}/lib/ext

現時点ではjmeter-plugins-manager-0.10.jarです。

2. WebDriver Pluginのインストール

plugins-managerの導入後にJMeterを起動すると、メニューの [オプション]配下に[Plugins Manager]が追加されるので、クリックします。
Plugins Mnagerダイアログが表示されるので、[Available Plugins]タブを選択し、インストール候補プラグイン一覧から[Selenium WebDriver Support]にチェックを入れ、「Apply Changes and Restart JMeter」ボタンをクリックします

現時点では

${JMeterインストールフォルダ}/lib/ext

jmeter-plugins-webdriver-1.4.0.jarがダウンロードされ、さらにlibフォルダに関連jarファイル群がダウンロードされます。

3. selenium-firefox-driverのjarファイルを更新

jmeter-plugins-manager v1.10, jmeter-plugins-webdriver v1.4.0の組み合わせでは以下の手順が必要です)

いったんJMeterを停止した後、

${JMeterインストールフォルダ}/lib

フォルダに"selenium-firefox-driver-2.xx.x.jar"(xは任意のバージョン番号)が存在する事を確認します。そして

Maven Repository: org.seleniumhq.selenium » selenium-firefox-driver


にv2.xx.x系のより新しいバージョンのjarファイルが存在するようであれば、jarファイルを入手して入れ替えます。Selenium WebDriverは、ブラウザが更新されると動かなくなる場合が多々有ります。そのつど上記jarをより新しいバージョンに入れ替えてください。

4. firefox 停止時のエラーを対策

jmeter-plugins-manager v1.10, jmeter-plugins-webdriver v1.4.0の組み合わせでは以下の手順が必要です)

ここまでJMeterをセットアップした状態で、WebDriver Pluginを使ってFirefoxを起動すると、シナリオ終了時に以下のような例外が出力されます。

2016/09/12 20:47:28 ERROR - jmeter.JMeter: Uncaught exception:  java.lang.NoClassDefFoundError: com/sun/jna/platform/win32/Kernel32
	at java.lang.ClassLoader.defineClass1(Native Method)
	at java.lang.ClassLoader.defineClass(ClassLoader.java:760)
	at java.security.SecureClassLoader.defineClass(SecureClassLoader.java:142)
	at java.net.URLClassLoader.defineClass(URLClassLoader.java:467)
	at java.net.URLClassLoader.access$100(URLClassLoader.java:73)
	at java.net.URLClassLoader$1.run(URLClassLoader.java:368)
	at java.net.URLClassLoader$1.run(URLClassLoader.java:362)
	at java.security.AccessController.doPrivileged(Native Method)

この問題に対応するため、

${JMeterインストールフォルダ}/lib

へplatform-3.5.1.jar およびjna-4.2.1.jarを導入し、JMeterを再起動します。
詳細は以下のサイトを参照。

hellotestworld.com

JMeterシナリオの作成

ここまでセットアップした後、「設定エレメント」として各ブラウザに対応するWebDriver設定が追加されます。この設定エレメントをシナリオへ加えることで、以降はWebDriver Pluginでは該当ブラウザが使用されるようになります。

f:id:ka-ka_xyz:20160913001813p:plain


また、「サンプラー」としてWebDriver Samplerが追加されます。

f:id:ka-ka_xyz:20160913001825p:plain

このSamplerにはスクリプトが記述できます。デフォルトではjavascript

WDS.sampleResult.sampleStart()
WDS.browser.get('http://jmeter-plugins.org')
WDS.sampleResult.sampleEnd()

のようなテンプレが入力されています。
このテンプレでは、測定開始後にjmeter-plugins.orgへアクセスし、測定を完了するという処理を行っています。

新規にシナリオを作成し、スレッドグループ配下にWebDriver設定、WebDriverサンプラー、リスナー「結果を表で表示」を追加してシナリオを保存します。この状態でシナリオを実行するとブラウザが立ち上がり、jmeter-plugins.orgへアクセスした後にブラウザウィンドウが閉じられます。実行結果はリスナーに出力されます。


WebDriverサンプラーの詳細については以下を参照。

www.blazemeter.com
www.blazemeter.com

複数のWebDriverサンプラー間でコードを再利用

各WebDriverサンプラーjavascript(あるいは選択可能な他のスクリプト)でコードを書くことが出来ますが、変数のスコープは各サンプラーの中に限定されます。ただ、それでは不便なので、サンプラー間でコードを共有してみます*1

シナリオに「設定エレメント」-「ユーザー定義変数」を追加し、ユーザー定義変数"lib"を定義します。
そして、"lib"を選択した状態で"Detail"ボタンをクリックすると、ユーザー定義変数を複数行編集出来るようになります。

サンプルとして、以下の内容を入力します。

var mylib = mylib || {};

mylib.pkg = JavaImporter(org.openqa.selenium);
mylib.conditions = org.openqa.selenium.support.ui.ExpectedConditions;
mylib.wait = new org.openqa.selenium.support.ui.WebDriverWait(WDS.browser, 10);
mylib.vars = org.apache.jmeter.threads.JMeterContextService.getContext().getVariables();

mylib.google = (function() {
  var googleUrl = 'https://www.google.co.jp';

  var access = function() {
    WDS.browser.get(googleUrl);
  };

  var search = function(keyword) {
  	var inputQuery = 'input#lst-ib';
  	mylib.wait.until(mylib.conditions.presenceOfElementLocated(mylib.pkg.By.cssSelector(inputQuery)));
    var inputElem = WDS.browser.findElement(mylib.pkg.By.cssSelector(inputQuery));
    inputElem.sendKeys(mylib.pkg.Keys.chord(mylib.pkg.Keys.CONTROL, 'a'), keyword, mylib.pkg.Keys.ENTER);
  };

  var waitForResultsShown = function() {
    var resultListQuery = '#search';
    mylib.wait.until(mylib.conditions.presenceOfElementLocated(mylib.pkg.By.cssSelector(resultListQuery)));
  };

  return {
    access: access,
    search: search,
    waitForResultsShown: waitForResultsShown
  };
})();

名前空間 mylib に、googleの検索フォームへの入力を行う関数やユーテリティ類を定義しています。
そして、サンプラー側で以下のスクリプトを記述します。


サンプラー1

//load mylib
eval(org.apache.jmeter.threads.JMeterContextService.getContext().getVariables().get('lib'));

WDS.sampleResult.sampleStart();
mylib.google.access();
mylib.google.search('jMeter');
mylib.google.waitForResultsShown();


WDS.sampleResult.sampleEnd();

サンプラー2

//load mylib
eval(org.apache.jmeter.threads.JMeterContextService.getContext().getVariables().get('lib'));

WDS.sampleResult.sampleStart();
mylib.google.access();
mylib.google.search('Selenium WebDriver');
mylib.google.waitForResultsShown();

WDS.sampleResult.sampleEnd();

ユーザー定義変数"lib"を文字列として読み出し、evalすることで変数"lib"中に書かれた"mylib"定義が読み出されます。

eval を使っている事に嫌悪感があるかもしれませんが、JMeter WebDriver Pluginの制約上しょうが無いかなと。"Eval is Evil"は原則として正しいとは思いますが、今回は外部から入力された文字列をevalしている訳ではなく、あくまでJMeterのシナリオで変数として定義された文字列をしようしているので、危険は無いはず(おねがいみのがして)。

f:id:ka-ka_xyz:20160913004600p:plain

このシナリオを実行すると

1. google.co.jpへアクセス
2. キーワード"jMeter"で検索
3. 検索結果が表示されるまで待つ
4. google.co.jpへアクセス
5. キーワード"Selenium WebDriver"で検索
6. 検索結果が表示されるまで待つ
7. ブラウザを閉じる

というシナリオが実行されます。


jmxファイルの内容は以下のとおり

<?xml version="1.0" encoding="UTF-8"?>
<jmeterTestPlan version="1.2" properties="2.9" jmeter="3.0 r1743807">
  <hashTree>
    <TestPlan guiclass="TestPlanGui" testclass="TestPlan" testname="テスト計画" enabled="true">
      <stringProp name="TestPlan.comments"></stringProp>
      <boolProp name="TestPlan.functional_mode">false</boolProp>
      <boolProp name="TestPlan.serialize_threadgroups">false</boolProp>
      <elementProp name="TestPlan.user_defined_variables" elementType="Arguments" guiclass="ArgumentsPanel" testclass="Arguments" testname="ユーザー定義変数" enabled="true">
        <collectionProp name="Arguments.arguments"/>
      </elementProp>
      <stringProp name="TestPlan.user_define_classpath"></stringProp>
    </TestPlan>
    <hashTree>
      <ThreadGroup guiclass="ThreadGroupGui" testclass="ThreadGroup" testname="スレッドグループ" enabled="true">
        <stringProp name="ThreadGroup.on_sample_error">continue</stringProp>
        <elementProp name="ThreadGroup.main_controller" elementType="LoopController" guiclass="LoopControlPanel" testclass="LoopController" testname="ループコントローラ" enabled="true">
          <boolProp name="LoopController.continue_forever">false</boolProp>
          <stringProp name="LoopController.loops">1</stringProp>
        </elementProp>
        <stringProp name="ThreadGroup.num_threads">1</stringProp>
        <stringProp name="ThreadGroup.ramp_time">1</stringProp>
        <longProp name="ThreadGroup.start_time">1473680458000</longProp>
        <longProp name="ThreadGroup.end_time">1473680458000</longProp>
        <boolProp name="ThreadGroup.scheduler">false</boolProp>
        <stringProp name="ThreadGroup.duration"></stringProp>
        <stringProp name="ThreadGroup.delay"></stringProp>
      </ThreadGroup>
      <hashTree>
        <com.googlecode.jmeter.plugins.webdriver.config.FirefoxDriverConfig guiclass="com.googlecode.jmeter.plugins.webdriver.config.gui.FirefoxDriverConfigGui" testclass="com.googlecode.jmeter.plugins.webdriver.config.FirefoxDriverConfig" testname="jp@gc - Firefox Driver Config" enabled="true">
          <stringProp name="WebDriverConfig.proxy_type">SYSTEM</stringProp>
          <stringProp name="WebDriverConfig.proxy_pac_url"></stringProp>
          <stringProp name="WebDriverConfig.http_host"></stringProp>
          <intProp name="WebDriverConfig.http_port">8080</intProp>
          <boolProp name="WebDriverConfig.use_http_for_all_protocols">true</boolProp>
          <stringProp name="WebDriverConfig.https_host"></stringProp>
          <intProp name="WebDriverConfig.https_port">8080</intProp>
          <stringProp name="WebDriverConfig.ftp_host"></stringProp>
          <intProp name="WebDriverConfig.ftp_port">8080</intProp>
          <stringProp name="WebDriverConfig.socks_host"></stringProp>
          <intProp name="WebDriverConfig.socks_port">8080</intProp>
          <stringProp name="WebDriverConfig.no_proxy">localhost</stringProp>
          <boolProp name="WebDriverConfig.maximize_browser">true</boolProp>
          <boolProp name="WebDriverConfig.reset_per_iteration">false</boolProp>
          <boolProp name="WebDriverConfig.dev_mode">false</boolProp>
          <boolProp name="FirefoxDriverConfig.general.useragent.override.enabled">false</boolProp>
          <boolProp name="FirefoxDriverConfig.network.negotiate-auth.allow-insecure-ntlm-v1">false</boolProp>
          <collectionProp name="FirefoxDriverConfig.general.extensions"/>
          <collectionProp name="FirefoxDriverConfig.general.preferences"/>
        </com.googlecode.jmeter.plugins.webdriver.config.FirefoxDriverConfig>
        <hashTree/>
        <Arguments guiclass="ArgumentsPanel" testclass="Arguments" testname="ユーザー定義変数" enabled="true">
          <collectionProp name="Arguments.arguments">
            <elementProp name="lib" elementType="Argument">
              <stringProp name="Argument.name">lib</stringProp>
              <stringProp name="Argument.value">var mylib = mylib || {};

mylib.pkg = JavaImporter(org.openqa.selenium);
mylib.conditions = org.openqa.selenium.support.ui.ExpectedConditions;
mylib.wait = new org.openqa.selenium.support.ui.WebDriverWait(WDS.browser, 10);
mylib.vars = org.apache.jmeter.threads.JMeterContextService.getContext().getVariables();

mylib.google = (function() {
  var googleUrl = &apos;https://www.google.co.jp&apos;;

  var access = function() {
    WDS.browser.get(googleUrl);
  };

  var search = function(keyword) {
  	var inputQuery = &apos;input#lst-ib&apos;;
  	mylib.wait.until(mylib.conditions.presenceOfElementLocated(mylib.pkg.By.cssSelector(inputQuery)));
    var inputElem = WDS.browser.findElement(mylib.pkg.By.cssSelector(inputQuery));
    inputElem.sendKeys(mylib.pkg.Keys.chord(mylib.pkg.Keys.CONTROL, &apos;a&apos;), keyword, mylib.pkg.Keys.ENTER);
  };

  var waitForResultsShown = function() {
    var resultListQuery = &apos;#search&apos;;
    mylib.wait.until(mylib.conditions.presenceOfElementLocated(mylib.pkg.By.cssSelector(resultListQuery)));
  };

  return {
    access: access,
    search: search,
    waitForResultsShown: waitForResultsShown
  };
})();
</stringProp>
              <stringProp name="Argument.metadata">=</stringProp>
            </elementProp>
          </collectionProp>
        </Arguments>
        <hashTree/>
        <com.googlecode.jmeter.plugins.webdriver.sampler.WebDriverSampler guiclass="com.googlecode.jmeter.plugins.webdriver.sampler.gui.WebDriverSamplerGui" testclass="com.googlecode.jmeter.plugins.webdriver.sampler.WebDriverSampler" testname="WebDriver Sampler1" enabled="true">
          <stringProp name="WebDriverSampler.script">//load mylib
eval(org.apache.jmeter.threads.JMeterContextService.getContext().getVariables().get(&apos;lib&apos;));

WDS.sampleResult.sampleStart();
mylib.google.access();
mylib.google.search(&apos;jMeter&apos;);
mylib.google.waitForResultsShown();


WDS.sampleResult.sampleEnd();</stringProp>
          <stringProp name="WebDriverSampler.parameters"></stringProp>
          <stringProp name="WebDriverSampler.language">javascript</stringProp>
        </com.googlecode.jmeter.plugins.webdriver.sampler.WebDriverSampler>
        <hashTree/>
        <com.googlecode.jmeter.plugins.webdriver.sampler.WebDriverSampler guiclass="com.googlecode.jmeter.plugins.webdriver.sampler.gui.WebDriverSamplerGui" testclass="com.googlecode.jmeter.plugins.webdriver.sampler.WebDriverSampler" testname="WebDriver Sampler2" enabled="true">
          <stringProp name="WebDriverSampler.script">//load mylib
eval(org.apache.jmeter.threads.JMeterContextService.getContext().getVariables().get(&apos;lib&apos;));

WDS.sampleResult.sampleStart();
mylib.google.access();
mylib.google.search(&apos;Selenium WebDriver&apos;);
mylib.google.waitForResultsShown();

WDS.sampleResult.sampleEnd();
</stringProp>
          <stringProp name="WebDriverSampler.parameters"></stringProp>
          <stringProp name="WebDriverSampler.language">javascript</stringProp>
        </com.googlecode.jmeter.plugins.webdriver.sampler.WebDriverSampler>
        <hashTree/>
        <ResultCollector guiclass="TableVisualizer" testclass="ResultCollector" testname="結果を表で表示" enabled="true">
          <boolProp name="ResultCollector.error_logging">false</boolProp>
          <objProp>
            <name>saveConfig</name>
            <value class="SampleSaveConfiguration">
              <time>true</time>
              <latency>true</latency>
              <timestamp>true</timestamp>
              <success>true</success>
              <label>true</label>
              <code>true</code>
              <message>true</message>
              <threadName>true</threadName>
              <dataType>true</dataType>
              <encoding>false</encoding>
              <assertions>true</assertions>
              <subresults>true</subresults>
              <responseData>false</responseData>
              <samplerData>false</samplerData>
              <xml>false</xml>
              <fieldNames>true</fieldNames>
              <responseHeaders>false</responseHeaders>
              <requestHeaders>false</requestHeaders>
              <responseDataOnError>false</responseDataOnError>
              <saveAssertionResultsFailureMessage>true</saveAssertionResultsFailureMessage>
              <assertionsResultsToSave>0</assertionsResultsToSave>
              <bytes>true</bytes>
              <threadCounts>true</threadCounts>
              <idleTime>true</idleTime>
            </value>
          </objProp>
          <stringProp name="filename"></stringProp>
        </ResultCollector>
        <hashTree/>
      </hashTree>
    </hashTree>
  </hashTree>
</jmeterTestPlan>

*1:ただし、今回紹介する方法だと共有できるのはあくまでコードだけで、状態は共有されないので注意

益川敏英『科学者は戦争で何をしたか』感想


正直な所、「微妙だ」という感想はあんまり力を入れて書きたくない。ただ、


を読んで、絶賛だけで終わるのはちょい違う気がするなあと思うのでちょっとネガティブな感想をまとめておきます。
この本を読んでいて一番引っかかったのは、科学者の戦争協力についてのこの文章

私がまだ学生だった頃、量子電磁力学の分野でノーベル賞を受賞した朝永振一郎博士が戦時下に書かれた論文を読んで、いたく感銘を受けたことがあります。朝永先生は戦時中、電波兵器の研究に動員されていました。「私はそんな研究に加担したくない」などと、戦時下での動員に抵抗すれば、たちまち非国民として投獄されてしまいます。
朝永先生も強制的にそうした研究に従事させられたわけですが、私は先生の論文を読んでいて、はたと膝を叩きたい思いに駆られました。量子力学を知っていればわりと簡単に見抜けることなのですが、電波の出力の関係を解析する部分を、限りなく一般的なところでまとめ、核心部分をうまくごまかしていたのです

以下、この文章についてTwitterでつぶやいていた感想のまとめ・補足



第二次大戦中の日本だと、自分の親族や隣近所の顔見知りな人たちが徴兵され、「敵」に殺されて死亡通知すら戻ってこないのが当たり前な状況でした。さらに1944年以降だと戦争に直接かかわらない市民が戦略爆撃で殺されていた訳です。そういう状況下で、本書の表現を借りれば"科学者である前に人間として"の観点から「自分の協力によって顔見知りの死や同胞の死を少しでも減らせるかもしれない」という発想が出て来るのはヒューマニストとして極自然なものではないでしょうか。
仮に上で引用したエピソードが事実であり、実際にサボタージュが行われていたとしたら、それはすさまじい苦悩の末の判断だったと思います。しかし………本書ではそのような苦悩が有ったのではないかということに触れられず、単純に素晴らしいサボタージュであり「本来の科学者の知恵」であると絶賛されています。


この本で一番危なさを感じるのはこの辺の話で、「自分の戦争協力によって顔見知りの死や同胞の死を少しでも減らせるかもしれない」という状況に追い込まれるかもしれないという可能性を考えていないように見えるところです。世論や状況が変わり、民間人の犠牲者が出た時には真っ先に戦争協力するんじゃないか的な気がします。それが良い・悪いという判断についてはここでは置いておくとして。


ファインマンがロスアラモスでの原爆開発に関わっていたことは本人が自伝で(物凄いノリノリに)書いていて有名ですが、彼も別に狂信的な国粋主義者でも無い訳で、戦時の高揚ってそういうものだと思うのです。ただ、この本では愛国心の甘美さとか戦時の高揚感、周りみんなが傷つく中で戦争に協力しないことへの罪悪感といったあたりのことについてはほぼ無視されていて危うげな気がします。

あー、あと蛇足ですがフリッツ・ハーバーが毒ガス開発に邁進した盲目的愛国者としてやり玉に上げられているあたりは

という背景について無視されてるのもなんだかなーという気がします。この辺は

毒ガス開発の父ハーバー 愛国心を裏切られた科学者 (朝日選書 834)

毒ガス開発の父ハーバー 愛国心を裏切られた科学者 (朝日選書 834)

に詳しいのでお勧め。

『撤退するアメリカと「無秩序」の世紀』についてメモ

米大統領選の大荒れ模様を見てて思い出したので軽くメモ。

撤退するアメリカと「無秩序」の世紀ーーそして世界の警察はいなくなった

撤退するアメリカと「無秩序」の世紀ーーそして世界の警察はいなくなった


まあ要約するなら「アメリカは"世界の警察"を捨てて内向き路線になりつつあるけれど、引きこもっても問題は解決しないししっぺ返しを食らうだけなので積極介入路線に戻ろうぜ」という本。で、何が気になったかというと、アメリカで中流層がどんどん貧しくなりつつあるから外征やめて引きこもろうという主張に対して

「高等教育のレベルは高いし、イノベイティブな起業家も一杯いるから大丈夫」

程度の軽い切り返しだけで終わらせている所。
例えば『誰が中流を殺すのか アメリカが第三世界に堕ちる日』に描かれているような、中流層が感じている不安の厳しさとの落差が気になってた。

誰が中流を殺すのか アメリカが第三世界に堕ちる日

誰が中流を殺すのか アメリカが第三世界に堕ちる日

なんというか、ビッグイシューを語る上でそんなの気にしないで問題無いという意識が透けて見えるというか、国際問題を語る上で「追い詰められた中流層」の話は一種のタブーになってるんじゃないかとも思うぐらいあっけらかんとした扱いで、最初に『撤退するアメリカと「無秩序」の世紀』を読んだ時には強烈な違和感があった。

で、米大統領選の大荒れって結局はこの辺の話になって、国内問題から目をそむけてきたエスタブリッシュメント層の傲慢さへの反発については色々と分析記事が出てるけれど、確かに自分が読んできた本を見てもそんな感じがするよなあというメモ。

現在中国では「隗より始めよ」の故事に"まず言い出しっぺが率先して実行しろ"というニュアンスは無いんじゃないかな?という話

自己まとめ。なんとなく気になったのでざっくり調べてみた。




「まず隗より始めよ」の元になったエピソード
manapedia.jp

は中国では"千金买骨"(骨でも大金で買う)として知られており
baike.baidu.com

その意味するところは

道理

这个故事告诉我们,要招聘人才,不仅仅要放下架子,更要有诚心,要拿出实际行动。郭隗的聪明睿智令人佩服,他的勇于自荐,自我推销的方式也很有艺术性。
现用来比喻渴望求得贤才。

とされており、Excite翻訳してみると

道理

このストーリは私達に教えて、人材を招聘して、棚をおろすだけではなくて、更に真心があって、実際行動を取り出します。郭と隗の賢い英知は人を感心させて、彼のは勇敢に自薦して、自ら売りさばく方法もとても芸術性があります。
現在比喩が賢才を求めるのを渇望するのに用います。

となる。要は「人材募集方法すげー、セルフブランディングすげー」というサクセス・ストーリーとして、そして「人材を大切にしよう」と言う意味で受け取られており、現在は「才能のある人を求めるならこういう方法を取るべきだ」という比喩として使われているとのこと。
まあ、あくまで「ソースは百度百科(キリッ」だし自動翻訳とあやふやな漢文理解だけなので断定出来ないんだけれど、この故事について"まず言い出しっぺが率先して実行しろ"というニュアンスは主流では無さそうということは分かる。


同じ古典をベースにしていてここまで解釈が違うというのは面白いし、「一見同じ古典文化を共有しているように見えるけど解釈がだいぶ違っていて、相互に誤解しっぱなし」という話は他にも結構有りそう。

P.W.シンガー 、オーガスト・コール 『ゴースト・フリート』感想

『戦争請負会社』『ロボット兵士の戦争』『子ども兵の戦争』といった一連のノンフィクションで、冷戦期の「戦争」観が覆りつつある状況を描いてミリヲタの話題をさらったP.W.シンガー。
そのシンガーが手がけた近未来米中仮想戦記ということで話題になっていた『ゴースト・フリート』が遂に翻訳されたので紹介。


中国軍を駆逐せよ!  ゴースト・フリート出撃す(上) (二見文庫 ザ・ミステリ・コレクション)

中国軍を駆逐せよ! ゴースト・フリート出撃す(上) (二見文庫 ザ・ミステリ・コレクション)

中国軍を駆逐せよ!  ゴースト・フリート出撃す(下) (二見文庫 ザ・ミステリ・コレクション)

中国軍を駆逐せよ! ゴースト・フリート出撃す(下) (二見文庫 ザ・ミステリ・コレクション)


amazonあらすじ。

2026年、中国が太平洋支配に動き、ハワイ制圧!
ロシアと同盟を組んで、太平洋制圧に挑む中国。
嘉手納基地急襲、国防総省サイバー攻撃を経て、オアフ島に上陸!

共産党支配からより少数独裁的な「董事会」体制に変わった中国は、2026年、マリアナ海溝近辺でガス田を発見、太平洋支配へと動きだした。
密かに同盟を結ぶロシアが嘉手納基地を急襲したのに続いて、中国はパナマ運河を通行不能にし、真珠湾で米軍艦船を爆破、
太平洋艦隊にも大打撃を与え、オアフ島に上陸してハワイを統治下に置くことに──。
中国のサイバー攻撃によりハイテク機器が使えないアメリカは、ハッキングの影響を受けない、現役を退いた旧い艦艇からなる「幽霊艦隊(ゴースト・フリート)」で
ハワイ奪還を目指すが──。
原題:Ghost Fleet

以下、ネタバレ

続きを読む

「The Making of Stanley Kubrick's 2001 - a Space Odyssey」感想

買ったのは10月ぐらいだったけど書いてなかったので

The Making of Stanley Kubrick's 2001 - a Space Odyssey

The Making of Stanley Kubrick's 2001 - a Space Odyssey


映画版『2001年宇宙の旅』のメイキング本。
この本、何が素晴らしいかというと、ハコのサイズがだいたい1:4:9というモノリス比率なところ。
まあ正確に言うと 4.5 cm x 17.5 cm x 38.2 cm ぐらいで厳密に1:4:9な訳ではないけど、将来的に日本語版が出るにしてもこのハコで出るとは限らないので買ってみた。内容的にも満足ですはい。

70年代を舞台にした第三次世界大戦海戦ゲーム『Command: Northern Inferno』紹介

Command: Northern Inferno は海空戦ゲーム Command: Modern Air / Naval Operations (CMANO)から派生したゲームです。

大本のCommand: Modern Air / Naval Operationsについての紹介記事はこちら

ka-ka-xyz.hatenablog.com

ka-ka-xyz.hatenablog.com

ka-ka-xyz.hatenablog.com


Command: Northern Infernoは、CMANOのゲームシステムを使用して70年代の「あり得たかもしれない」第三次世界大戦の海空戦を扱ったゲームです。アイスランド北大西洋ノルウェー付近を中心に扱った13のシナリオを(シングルシナリオあるいは連続したキャンペーンシナリオとして)プレイする事が可能です。
既にCommand: Modern Air / Naval Operationsを持っている人は拡張シナリオ集として購入可能。持ってない人はスタンドアローンなゲームとして同じ価格で購入可能。

どんなゲーム?

基本はCMANOと同様。ユニットは全てNTDSアイコンで表示され、リアルタイム(時間圧縮・停止有り)で動いていきます。
要はこういう画面。

f:id:ka-ka_xyz:20151108225508p:plain

こういう画面から「ノルウェー海軍の高速艇戦隊がソ連の揚陸船団へ強襲をかけるありさま」を妄想できるかどうかが、このゲームを楽しめるかどうかの鍵となります。

核もあるよ(使用許可が出た場合のみ)

攻略Tips

シナリオ1: Opening Moves

GIUKギャップ(グリーンランドアイスランド・英国を結ぶライン)を突破しようとするソ連潜水艦を狩るシナリオ。
といってもSOSUSで全ての潜水艦の位置は既に掴まれているので、航空機やヘリを使って反撃されない立場から一方的に殴っていくだけの簡単なシナリオです。注意点としては、攻撃前にちゃんと敵かどうかを判定すること。最初は潜水艦は黄色いアイコン(敵味方不明)で示され、敵潜水艦なのか生物学的な(イルカとかクジラとかマグロの群れとか)音響なのか区別されていません。周囲にソノブイを落として潜水艦であることをきっちり判定してから狩りましょう。
(実際の所、うっかり海洋生物を攻撃してもペナルティは無い)

シナリオ2: Goblin on the doorstep

イギリスの弾道ミサイル原潜 HMSリベンジ を大西洋上の哨戒海域まで護衛するシナリオ。敵ユニットは潜水艦とMayak級の情報収集船複数

Mayakは限定的な対潜能力を持ち、放置しておくと危険です。ただ、陸上基地から発信する航空機は対艦ミサイルを装備できないため、こちらも水上艦で対応することになります。また、水上艦搭載のヘリも対艦ミサイルを搭載出来るものもあるので、兵装を換装して置いたほうが良いはず。

また、SSBN HMSリベンジについては"Doctrin & RoE"の設定で "Automatic evasion"の値を「No」に変えるのがおすすめ。初期値のままだと敵を探知するたびにあさっての方向へ転舵してしまうので厄介です。
また、航空機で洋上哨戒する際に、レーダーで敵味方不明な船を見つけても敵か味方か識別できない場合があります。その時は高度を雲の下に指定すると、目視確認が効きます。

あとは、陸上基地から発信した哨戒機やヘリを使って、HMSリベンジの周囲とノース海峡の北側出口付近にソノブイの網を張り、徐々に外側へ広げていけばそのうち潜水艦が捕まるはずです。

シナリオ3: The Fast and the Furious



注意!
このシナリオは現在のところ、ここで配布されているhotfixを適用しないとクリア不可能です。勝利条件を満たしていても次のキャンペーンへ進みません。steamではhotfixがまだ配布されていないので、上記のforumからファイルを入手する必要があります。
また、hotfix適用後にいったんシナリオ3のsaveファイルを削除し、シナリオ2からやりなおして改めてシナリオ3を開始する必要があります。


ノルウェー北岸へ接近しつつ有るソ連両用(揚陸)船団を撃退するシナリオ。
手駒として使える航空機は少数のF-5軽戦闘機とその偵察機タイプ、哨戒ヘリのみ。海上戦力は魚雷艇ミサイル艇を中心としたものであり、あまり有力なものではありません。
一方ソ連側は、揚陸戦団の直衛戦力とは別に、多数のオーサ型高速ミサイル艇が間接護衛戦力として待ち構えています。手駒の高速艇を使って最初から揚陸船団を襲撃すると、オーサ型に寄ってたかって殴り返されるので注意が必要です。

ということで、F-5戦闘機を使ってオーサ型ミサイル艇を狩りつくしてから、水上戦力で揚陸船団を襲撃するのが一番すんなり行きます。ただ、F-5の使い方が結構厄介で、普通に使っているととてもオーサ型ミサイル艇を狩りつくせません。とりあえず

  • F-5ユニットの"Doctrin & RoE"で"RTB when Winchester"(兵装を使いきった場合は基地へ帰投)を「No」に指定(兵装を使い終わった途端に敵の防空網を突っ切って帰還しようとする場合があるので防止のため)
  • F-5ユニットの"Doctrin & RoE"で"Use gun against surface/land contacts (starfing)"を「Yes」に指定(ミサイル艇を相手にする場合、機銃の攻撃が結構効く場合も有るため)
  • 初期兵装 Mk20 Rockeye II CB の機体はとりあえず出撃させるが、帰投時に AGM-12B Bullpup Aへ兵装転換(Rockeye IIは全く役にたたない。Bullpupは長射程なうえに一発当たればミサイル艇にとって致命傷。ただし、兵装の残数が少ないので注意)
  • Bullpupや通常爆弾を使ってオーサ型を攻撃するときは、自動攻撃せずに手動で攻撃すること(一発当たれば致命傷なのに自動攻撃だと一回で全弾を撃ち尽くすため)
  • F-5は出撃後再補給に約5時間かかるので、だいたい2出撃が限界というか3出撃前に揚陸船団が揚陸ポイントに達してしまう。2出撃目までにオーサ型ミサイル艇を可能な限り減らす
  • F-5を使ってオーサ型ミサイル艇を減らすまでは、水上艦艇は初期位置のちょと西寄りあたりに退避。ただし、あまり西へ行き過ぎると揚陸船団の迎撃に間に合わなくなるので注意
  • 揚陸艦が積んでいるハインド攻撃ヘリ魚雷艇にとって結構脅威なので、水上艦艇で揚陸船団を襲撃するタイミングで、F-5の上空直護を付ける
  • 揚陸船団本体は対空防御は強いものの、対艦攻撃能力は低い。オーサ型さえなんとかすれば手持ちの戦力で殴り放題出来る。ただし、西の端にいるFFGについては、もし余裕があればBullpupを装備したF-5で予め沈めてしまうと楽になるはず。


このシナリオだけ攻略情報が細かいのは、最初の注意点で引っかかって5回ぐらいやりなおしたから。

シナリオ4: Barents Sea Boomers

バレンツ海ソ連の「聖域」(厳重に防衛されたSSBN哨戒海域)へ潜入したスタージョン級SSNでSSBNを狩るシナリオ。
このシナリオは多分に運が絡む。というか運ゲー。Creep速度で哨戒海域をぐるぐるめぐり、運が良ければSSBNと巡り会える。運が悪ければ対潜哨戒機から魚雷落とされて一方的にボコられて終わります。
注意点として、同じ海域でプレイヤーの指揮下に無いイギリスのスイフトシュア級SSNも行動しているので、同士討ちを避けること・・・つまり、「ソナーでSSNを探知して10時間追い続けた挙句に味方と判明」もあり得えます。


今のところシナリオ5プレイ中なので攻略情報はここまで。