Catalystの練習 - じゃんけん大会を作ってみる(2)

Catalystの練習 - じゃんけん大会を作ってみるの続き

とりあえず、なんとなく形になったので、全部貼り付け。
相当ださくてヘボくておぞいかと思いますが、やってみていろいろ勉強になりました。
#ちなみにただコピーするだけでは、動かないわな(=w=;
 最初にデータベースにダミーデータをいれてやんなきゃなんない。
 セットアップ関数を作ればいいんだろうけど…。

テーブルは2個作る。
◆参加者データ保存用
Table: spr
id INTEGER
name VARCHAR(45) 参加者氏名
count INTEGER 何番目の
hand VARCHAR(45) 手
time INTEGER 大会番号

◆大会データ保存用
Table: competiton
id INTEGER
time INTEGER 第N回
name VARCHAR(45) 大会名
state ENUM('invited','closed','result') 開催状態
organizer VARCHAR(45) 主催者
appeal VARCHAR(512) アピール
num INTEGER 募集人数
password VARCHAR(45) 管理パスワード

テンプレートは4つ
◆index.tt

<html>
<head>
	<title>spr-tournament</title>
</head>
<body>
トーナメント表
[% system_message %]
<table border=1>
<tr><td>回</td><td>大会名</td><td>募集人数</td><td>状況</td><td>主催者</td><td>管理パスワード</td></tr>
[% FOREACH i IN compe_list %]
<tr>
	<td>[% i.time %]</td>
	<td>[% i.name %]</td>
	<td>[% i.num %]</td>
	<td>
		[% SWITCH i.state %]
		[% CASE 'invited' %]
			<form action="post" method="post">
			<input type="hidden" name="time" value="[% i.time %]">
			<input type="submit" value="募集中">
			</form>
		[% CASE 'closed' %]集計中
		[% CASE %]
			大会終了
			<form action="result" method="post">
			<input type="hidden" name="time" value="[% i.time %]">
			<input type="submit" value="結果発表">
			</form>
		[% END %]
	</td>
	<td>[% i.organizer %]</td>
	<td><form action="admin" method="post">
		<input type="hidden" name="time" value="[% i.time %]">
		<input type="password" name="password" size="30">
		<input type="submit" value="管理">
	</form></td>
</tr>
[% END %]
</table>

<br>
<a href="admin">大会の新規設置はこちら</a><br>
</body>
</html>

◆construct.tt

<html>
<head>
	<title>spr-tournament/construct</title>
</head>
<body>
<form action="admin" method="post">
            <input type="hidden"   name="time"                value="[% time %]">
大会名  : <input type="text"     name="name"      size="30" value="[% compename %]"> <br>
募集人数 :
[% IF num != '' %]
            [% num %] <br>
[% ELSE %]
            <input type="text"     name="num"       size="30" value="[% num %]"> <br>
[% END %]
主催者  : <input type="text"     name="organizer" size="30" value="[% organizer %]"> <br>
パスワード: <input type="password" name="password"  size="30"> <br>
[% IF state != '' %]
募集状況 : <select name="state">
		<option value=[% state %] selected>[% state %]
		<option value="closed">集計中
		<option value="result">結果表示
	    </select> <br>
[% END %]
アピール : <input type="text"     name="appeal"    size="30" value="[% appeal %]"> <br>
<input type="submit" value="登録">
</form>
<a href="./">トップページへもどる</a><br>

</body>
</html>

◆post.tt

<html>
<head>
	<title>spr-tournament/post</title>
</head>
<body>
第[% time %]回大会「[% compename %]」への参加申し込み
<form action="post" method="post">
<input type="hidden" name="time" value="[% time %]"> <br>
NAME: <input type="text" name="name" size="30"> <br>
[% count = 0 %]
[% WHILE count < 10 %]
HAND[% count %]:
<select name="hand[% count %]">
<option value="" selected>--選択してください--
<option value="r">グー
<option value="s">チョキ
<option value="p">パー
</select><br>
[% count = count + 1 %]
[% END %]
<input type="submit" value="登録">
</form>
<a href="./">トップページへもどる</a><br>

</body>
</html>

◆result.tt

<html>
<head>
	<title>spr-tournament</title>
</head>
<body>
トーナメント表

<table border=1>
<tr><td>回</td><td>大会名</td><td>募集人数</td><td>状況</td><td>主催者</td></tr>
[% FOREACH i IN compe_list %]
<tr>
	<td>[% i.time %]</td>
	<td>[% i.name %]</td>
	<td>[% i.num %]名</td>
	<td>
		[% SWITCH i.state %]
		[% CASE 'invited' %]募集中
		[% CASE 'closed' %]集計中
		[% CASE %]大会終了
		[% END %]
	</td>
	<td>[% i.organizer %]</td>
</tr>
[% END %]
</table>

[% tournament_graph %]
<br>
[% score %]
<a href="./">トップページへもどる</a><br>
</body>
</html>

あとはRoot.pmに下記のように書く。

sub default : Private {
	my ( $self, $c ) = @_;

	$c->forward('make_competition_list');	# 大会情報
	$c->forward('make_member_list');		# 登録者名簿をstashに格納
	$c->forward('make_hand_list');			# じゃんけんデータをstashに格納
#	$c->forward('tournament');				# 勝敗表の作成
	
	$c->stash->{template}	= 'index.tt';
	$c->forward('spr::View::TT');
	
}

sub begin : Private {
	my ( $self, $c ) = @_;
	
	$c->stash->{'judge'} = ({
		"ss" => 'draw',		# チョキxチョキ
		"sp" => 'player1',	# チョキxパー 
		"sr" => 'player2',	# チョキxグー 
		"ps" => 'player2',	# パー xチョキ
		"pp" => 'draw',		# パー xパー 
		"pr" => 'player1',	# パー xグー 
		"rs" => 'player1',	# グー xチョキ
		"rp" => 'player2',	# グー xパー 
		"rr" => 'draw'		# グー xグー 
	});
	$c->stash->{time}		= $c->req->param('time');
	
}

# 参加登録
sub post : Global {
	my ( $self, $c ) = @_;
	
	$c->forward('make_member_list');		# 登録者名簿をstashに格納
	if(	$c->req->param('name')	ne "" &&
		$c->req->param('hand0')	ne "" &&
		$c->req->param('time')	ne "" )
	{	# 登録情報があれば、大会参加情報の登録
		$c->forward('check_duplication');		# 登録者重複チェック
		my @data	= spr::Model::CDBI::Competition->search_like( time => $c->req->param('time') );
#		$c->stash->{'test'} = "test";
		if(	$data[0]->get('num') > $#{$c->stash->{'member'}} + 1 &&
			$c->stash->{dupe} ne "dupe"){
			# データベースに登録
			for(my $i = 0; $i < 10; $i++){
				last if ($c->req->param("hand$i") eq "");
				spr::Model::CDBI::Spr->insert({
					name	=> $c->req->param('name'),
					count	=> $i,
					hand	=> $c->req->param("hand$i"),
					time	=> $c->req->param('time')
				});
			}
			if ( $data[0]->get('num') == $#{$c->stash->{'member'}} + 2){	# なんか+2ってかっこ悪いな…
				$data[0]->set( state => 'closed');	# "募集〆切ました"を記録
			}
			$c->stash->{system_message} = '参加登録を受け付けました。';
		}
		else{
			# 登録できません
			$c->stash->{system_message} = '参加登録できませんでした。';
		}
		
		$c->stash->{template}	= 'index.tt';							# redirectすると、stashがリセットされる
		$c->forward('spr::View::TT');									# 
	}
	else{	# 登録情報がなければ、大会参加登録画面を表示
		my @times	= spr::Model::CDBI::Competition->search_like(
			time => $c->req->param('time')
		);
		$c->stash->{compename}	= $times[0]->get('name');
		$c->stash->{template}	= 'post.tt';
		$c->forward('spr::View::TT');
	}
}

# 重複登録のチェック
sub check_duplication : Private {
	my ( $self, $c ) = @_;
	
	$c->stash->{dupe} = '';
	foreach my $name ( @{$c->stash->{'member'}} ){
		if( $name eq $c->req->param('name') ){
			$c->stash->{dupe} = 'dupe';
		}
	}
}

# 大会情報一覧の作成
sub make_competition_list : Private {
	my ( $self, $c ) = @_;
	
	my @compe	= reverse spr::Model::CDBI::Competition->retrieve_all;
	$c->stash->{'compe_list'} = \@compe;
}

# 関数長くなってしまった…
sub admin : Global {
	my ( $self, $c ) = @_;
	my $auth;
	my @data;

	if( $c->req->param('time') ne "" ){	# 第○回の情報がきてれば、開催告知済みの大会の情報更新とみなす
		@data	= spr::Model::CDBI::Competition->search( time => $c->req->param('time') );
		# 主催者認証
		$auth	= ( $data[0]->get('password') eq crypt( $c->req->param('password'), $data[0]->get('password') ) ) ? "OK" : "NG";
		if( $auth eq "OK" ){
			if(	$c->req->param('name')		ne "" &&
				$c->req->param('organizer')	ne "" &&
				$c->req->param('state')		ne "" )
			{		# 変更内容があれば、大会情報の変更登録
				$data[0]->set( name			=> $c->req->param('name') );
				$data[0]->set( organizer	=> $c->req->param('organizer') );
				$data[0]->set( appeal		=> $c->req->param('appeal') );
				$data[0]->set( state		=> $c->req->param('state') );
				
				$c->stash->{template}	= 'index.tt';
				$c->forward('spr::View::TT');
			}
			else{	# 変更内容がなければ、主催者設定画面を表示
				$c->stash->{time}		= $data[0]->get('time');
				$c->stash->{compename}	= $data[0]->get('name');
				$c->stash->{num}		= $data[0]->get('num');
				$c->stash->{organizer}	= $data[0]->get('organizer');
				$c->stash->{appeal}		= $data[0]->get('appeal');
				$c->stash->{state}		= $data[0]->get('state');
				$c->stash->{template}	= 'construct.tt';
				$c->forward('spr::View::TT');
			}
		}
		else{	#
				$c->stash->{system_message} = '主催者認証に失敗しました。';
				$c->stash->{template}	= 'index.tt';
				$c->forward('spr::View::TT');
		}
	}
	else{	# 新規大会の登録
		if(	$c->req->param('name')		ne "" &&
			$c->req->param('num')		ne "" &&
			$c->req->param('organizer')	ne "" &&
			$c->req->param('appeal')	ne "" &&
			$c->req->param('password')	ne ""	)
		{		# 登録情報があれば、大会情報の登録
			# 大会番号の取得(DB的にピンポイントで取れそうな気がするけど、やり方がわからなかったので強引に)
			my @times	= spr::Model::CDBI::Competition->retrieve_from_sql(qq{
				num > 0
				ORDER BY time DESC
			});
			my $time	= $times[0]->get('time') + 1;
			
			# データベースに登録
			spr::Model::CDBI::Competition->insert({
				name		=> $c->req->param('name'),
				num			=> $c->req->param('num'),
				organizer	=> $c->req->param('organizer'),
				appeal		=> $c->req->param('appeal'),
				password	=> crypt( $c->req->param('password'), "ab" ),
				state		=> 'invited',
				time		=> $time
			});
			$c->res->redirect('/');
		}
		else{	# 登録情報がなければ、大会設定登録画面を表示
			$c->stash->{template}	= 'construct.tt';
			$c->forward('spr::View::TT');
		}
	}
}

# 大会結果の表示
sub result : Global {
	my ( $self, $c ) = @_;
	$c->stash->{time}	= $c->req->param('time');
	my @competition_data	= spr::Model::CDBI::Competition->search_like( time => $c->stash->{time} );
	
	if ( $competition_data[0]->get('state') eq "result" ){
		# 大会結果発表
		$c->forward('make_member_list');		# 登録者名簿をstashに格納
		$c->forward('make_hand_list');			# じゃんけんデータをstashに格納
		$c->forward('tournament');				# 勝敗表の作成
		$c->stash->{'compe_list'} = \@competition_data;
	}
	elsif ( $competition_data[0]->get('state') eq "invited" ){
		# 参加者受付中
		$c->stash->{tournament_graph} =<<EOM;
現在、参加者受付中です。
<form action="post" method="post">
<input type="hidden" name="time" value="$c->stash->{time}">
<input type="submit" value="参加登録画面へ">
</form>
EOM
	}
	elsif ( $competition_data[0]->get('state') eq "closed" ){
		# 受付〆切ました
		$c->stash->{tournament_graph} =<<EOM;
現在、大会結果集計中です。
EOM
	}
	else{
		# 大会データが存在しません
		$c->stash->{tournament_graph} =<<EOM;
大会データが存在しません。
EOM
	}
	$c->stash->{template}	= 'result.tt';
	$c->forward('spr::View::TT');
}

sub match : Private {
	my ( $self, $c ) = @_;
	my $i;					# 
	my $head;
	my $count1 = 0;
	my $count2 = 0;
	my $player1 = $c->stash->{'player1'};
	my $player2 = $c->stash->{'player2'};
	my ($hand1, $hand2, $index);	# 勝敗表作成用テンポラリ
	my $match = ++$c->stash->{'num_of_match'};
	
	$c->stash->{'winner'} = $player1;			# 最後まであいこなら、player1勝利
	
	$c->stash->{'score'} .= "<a name=\"$match\">第$match試合</a> <a href=\"\#\">↑topへ</a><table border=1 cellspacing=0 cellpadding=0>\n";	# 勝敗表の作成
	$head	= "<tr><td>名前</td>";
	$hand1	= "<tr><td>$player1</td>";
	$hand2	= "<tr><td>$player2</td>";
	for ($i = 1; $i < 11; $i++){
		my $a = $c->stash->{'hand'}->{"$player1"}->["$count1"];
		my $b = $c->stash->{'hand'}->{"$player2"}->["$count2"];
		$head  .= "<td>$i</td>";
		$hand1 .= "<td>$count1:$a</td>";
		$hand2 .= "<td>$count2:$b</td>";
		if ($a ne $b){
			$c->stash->{'winner'} = ($c->stash->{'judge'}->{"$a$b"} eq 'player1') ? $player1 : $player2;
			last;
		}
		$count1 = ($count1 < $#{$c->stash->{'hand'}->{"$player1"}}) ? $count1+1 : 0;	# 次の手がなければ、先頭に戻る(Player1)
		$count2 = ($count2 < $#{$c->stash->{'hand'}->{"$player2"}}) ? $count2+1 : 0;	# 次の手がなければ、先頭に戻る(Player2)
	}
	$head  .= "</tr>\n";
	$hand1 .= "</tr>\n";
	$hand2 .= "</tr>\n";
	$c->stash->{'score'} .= $head . $hand1 . $hand2 ."</tr></table><br>\n";
}

# ここまできたら、(データベースからstashに読み込んでおく)関数でくくれる気がする
sub make_hand_list :Private {	# データベースからstashに読み込んでおく
	my ( $self, $c ) = @_;
	my @hands;
	my $name;
	my @names_hand;
	my ($i, $j);	# ループカウンタ
	my %hand_list;	# { 名前番号 => 出す手 }
	
	# 登録者名簿の名前の数だけループを回す
	for ($j = 0; $j < $#{$c->stash->{'member'}} + 1; $j++){
		my @names_hand = ();
		$name = $c->stash->{'member'}[$j];
		@hands = spr::Model::CDBI::Spr->search_like(name => $name, time => $c->stash->{time}, { order_by => 'count'});
		for($i = 0; $i < $#hands + 1; $i++){
			$names_hand[$i] = $hands[$i]->get('hand');
		}
		$hand_list{"$name"} = [ @names_hand ];
	}
	$c->stash->{'hand'} = \%hand_list;
}

sub make_member_list : Private {	# データベースからstashに読み込んでおく
	my ( $self, $c ) = @_;
	my $name;
	my $i = 0;		# ループカウンタ
	my %name_to_num;
	my @member;
	
	my $it = spr::Model::CDBI::Spr->search_like( count => 0, time => $c->stash->{time} );	# イテレータのit
	while(my $member = $it->next){
		$member[$i] = $member->name;
		$i++;
	}
	for($i = 0; $i < $#member + 1; $i++){
		$name_to_num{$member[$i]} = $i;
	}
	$c->stash->{'member'}		= \@member;			# 登録者名簿( 名前 )のリスト
	$c->stash->{'name_to_num'}	= \%name_to_num;	# 登録者名簿{ 名前 => 番号 }のハッシュ
}

sub tournament : Private {	# トーナメント表の作成
	my ( $self, $c ) = @_;
	my $max = 2;
	my $num_of_members = $#{$c->stash->{'member'}} + 1;
	my $lvl;
	my ($i, $j);	# ループカウンタ
	my $n;
	my $p;
	my $wi;
	my $wn;
	my @cell;
	my $player1;
	my $player2;
	my $match;
	
	for( $lvl=1; $max <= $num_of_members; $lvl++){ $max *= 2; }		# 参加人数をこえる最小の2^nを求める
	my $seads		= $max - $num_of_members;						# シード選手の人数 = (参加人数をこえる最小の2^n) - 参加人数
	my $halfseads	= int( $seads / 2 );							# シード数の1/2
	my $restseads	= $num_of_members - ( $seads - $halfseads );	# 残りのシード数
	
	# セルの初期化
	for( $j = 1; $j <= $lvl * 2; $j++ ){
		for( $i = 0; $i < $num_of_members * 2; $i++ ){
			$cell[$j][$i] = "";
		}
	}
	
	$p = 0;
	
	# 表示内容を配列@cellに格納する
	for( $i = 0; $i < $num_of_members; $i++ ){			# Level 1の表示内容決定(初戦のみシード枠判定がある)
		$wi	= $i + 1;
		if( $i < $halfseads || $i >= $restseads){
			$cell[1][$i*2] = "─";						# 参加者が2^nの場合、全員初戦シード扱いになる
			$cell[2][$i*2] = $c->stash->{'member'}[$i];
		}
		else{
			if( $p == 0 ){								# 対戦アーチ描画中でなければ
				$cell[1][$i*2]			= "┐";			# 
				$p						= 1;			# 対戦アーチ描画中フラグON
			}
			else{
				$cell[1][$i*2]			= "┘";			# 
				$cell[1][$i*2-1]		= "├";			# 
				$c->stash->{'player1'}	= $c->stash->{'member'}[$i-1];	# 
				$c->stash->{'player2'}	= $c->stash->{'member'}[$i];	# 
				$c->forward('match');					# 「じゃんけんぽんっ」
				$cell[2][$i*2-1]		= $c->stash->{'winner'};
				$match					= $c->stash->{'num_of_match'};
				$cell[1][$i*2-1]		.= "<a href=\"\#$match\">" . $match . "</a>";	# 
				$p						= 0;			# 対戦アーチ描画中フラグOFF
			}
		}
	}
	for( $j = 3; $j <= $lvl * 2; $j = $j+2 ){			# Level 2以上の表示内容決定
		$p = 0;
		for( $i = 0; $i < $num_of_members * 2; $i++ ){
			if( $cell[$j-1][$i] ne "" ){				# 一段下のセルが空白でなければ描画開始
				if( $p == 0 ){							# 対戦アーチ描画中でなければ
					$cell[$j][$i]		= "┐";			# 
					$c->stash->{'player1'}	= $cell[$j-1][$i];
					$p					= 1;			# 対戦アーチ描画中フラグON
					$n					= 1;			# 対戦アーチの長さを測定開始
				}
				else{
					$cell[$j][$i]		= "┘";			# 
					$c->stash->{'player2'}	= $cell[$j-1][$i];
					$wn					= int( $n / 2 );	# 対戦アーチ描画終了時に、真ん中を"├"に変更
					$cell[$j][$i-$wn]	= "├";			# 
					$c->forward('match');				# 「じゃんけんぽんっ」
					$cell[$j+1][$i-$wn]	= $c->stash->{'winner'};
					$match				= $c->stash->{'num_of_match'};
					$cell[$j][$i-$wn]	.= "<a href=\"\#$match\">" . $match . "</a>";	# 
					$p					= 0;			# 対戦アーチ描画中フラグOFF
				}
			}
			elsif( $p == 1 ){
				$cell[$j][$i]			= "│";			# 
				$n++;									# 対戦アーチの長さを測定する
			}
		}
	}
	
	# 表示内容をstash->{'tournament_graph'}に格納する
	$c->stash->{'tournament_graph'} = "<table border=0 cellspacing=0 cellpadding=0>\n";
	for( $i = 0; $i < $num_of_members * 2; $i++){		# 縦幅(セル数)は、参加者*2マス
		$c->stash->{'tournament_graph'} .= "<TR>";
		for( $j = 0; $j <= $lvl * 2; $j++){				# 最下位Levelから出力
			$c->stash->{'tournament_graph'} .= "<TD>";
			if( $j == 0 ){								# Level 0は参加者表示欄
				if( $i % 2 == 0 ){						# 偶数欄に参加者No.を表示
					$wi = int( $i / 2 );				# 
					my $name = $c->stash->{'member'}[$wi];
					$c->stash->{'tournament_graph'} .= $c->stash->{'member'}[$wi];
				}
			}
			else{										# Level 1以上
				if($cell[$j][$i] ne ""){$c->stash->{'tournament_graph'} .= $cell[$j][$i];}
														# その他セルは$cell[$i][$j]=0のため、何も出力しない(空白セル)
			}
			$c->stash->{'tournament_graph'} .= "</TD>\n";
		}
		$c->stash->{'tournament_graph'} .= "</TR>";
	}
	$c->stash->{'tournament_graph'} .= "</table>";
}