[PHP]文字化けせずにCSVファイルを読み込み、配列に変換する

CSV ファイルを読み込むとき、最もシンプルな方法は次のようなものです。

<?php
$csv  = array();
$file = 'test.csv';
$fp   = fopen($file, "r");

while (($data = fgetcsv($fp, 0, ",")) !== FALSE) {
  $csv[] = $data;
}
fclose($fp);

var_dump($csv);

fgetcsv() を用いて単純に一行ずつ処理しています。
プログラム側と CSV ファイルの文字コードが同じであればこの方法でもいいのですが、エクセルなどで作られた CSV あるいは TSV(タブ区切り)などは、Shift-JIS で保存されている場合が多いため、PHP側が UTF-8 で記述されている時、上の方法で文字コードを変換するには、mb_convert_variables() を使って再帰的に処理するか、ループ内で一行ずつ mb_convert_encoding() していくことになってしまいます。

しかしそのやり方はかなり無駄が多いので、file_get_contents() でファイル内容を取得し、一旦文字コードを変換した一時ファイルを作ってから、それを CSV ファイルとして再度読み込みます。

<?php
setlocale(LC_ALL, 'ja_JP.UTF-8');

$file = 'test.csv';
$data = file_get_contents($file);
$data = mb_convert_encoding($data, 'UTF-8', 'sjis-win');
$temp = tmpfile();
$csv  = array();

fwrite($temp, $data);
rewind($temp);

while (($data = fgetcsv($temp, 0, ",")) !== FALSE) {
	$csv[] = $data;
}
fclose($temp);

var_dump($csv);

このやり方を使えば mb_convert_encoding() を一回使うだけでエンコードの異なる CSV ファイルの読み込みができます。
tmpfile() がどこに一時ファイルを作成するかは sys_get_temp_dir() を使って調べられます。もし tmpfile() の結果が false の場合はそのフォルダに適切なパーミッションや所有者が設定されているか確認して下さい。

TSV 形式を読み込む場合でも fgetcsv() で読み込むことができます。

fgetcsv($temp, 0, "\t")

SplFileObject を利用する方法でも同様に読み込むことが出来ます。PHP 5.1 以上で利用できるクラスですが fgetcsv() よりも高速に動作します。
通常では文字列やファイルストリームを SplFileObject に変換することは出来ないので、一時ファイルのメタデータからファイルパスを取得して読み込みます。

<?php
setlocale(LC_ALL, 'ja_JP.UTF-8');

$data = file_get_contents("test.csv");
$data = mb_convert_encoding($data, 'UTF-8', 'sjis-win');
$temp = tmpfile();
$meta = stream_get_meta_data($temp);

fwrite($temp, $data);
rewind($temp);

$file = new SplFileObject($meta['uri']);
$file->setFlags(SplFileObject::READ_CSV);

$csv  = array();

foreach($file as $line) {
    $csv[] = $line;
}

fclose($temp);
$file = null;

var_dump($csv);

fclose() を使った段階で一時ファイルは自動的に消えてしまいますので不要になった SplFileObject も破棄しておきます。

[PHP]トランプのブラックジャックを作る

ブログに載せられる程度の行数でトランプゲームの一種、ブラックジャックを作ってみました。

ルールとしては、二人のプレイヤーがそれぞれ二枚のランダムなカードを持った状態でスタートし、手札の合計を考慮した上でカードを引く(Hit)か、そのまま(Stand)を選びます。

最終的に手札の合計が 21 に近いほうが勝ちなのですが 21 を超えてしまうと負けとなります。(バースト)

ジャック・クイーン・キングはいずれも 10 として計算され、エース(A)は 1 か 11 か好きなほうとしてカウントできます。

<?php
session_start();

$end_game = false;		//終局フラグ
$cards	 = array();		//山札
$player	 = array();		//プレイヤーの手札
$opp	 = array();		//対戦相手の手札

if(!isset($_GET['reset'])){
	if(isset($_SESSION['cards']))		 $cards	 = $_SESSION['cards'];
	if(isset($_SESSION['player']))		 $player = $_SESSION['player'];
	if(isset($_SESSION['opponent']))	 $opp	 = $_SESSION['opponent'];
}

if(isset($_SESSION['cards']) && !isset($_GET['reset'])){
	$cards = $_SESSION['cards'];
} else {
	$cards = init_cards();
	//2枚ずつカードを配る
	$player[]	 = array_shift($cards);
	$player[]	 = array_shift($cards);
	$opp[]		 = array_shift($cards);
	$opp[]		 = array_shift($cards);
}

if(isset($_GET['hit'])) $player[] = array_shift($cards);

//対戦相手の思考と終局判定
if( isset($_GET['hit']) || isset($_GET['stand']) ){
	$threshold = 15;	//ヒットするしきい値
	if( sum_up_hands($opp) < $threshold ){
		$opp[] = array_shift($cards);
	} else if( isset($_GET['stand']) ){
		$end_game = true;
	}
}

//合計
$player_total	 = sum_up_hands($player);
$opp_total		 = sum_up_hands($opp);

if($player_total > 21 || $opp_total > 21) $end_game = true;

$_SESSION['cards']		 = $cards;
$_SESSION['player']		 = $player;
$_SESSION['opponent']	 = $opp;

function init_cards(){
	//山札を用意する
	$cards = array();
	$suits = array('spade', 'heart', 'diamond', 'club');

	foreach($suits as $suit){
		for($i=1;$i<=13;$i++){
			$cards[] = array(
				'num' => $i,
				'suit' => $suit
			);
		}
	}
	shuffle($cards);
	return $cards;
}

function sum_up_hands($hands){
	$ace = 0;
	$total = 0;
	foreach($hands as $card){
		$num = $card['num'];
		if($num > 10){
			$total += 10;
		} else if($num === 1){
			$ace++;
			$total += 1;
		} else {
			$total += $num;
		}
	}
	//Aの処理
	if(!empty($ace)){
		$add = 10 * floor( (21 - $total) / 10 );
		if($add > 0) $total += $add;
	}
	return $total;
}
?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<title>Black Jack</title>
</head>
<body>
<p>Your Hand: 
<?php
foreach($player as $card){
  echo $card['num'] . ' ';
}
?>
<br />
Total:
<?php
echo $player_total;
if($player_total > 21){
	echo ' Burst';
} else if($end_game === true && $player_total > $opp_total){
	echo ' Win';
}
?>
</p>

<hr />

<p>Opponent's Hand: 
<?php
foreach($opp as $card){
  echo $card['num'] . ' ';
}
?>
<br />
Total:
<?php
echo $opp_total;
if($opp_total > 21){
	echo ' Burst';
} else if($end_game === true && $opp_total > $player_total){
	echo ' Win';
}
?>
</p>

<hr />

<ul>
  <?php if($end_game === false):?>
  <li><a href="?hit">Hit</a></li>
  <li><a href="?stand">Stand</a></li>
  <?php endif;?>
  <li><a href="?reset">Reset</a></li>
</ul>
</body>
</html>

ゲームデータの保持にはファイル一つで済ませるためにセッションを使いました。
カードはスート(スペードなどの4種類のシンボル)ごとに配列の形で生成され、shuffle() を用いてシャッフルされます。
カード一枚は、スートと数値で構成されています。

カードを引く動作は array_shift() で行います。
カードを引くかどうかの選択は GET のパラメータで判断する仕組みです。

エースの扱いがやや難しいですが、式としてはシンプルになりました。
ひとまず全て 1 として計算した上で、バーストしない範囲で 10 ずつ足すという形です。
要するに上限の 21 からエースを 1 として扱った場合の合計を引き、それを 10 で割った整数部が 11 として扱えるエースの枚数になります。すでに 1 として合計済みなので、一枚につき 11 を足さずに 10 を足します。

対戦相手の思考は単純で、手札の合計がある値よりも小さければカードを引き、大きければスタンドします。
両者ともにヒットしていないか、どちらかがバーストしていればゲーム終了となり、勝敗を判定します。

ゲームと呼ぶには文字ばかりでおもしろみがありませんが、
内部的にはハートやダイヤなどの情報も持っているので、カードのグラフィックを用意してみるとそれらしくなると思います。

[PHP][jQuery]Ajax(非同期通信)を使ったチャット

あまり JavaScript の Ajax に関して経験がないので、
ちょっとした練習としてチャットのようなものを作ってみました。
練習用の単純なプログラムですので脆弱性等の問題があります。実際のサーバーでは運用しないで下さい。
また、Google Chrome ではローカルファイルの読み込みに制限があります。

非同期通信を利用するために jQuery を利用しています。

・表示部分(HTMLファイル)

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<title>Chat</title>
<script src="//ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js"></script>

<style type="text/css">
#log {
	border: 1px solid #AAA;
	height:400px;
	width:600px;
	overflow: scroll;
}
</style>

</head>

<body>

<script type="text/javascript">

function load_log()
{
	$.ajax({
		type: 'post',
		url: './log.txt',
		success: function( data ){
			log = data.replace(/[\n\r]/g, "<br />");
			$('#log').html(log);
		}
	});
}

function write_message()
{
	$.ajax({
		type: 'post',
		url: 'write.php',
		data: {
			'message' : $("#message").val()
		},
		success: function(){
			load_log();
			$("#message").val('');
		}
	});
}

$(document).ready(function()
{
	load_log();
	setInterval('load_log()', 5000);
});

</script>

<form method="post" onsubmit="write_message();return false;">
  <input id="message" name="message" type="text" value="" />
  <input type="button" value="送信" onclick="write_message()">
</form>

<div id="log"></div>

</body>
</html>

・write.php(書き込み部分)

<?php
// Ajax以外からのアクセスを遮断
$request = isset($_SERVER['HTTP_X_REQUESTED_WITH']) ?
	strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) : '';
if($request !== 'xmlhttprequest') exit;

$message = (isset($_POST['message']) && is_string($_POST['message'])) ?
	htmlspecialchars($_POST['message'], ENT_QUOTES) : "";
if($message == "") exit;

$max   = 30;		// 行の上限
$count = 0;
$log   = '';

$fp = fopen('log.txt', 'r');
if(flock($fp, LOCK_SH)){
	while(!feof($fp)){
		if($count >= $max - 1) break;
		$log .= fgets($fp);
		$count++;
	}
	flock($fp, LOCK_UN);
}
fclose($fp);

$log = date("Y-m-d H:i:s") . ' - ' . $message . "\n" . $log;
file_put_contents('log.txt', $log, LOCK_EX);

5 秒間隔で log.txt を読み込み、表示しているだけなので負荷は高めです。
本来であれば JSON で日付や発言者等の細かいパラメータを含めて渡したほうがよいでしょう。

実用的なチャットにするためには、ログイン機能の実装と、負荷対策として Comet のような仕組みを取り入れる必要があると思います。