2014年1月25日(日)、株式会社ネオジニア様が主催するプログラミング大会「ネオ富豪」に参加しました。
「ネオ富豪」とは、トランプゲーム”大富豪”を、参加者それぞれが作成したプログラムで対戦する大会です。さて、リサイクが準備したプログラム“risa@徹夜”は、好成績を残せたのでしょうか。
当日の様子
参加者が会場であるネオジニア様(以降、主催者)の事務所(以下、会場)へノートPCを持ち寄って集結しました。1名は会場へ来れないという事で、Skypeのテレビ電話で参加されていました。
会場では、主催者が無線LANを用意して下さっており、それぞれのノートPCはその無線LAN経由でインターネットに接続しました。対戦は、参加者が作成したプログラムを、それぞれが持ち寄ったノートPC上で実行して行いました。ちなみに主催者である前田さんはAndroidスマートフォンからの参戦です。
「ネオ富豪」について
参加するプログラムから主催者が用意したマスタサーバへWebSocketのコネクションをはると、状態遷移の度にサーバからJSON形式のメッセージが送信されてきます。自分の番が回ってきた時に、マスタサーバに対して提出するカードを知らせます。それを繰り返す事により、ゲームが進行します。マスタサーバはクラウド上に配置されているので、インターネットさえつながれば世界中どこからでも参戦が可能です(ですので、会場に来れなかった方も参加できました)。
…ちょっと難しいですよね。
今までの話を図で表すとこんな感じです。

戦いの様子は、マスタサーバが公開している観戦用ページで見ることができました。

WebSocketやクラウド等、新しい技術に気軽に触れられ、参加者にとって勉強になります。また、プログラム側へ送られるJSONメッセージ仕様もステートレスでいつでも再接続可能なため、参加者のプログラミングの難易度も十分に低くなるように考慮されています。さらに、プログラミングに詳しくない人でも観戦用ページの出来が素晴らしいため、十分楽しめるものになっています。
ネオジニア様の企画力、設計力、技術力の高さが存分に発揮されています。すばらしいです!
参戦したプログラムについて
仕事が忙しく、大会前日の前日の24時から作成に着手して翌朝8時にひと段落しました。作成時間は8時間です。なのでプログラム名は“risa@徹夜”です。本当は、もっと組み込みたい処理がたくさんあったのですが、仮眠しないと身が持たないのと、実装を開始しても大会開始の14時までに完成する確信が無かったからです。
ジョーカーがオールマイティとして使えるというのを失念していたり、基本的には出せる一番弱いカードを出すだけという残念なロジックですが、成績はというと…
な、なんと。優勝しちゃいました!ちゃんとした景品までいただきました。

勝因は何かと問われると「運」だと思います。大富豪は運の要素が強いゲームです。ただ、試合風景を観戦していると、無駄な手がかなりあり、まだまだ強くすることが出来そうです。
…勝因を「運」と言い切ってしまうと面白くないですね。敢えて言うなら「参加した事」ですかね。忙しさを言い訳に何もしなければ何も変えられません。挑戦しなければ何も得られない、挑戦すれば何かを得られるかもしれないという事かと思います。
ちなみに景品は、リサイクのメンバーでの食事に使わせてもらおうかと考えています。
※全体の成績等はネオ富豪公式サイトをご参照ください。
最後になりましたが、ネオジニア様へ
おかげさまで貴重な経験をさせていただきました。大会に向けて大変なご苦労をされたことかと思います。ほんとうにお疲れ様でした&ありがとうございました。あと、終わってからこれを言うのは酷かもしれませんが、次のイベントも楽しみにしています。もっと大きなイベントを行うなら、共催も喜んで引き受けます!
おまけ
今回の大会に参戦したプログラム“risa@徹夜”のソースコードを公開します。急いで作ったので汚いですし、動作実績のない個所もありますが、ほぼそのまま公開します。
WebSocketの接続を閉じるところでステータスコード4500を意味無く設定しているのはちょっとひどいですね。これは参考にさせて頂いたページのコードをそのままにしたせいです。
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>ネオ富豪参加プログラム "risa@徹夜"</title>
<script type="text/javascript">
const PGNAME = 'risa%40%e5%be%b9%e5%a4%9c';
const KIGOJK = 'JK';
const KIND = ['C', 'H', 'D', 'S'];
const NO = ['3', '4', '5', '6', '7', '8', '9', '0', 'J', 'Q', 'K', 'A', '2']; // 平常時の弱い順
var Test1 = function() {
this.received = document.getElementById("received");
this.message = document.getElementById("message");
this.ws = new WebSocket('ws://neof5master.********************/test/connection?name=' + PGNAME);
var outer = this;
// コールバック関数
this.ws.onmessage = function(event) {
var li = document.createElement("li");
li.innerHTML = event.data;
outer.received.appendChild(li);
};
// メッセージ送信
this.send = function(){
this.ws.send(message.value);
};
// 明示的に接続を閉じる
this.close = function(){
var code = 4500;
var reason = "close";
this.ws.close(code,reason);
};
}
var Test2 = function(roomNo) {
main(roomNo, 'ws://neof5master.********************/test/ruletest/');
};
var Test3 = function(roomNo) {
main(roomNo, 'ws://neof5master.********************/test/practice/');
};
var Performance = function(roomNo) {
main(roomNo, 'ws://neof5master.********************/play/A/');
};
var main = function(roomNo, uri) {
this.received = document.getElementById("received");
this.MyNum = document.getElementById("MyNum");
this.Kind = document.getElementById("Kind");
this.Teban = document.getElementById("Teban");
this.IsKakumei = document.getElementById("IsKakumei");
this.PlayerInfo = document.getElementById("PlayerInfo");
this.Deck = document.getElementById("Deck");
this.Ba = document.getElementById("Ba");
this.Yama = document.getElementById("Yama");
this.History = document.getElementById("History");
this.answer = document.getElementById("answer");
this.ws = new WebSocket(uri + roomNo + '?name=' + PGNAME);
var outer = this;
this.ws.onmessage = function(event) {
var json = JSON.parse(event.data);
outer.MyNum .innerHTML = json['YourNum'];
outer.Kind .innerHTML = json['Kind'];
outer.Teban .innerHTML = json['Teban'];
outer.IsKakumei .innerHTML = json['IsKakumei'];
outer.PlayerInfo.innerHTML = json['PlayerInfo'];
outer.Deck .innerHTML = json['Deck'];
outer.Ba .innerHTML = json['Ba'];
outer.Yama .innerHTML = json['Yama'];
outer.History .innerHTML = json['History'];
if(json['Kind']=='ProcessTurn') {
var risa = new RisaBrain(json);
var nextCards = risa.nextCards();
var nextCardsStr = nextCards.join(' ');
var messageObj = {
Kind : "Put",
Cards : nextCardsStr
};
outer.answer.innerHTML = nextCardsStr;
outer.ws.send(JSON.stringify(messageObj));
}
};
// 明示的に接続を閉じる
this.close = function(){
var code = 4500;
var reason = "close";
this.ws.close(code,reason);
};
}
// このプログラムの頭脳
var RisaBrain = function(json) {
this.orgDeck = json['Deck'];
this.orgBa = json['Ba'];
this.isKakumei = json['IsKakumei'];
// 残りのカードを弱い順に算出
// ほんとうは毎回算出する必要はないが、大富豪という事で、富豪プログラミング
this.remainingJk = true;
this.remainingCards = new Array(52);
// コンストラクタ中の処理みたいに書きたいけど
// やり方わからなくなった
this.setRemaining = function() {
var index=0;
for(var i=0; i<NO.length; i++) {
for(var j=0; j<KIND.length; j++) {
this.remainingCards[index++] = KIND[j] + NO[i];
}
}
var history = json['History'];
var historyArray = new Array();
for(var hi=0; hi < history.length; hi++) {
if(endwith(history[hi], "PASS") || history[hi] == "/") {
continue;
}
historyArray = historyArray.concat(history[hi].split("[")[1].split("]")[0].split(" "));
}
// ブロックスコープじゃないからループカウンタかえなきゃ
// 流用してもいいけど
for(var k=0; k<historyArray.length; k++) {
for(var l=0; l<this.remainingCards.length; l++) {
if(historyArray[k] == KIGOJK) {
this.remainingJk = false;
} else if(historyArray[k] == this.remainingCards[l]) {
this.remainingCards.splice(l, 1);
l--;
}
}
}
};
// 参照渡しにしたいからジョーカーは配列
// かっこわるい
this.deck = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
this.deckJk = [false];
this.lastBa = this.orgBa.length == 0 ? "" : this.orgBa[this.orgBa.length-1];
this.arrangedLastBa = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
this.lastBaJk = [false];
this.arrange = function(from, target, targetJk) {
// カードの整理
for(var i=0; i < target.length; i++) {
var fromArray = from.split(" ");
for(var j=0; j < fromArray.length; j++) {
var kigo = fromArray[j].substring(0, 1);
var num = fromArray[j].substring(1, 2);
if(fromArray[j] == KIGOJK) {
targetJk[0] = true;
continue;
}
for(var k=0; k < NO.length; k++) {
if(num == NO[k]) {
if(i == k) {
target[i]++;
break;
}
}
}
}
}
};
this.arrange(this.orgDeck, this.deck, this.deckJk);
this.arrange(this.lastBa, this.arrangedLastBa, this.lastBaJk);
this.nextCards = function() {
this.setRemaining();
// あまりきれいではない
var baForce = 0;
var baIndex = this.isKakumei ? 13 : -1;
var ret = new Array();
for(var i=this.arrangedLastBa.length-1; i>=0; i--) {
if(this.arrangedLastBa[i] > 0) {
baForce = this.arrangedLastBa[i];
baIndex = i;
break;
}
}
// 場にジョーカーがあったらすぐあきらめる。
if(this.lastBaJk[0]) {
return [];
}
// ジョーカーがあって、ジョーカー以外が残り1枚ならジョーカーを出す
if(this.deckJk[0] && (baForce == 0 || baForce == 1)) {
var cnt = 0;
for(var i=0; i<this.deck.length; i++) {
if(this.deck[i]>0) {
cnt++;
}
}
if(cnt==1) {
return [KIGOJK];
}
}
// ひねればシンプルにできそう
// でも眠いので無理しない
if(!this.isKakumei) {
// 革命中ではない
/*
// ちゃんと考えるアルゴリズムも入れないと面白くない
// ただ、場のカードが0枚か1枚のときしか頭が回らない
⇒挫折。せっかく残りのカード全部わかるようにしたのに。。。
if(baForce == 0 || baForce == 1) {
if(this.deckJk[0]) {
// まず、自分がジョーカーをもっている場合
// ⇒確実に他の人はパス
// 次に自分のカードの中で一番強いのは?
var strong;
for(var i=this.deck.length-1; i <= 0; i--) {
if(this.deck[i] > 0) {
strong = i;
break;
}
}
}
}
*/
// 出せるカードのうち、一番弱いのを出すロジック
for(var i=baIndex+1; i<this.deck.length; i++) {
if(this.deck[i] >= baForce) {
var no = NO[i];
var splitOrgDeck = this.orgDeck.split(" ");
for(var j=0; j<splitOrgDeck.length; j++) {
var kigo = splitOrgDeck[j].substring(0, 1);
var num = splitOrgDeck[j].substring(1, 2);
if(no == num) {
ret.push(kigo+num);
if((ret.length >= baForce && baForce != 0) ||
(ret.length >= this.deck[i] && baForce == 0)) {
// うっかり革命を起こしそうなときはちょっと冷静になる
if(ret.length == 4) {
var smaller = 0;
var bigger = 0;
for(var k=0; k < 6; k++) {
if(k != i) {
smaller++;
}
}
for(var l=6; l < 13;l++) {
if(l != i) {
bigger++;
}
}
if(smaller < bigger) {
// 大きな数字の方が多いから
// 革命は遠慮する
if(baForce == 0) {
ret = ret.splice(0, 1);
} else {
ret = [];
}
}
}
return ret;
}
}
}
}
}
} else {
// 革命中!
for(var i=baIndex-1; i>=0; i--) {
if(this.deck[i] >= baForce) {
var no = NO[i];
var splitOrgDeck = this.orgDeck.split(" ");
for(var j=0; j<splitOrgDeck.length; j++) {
var kigo = splitOrgDeck[j].substring(0, 1);
var num = splitOrgDeck[j].substring(1, 2);
if(no == num) {
ret.push(kigo+num);
if((ret.length >= baForce && baForce != 0) ||
(ret.length >= this.deck[i] && baForce == 0)) {
if(ret.length == 4) {
var smaller = 0;
var bigger = 0;
for(var k=0; k<6; k++) {
if(k != i) {
smaller++;
}
}
for(var l=6; l<13;l++) {
if(l != i) {
bigger++;
}
}
if(smaller > bigger) {
// 小さな数字の方が多いから
// 革命返しは遠慮する
if(baForce == 0) {
ret = ret.splice(0, 1);
} else {
ret = [];
}
}
}
return ret;
}
}
}
}
}
}
// 出せるカードが無いと思いきや、ジョーカーがある。
// ⇒ジョーカー以外が残り1枚なら出す
if(ret.length == 0 && this.deckJk[0]) {
if(baForce == 1) {
var cnt = 0;
for(var i=0; i<this.deck.length; i++) {
if(this.deck[i]>0) {
cnt++;
}
}
if(cnt==1) {
return [KIGOJK];
}
}
}
return ret;
};
}
function endwith(value, suffix) {
var sub = value.length - suffix.length;
return (sub >= 0) && (value.lastIndexOf(suffix) === sub);
};
var test1;
var test2;
var test3;
var performance;
</script>
</head>
<body>
<h1>ネオ富豪参加プログラム "risa@徹夜"</h1>
<hr>
<h2>test1</h2>
<p>受信メッセージ表示領域</p>
<ul id="received">
</ul>
<input type="button" value="test1 open" onclick="test1 = new Test1();"><br>
<input id="message" type="text">
<input type="button" value="test1 start" onclick="test1.send()"><br>
<input type="button" value="test1 close" onclick="test1.close()">
<hr>
<h2>test2</h2>
<input id="roomNo" type="text">
<input type="button" value="test2 open" onclick="test2 = new Test2(document.getElementById('roomNo').value);"><br>
<input type="button" value="test2 close" onclick="test2.close()">
<hr>
<h2>test3</h2>
<input id="roomNoTest3" type="text">
<input type="button" value="test3 open" onclick="test3 = new Test3(document.getElementById('roomNoTest3').value);"><br>
<input type="button" value="test3 close" onclick="test3.close()">
<hr>
<h2>本番A</h2>
<input id="roomNoPerformance" type="text">
<input type="button" value="performance open" onclick="performance = new Performance(document.getElementById('roomNoPerformance').value);"><br>
<input type="button" value="performance close" onclick="performance.close()">
<hr>
<table>
<tr><td>MyNum</td><td><span id="MyNum"></span></td></tr>
<tr><td>Kind</td><td><span id="Kind"></span></td></tr>
<tr><td>Teban</td><td><span id="Teban"></span></td></tr>
<tr><td>IsKakumei</td><td><span id="IsKakumei"></span></td></tr>
<tr><td>PlayerInfo</td><td id="PlayerInfo"></td></tr>
<tr><td>Deck</td><td><span id="Deck"></span></td></tr>
<tr><td>Ba</td><td id="Ba"></td></tr>
<tr><td>Yama</td><td><span id="Yama"></span></td></tr>
<tr><td>History</td><td id="History"></td></tr>
</table>
Answer is <span id="answer"></span>
</body>
</html>
