【Java:的当てゲーム】ランキングデータの保存機能をCSVで実装

2022年4月8日

前回までの記事でゲーム自体は、ほぼ完成の状態まできました。

前の記事 的当てゲーム制作Top 次の記事

本稿ではランキング形式のデータ保存機能を実装していきたいと思います。

保存形式の選択

まず最初に決めていきたいのはデータの保存形式です。この辺は最初に仕様の時点では何も考えておりませんでしたので、ここで考えていきます。

保存形式はいろいろ考えられますがライブラリに実装済の保存形式を使用したいと思います。

ライブラリのGTKFileUtilクラスには下記のような保存形式を用意しています。
文字列形式
バイナリ形式
クラス形式(オブジェクトシリアライズ)
linuxの設定ファイルに類似するプロパティ形式
・CSV形式

これに+で上記の暗号化形式があります。

この中で最も簡単に実装できる形式は間違いなくクラス形式なのですが、今回は、まだ記事にしたことがないCSV形式を利用することにします。(ライブラリの関数呼ぶだけなので実際のプログラムは作りません)

CSV形式は「,」区切りでデータ分割し改行ごとに1組のデータとして扱う文字列形式です。非常に単純な形式であるため、外部とのシステム連携が容易となります。

EXCELで開くとこんな感じのやつ

業務用としても使用される形式なんじゃないかなぁと思います。自力での実装もsplit関数で容易に可能です。(・・・とは言いましたがCSV形式を本気で対応しようとするとそこそこ面倒くさいです。)

データの中身が単純であるため、人間でも容易に保存内容を書き換えられます。

スコアを書き換えられるのはよろしくないので暗号化したほうが良いかもしれませんがサイトで公開するソースでは暗号化しません。データの中身をわかりやすくするためです。ただしgitにアップするほうは暗号化を実装しておくことにします。


今回のゲームにはランキングに登録する名前を入力してもらう方式にするのですが、前回入力した内容も保持したいのでこれもファイル化できるようにしておきます。

この用途の場合に簡単なのは間違いなく文字列形式の保存です。ですが、あえてプロパティ形式を利用することにします。

プロパティ形式は以前記事にしました。

プロパティ形式とはlinuxでの設定ファイルなどでよく利用される形式です。「key=value」の形式で保持し、プログラム側でもhashmapのようにkey,valueでアクセス可能です。

このファイルも人間が容易に修正できる形式なのですが、このファイルは修正されても多分困らないので暗号化はしないこととします。

クラスの定義

ランキングデータを保持するクラスを定義しておきます。

データファイルの入出力はGameFileManager.javaとして定義します。

保存したいデータは[No,名前,得点,ランク,日時]を一組とし、それのハイスコアの上位10組分です。

ということで最初に一組分のデータセットを保持するDataInfoクラスをGameFileManagerクラスの内部クラスとして定義します。

ファイルはCSV形式ですので、すべて文字列で保持することにしました。

ただし、スコアのみはデータの並び替えで使用したいので、数値に変換して取得するメソッドなども用意しておきます。(もともとint型にしておいても良いです。むしろそっちのがいいかも・・・)

public class GameFileManager {
	//ランキングの1行分のデータを保持できるクラス
	public static class DataInfo{
		public String no;
		public String score;
		public String name;
		public String datetime;
		public String rank;
		public int getScore() {
			try {
				return Integer.parseInt(score);
			} catch (Exception e) {
				return 0;
			}
		}
		public void setScore(int score) {
			this.score = ""+score;
		}
	}
}

次にファイルのロード処理と保存処理を実装します。

ロード処理はstaticコンテキストでロードします。こうしておけば利用したいときに必ずロードされるようになるはずです。

	private static Path path = Paths.get("data/data.dat");
	private static Path confPath = Paths.get("data/conf.dat");
	private static Properties prop = new Properties();
	private static ArrayList dataList = new ArrayList<>();
	static {
		//ファイルロード
		if ( Files.exists(confPath) ) {
			prop = GTKFileUtil.loadProperty(confPath.toFile());
		}

		//ランキングファイルロード
		if ( Files.exists(path) ) {
			String[][] data = GTKFileUtil.loadCSV(path.toFile());
			for ( int i = 0;i < 10;i++ ) {
				String[] d = data == null || data.length <= i ? null : data[i];
				DataInfo info = new DataInfo();
				if ( d != null ) {
					info.no = (i+1)+"";
					info.name = d[0];
					info.score = d[1];
					info.rank = d[2];
					info.datetime = d[3];
					dataList.add(info);
				} else {
					String fill = "";
					info.no = (i+1)+"";
					info.name = fill;
					info.score = fill;
					info.rank = fill;
					info.datetime = fill;
					dataList.add(info);
				}
			}
		} else {
			for ( int i = 0;i < 10;i++ ) {
				DataInfo info = new DataInfo();
				String fill = "";
				info.no = (i+1)+"";
				info.name = fill;
				info.score = fill;
				info.rank = fill;
				info.datetime = fill;
				dataList.add(info);
			}
		}
	}

明らかに短縮できそうで冗長な部分が存在しますが、面倒なので、このままでいきます。

次に保存メソッドを用意します。

簡単な名前の保存はPropertiesにデータを入れてsavePropertyメソッドを呼び出して終了ですが、ランキングはちょっと面倒です。

引数はDataInfoを渡してもらうことにし、このデータをArrayListに追加します。

そのあとCollections.sortと二次元配列化を行いsave関数を呼び出すことにしました。ソートは下記の記事でやりました。

また、ランキング保存すべきかどうかを取得するaddokメソッドも作成します。

これらを実装した最終版が下記のソースになります。

import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Properties;
import java.util.stream.Collectors;

import com.nompor.gtk.file.GTKFileUtil;

//ゲームのファイルの入出力メソッド群
//GTKFileUtilを使用し、ファイルの入出力を行う
//書き出し処理は自動でフォルダが作成されるため自力でフォルダ作成する必要はない
public class GameFileManager {
	private static Path path = Paths.get("data/data.dat");
	private static Path confPath = Paths.get("data/conf.dat");
	private static Properties prop = new Properties();
	private static ArrayList dataList = new ArrayList<>();
	static {
		//ファイルロード
		if ( Files.exists(confPath) ) {
			prop = GTKFileUtil.loadProperty(confPath.toFile());
		}

		//ランキングファイルロード
		if ( Files.exists(path) ) {
			String[][] data = GTKFileUtil.loadCSV(path.toFile());
			for ( int i = 0;i < 10;i++ ) {
				String[] d = data == null || data.length <= i ? null : data[i];
				DataInfo info = new DataInfo();
				if ( d != null ) {
					info.no = (i+1)+"";
					info.name = d[0];
					info.score = d[1];
					info.rank = d[2];
					info.datetime = d[3];
					dataList.add(info);
				} else {
					String fill = "";
					info.no = (i+1)+"";
					info.name = fill;
					info.score = fill;
					info.rank = fill;
					info.datetime = fill;
					dataList.add(info);
				}
			}
		} else {
			for ( int i = 0;i < 10;i++ ) {
				DataInfo info = new DataInfo();
				String fill = "";
				info.no = (i+1)+"";
				info.name = fill;
				info.score = fill;
				info.rank = fill;
				info.datetime = fill;
				dataList.add(info);
			}
		}
	}
	//ランキングデータの取得
	public static ArrayList get(){
		return dataList;
	}
	public static void saveInfo(DataInfo info){
		//追加後にセーブする
		dataList.add(info);

		//スコアの降順にソート
		Collections.sort(dataList, (e1,e2)->e2.getScore()-e1.getScore());
		for ( int i = 0;i < dataList.size();i++ ) {
			dataList.get(i).no = i+1+"";
		}

		//全ランキングデータを二次元配列化
		String[][] result = dataList.stream().map(e ->{
			String[] s = new String[4];
			s[0] = e.name;
			s[1] = e.score;
			s[2] = e.rank;
			s[3] = e.datetime;
			return s;
		}).collect(Collectors.toList()).toArray(new String[0][]);

		//CSVファイルの書き出し
		GTKFileUtil.saveCSV(path.toFile(), result);
		while ( dataList.size() > 10 ) {
			dataList.remove(dataList.size()-1);
		}
	}
	//ランキングに追加できるかどうかを判定します。
	public static boolean addok(int score){
		if ( dataList.size() <= 0 ) {
			return true;
		}
		return dataList.get(dataList.size()-1).getScore() < score;
	}
	//名前を取得
	public static String getLastName() {
		return prop.getProperty("name");
	}
	//名前を保存
	public static void saveName(String name) {
		prop.setProperty("name", name);
		GTKFileUtil.saveProperty(confPath.toFile(), prop);
	}

	//ランキングの1行分のデータを保持できるクラス
	public static class DataInfo{
		public String no;
		public String score;
		public String name;
		public String datetime;
		public String rank;
		public int getScore() {
			try {
				return Integer.parseInt(score);
			} catch (Exception e) {
				return 0;
			}
		}
		public void setScore(int score) {
			this.score = ""+score;
		}
	}
}

改めて処理を見返してみるとバグりそうな処理がありますね。バグりはしなかったのであえて修正はしませんがw

そこそこプログラミングやってる方は言ってる意味がわかると思います。こんなソース貼り付けて恥ずかしくないのかと思われてしまうかもしれませんねw

それでは、すこしセーブデータの出力をテストしてみましょう。

import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;


public class Test{
	public static void main(String[] args) {
		for ( int i = 0;i < 10;i++ ) {
			GameFileManager.DataInfo info = new GameFileManager.DataInfo();
			info.no=i+1+"";
			info.name="Yahoo"+i;
			info.rank="SSS"+i;
			info.score=""+i*100;
			info.datetime=LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy/MM/dd"));
			GameFileManager.saveInfo(info);
		}
	}
}
実行結果

うまく処理できているようです。使用しているライブラリのクラス自体は無駄に頑張ってカンマを値に入れ込めるようにしているので良かったら使ってあげてくださいね。

これでGameFileManagerのプログラムは実装完了となります。いつも通りgitに上げときます。

ちなみにランキングデータを暗号化したい場合はsaveCryptCSV、loadCryptCSVを利用すると暗号化できます。

暗号化など気休め程度ですが、平文での保存よりは多少マシでしょう。

Java

Posted by nompor