- Implemented multiple users, including a selection dialog and automatic preference saving and loading.
- Integrated the ANN with the game. The network now predicts a user move, completely ignores it, and trains itself on the players actual move. This integration also included implementing two new functions. The first translates a board state to a boolean array to correspond with input nodes. The second translates a move to a boolean array to correspond with output nodes.
This commit is contained in:
@@ -30,19 +30,38 @@ public class Referee implements Runnable {
|
|||||||
private final MainFrame mf;
|
private final MainFrame mf;
|
||||||
private PlayerModel playerModel = null;
|
private PlayerModel playerModel = null;
|
||||||
|
|
||||||
public Referee(MainFrame mnFrm) {
|
public Referee(MainFrame mnFrm, String player) {
|
||||||
if (PlayerModel.TRY_LOAD && PlayerModel.exists()) {
|
if (PlayerModel.exists(player)) {
|
||||||
playerModel = PlayerModel.load();
|
PlayerModel.getPlayerPath(player);
|
||||||
|
playerModel = PlayerModel.load(PlayerModel.getPlayerPath(player));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (playerModel == null) {
|
if (getPlayerModel() == null) {
|
||||||
playerModel = new PlayerModel();
|
playerModel = new PlayerModel(player);
|
||||||
}
|
}
|
||||||
|
|
||||||
mf = mnFrm;
|
mf = mnFrm;
|
||||||
initGame();
|
initGame();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean[] getBoardState() {
|
||||||
|
boolean[] boardState = new boolean[getPlayerModel().getNumInputNodes()];
|
||||||
|
|
||||||
|
int i = 0;
|
||||||
|
for (int r = 0; r < Board.NUM_ROWS; r++) {
|
||||||
|
for (int c = 0; c < Board.NUM_COLS; c++) {
|
||||||
|
boardState[i] = (board.getTile(r, c) == TileColor.BLUE);
|
||||||
|
boardState[i + 1] = (board.getTile(r, c) == TileColor.GREEN);
|
||||||
|
boardState[i + 2] = (board.getTile(r, c) == TileColor.RED);
|
||||||
|
boardState[i + 3] = (board.getTile(r, c) == TileColor.YELLOW);
|
||||||
|
|
||||||
|
i += 4;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return boardState;
|
||||||
|
}
|
||||||
|
|
||||||
public Player getComputerPlayer() {
|
public Player getComputerPlayer() {
|
||||||
return computerPlayer;
|
return computerPlayer;
|
||||||
}
|
}
|
||||||
@@ -83,13 +102,13 @@ public class Referee implements Runnable {
|
|||||||
initGame();
|
initGame();
|
||||||
mf.updateBoard();
|
mf.updateBoard();
|
||||||
play();
|
play();
|
||||||
playerModel.logGame(getPlayerScore());
|
getPlayerModel().logGame(getPlayerScore());
|
||||||
|
|
||||||
if (!playerModel.save()) {
|
if (!getPlayerModel().save()) {
|
||||||
System.err.println("Saving PlayerModel failed.");
|
System.err.println("Saving PlayerModel failed.");
|
||||||
}
|
}
|
||||||
|
|
||||||
new HighScoreDialog(mf, playerModel);
|
new HighScoreDialog(mf, getPlayerModel());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -97,6 +116,28 @@ public class Referee implements Runnable {
|
|||||||
this.boardPanel = boardPanel;
|
this.boardPanel = boardPanel;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private boolean[] getMoveArray(Move mv) {
|
||||||
|
boolean[] move = new boolean[getPlayerModel().getNumOutputNodes()];
|
||||||
|
|
||||||
|
move[0] = (mv.getColor() == TileColor.BLUE);
|
||||||
|
move[1] = (mv.getColor() == TileColor.GREEN);
|
||||||
|
move[2] = (mv.getColor() == TileColor.RED);
|
||||||
|
move[3] = (mv.getColor() == TileColor.YELLOW);
|
||||||
|
|
||||||
|
int tile = 0;
|
||||||
|
for (int r = 0; r < Board.NUM_ROWS; r++) {
|
||||||
|
for (int c = 0; c < Board.NUM_COLS; c++) {
|
||||||
|
move[tile] = (mv.getCell().r == r && mv.getCell().c == c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return move;
|
||||||
|
}
|
||||||
|
|
||||||
|
private PlayerModel getPlayerModel() {
|
||||||
|
return playerModel;
|
||||||
|
}
|
||||||
|
|
||||||
private ScorePanel getScorePanel() {
|
private ScorePanel getScorePanel() {
|
||||||
return mf.getScorePanel();
|
return mf.getScorePanel();
|
||||||
}
|
}
|
||||||
@@ -126,6 +167,9 @@ public class Referee implements Runnable {
|
|||||||
Move mv = humanPlayer.getMove(board);
|
Move mv = humanPlayer.getMove(board);
|
||||||
if (board.getTile(mv.getCell().r, mv.getCell().c) == TileColor.NONE) {
|
if (board.getTile(mv.getCell().r, mv.getCell().c) == TileColor.NONE) {
|
||||||
playToken(humanPlayer.getMove(board));
|
playToken(humanPlayer.getMove(board));
|
||||||
|
|
||||||
|
getPlayerModel().train(getMoveArray(mv));
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
humanPlayer.denyMove();
|
humanPlayer.denyMove();
|
||||||
}
|
}
|
||||||
@@ -133,6 +177,13 @@ public class Referee implements Runnable {
|
|||||||
} else {
|
} else {
|
||||||
Move mv = computerPlayer.getMove(board);
|
Move mv = computerPlayer.getMove(board);
|
||||||
playToken(mv);
|
playToken(mv);
|
||||||
|
|
||||||
|
// TODO
|
||||||
|
// This is the call that gets a prediction of a user's move.
|
||||||
|
// Some changes will probably be necessary to put it in the
|
||||||
|
// right place and also to get the node weights. But... all in
|
||||||
|
// due time.
|
||||||
|
getPlayerModel().getPrediction(getBoardState());
|
||||||
}
|
}
|
||||||
|
|
||||||
mf.updateMessage(getMessage());
|
mf.updateMessage(getMessage());
|
||||||
|
|||||||
@@ -17,30 +17,27 @@ import model.playerModel.node.SigmoidNode;
|
|||||||
|
|
||||||
public class PlayerModel implements Serializable {
|
public class PlayerModel implements Serializable {
|
||||||
|
|
||||||
public static final String PLAYER_MODEL_PATH = "playerModel.dat"; // Path to
|
public static final String DATA_FOLDER = "data/";
|
||||||
// the
|
public static final String PLAYER_MODEL_PREFIX = "playerModel";
|
||||||
// stored
|
public static final String PLAYER_MODEL_SUFFIX = ".dat";
|
||||||
// player
|
|
||||||
// model.
|
|
||||||
public static final Random rand = new Random(); // Randomizer object.
|
public static final Random rand = new Random(); // Randomizer object.
|
||||||
public static final boolean TRY_LOAD = true; // Set to false if any existing
|
|
||||||
// player model should be
|
|
||||||
// discarded and the next
|
|
||||||
// game should begin a new
|
|
||||||
// sequence.
|
|
||||||
|
|
||||||
private static final long serialVersionUID = 1L;
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
public static boolean exists() {
|
public static boolean exists(String playerName) {
|
||||||
return (new File(PLAYER_MODEL_PATH)).exists();
|
return (new File(getPlayerPath(playerName))).exists();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static PlayerModel load() {
|
public static String getPlayerPath(String playerName) {
|
||||||
|
return DATA_FOLDER + PLAYER_MODEL_PREFIX + "_" + playerName
|
||||||
|
+ PLAYER_MODEL_SUFFIX;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static PlayerModel load(String path) {
|
||||||
FileInputStream fin = null;
|
FileInputStream fin = null;
|
||||||
ObjectInputStream oin = null;
|
ObjectInputStream oin = null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
fin = new FileInputStream(PLAYER_MODEL_PATH);
|
fin = new FileInputStream(path);
|
||||||
oin = new ObjectInputStream(fin);
|
oin = new ObjectInputStream(fin);
|
||||||
PlayerModel pm = (PlayerModel) oin.readObject();
|
PlayerModel pm = (PlayerModel) oin.readObject();
|
||||||
oin.close();
|
oin.close();
|
||||||
@@ -54,29 +51,31 @@ public class PlayerModel implements Serializable {
|
|||||||
private final SigmoidNode[] hiddenLayer;
|
private final SigmoidNode[] hiddenLayer;
|
||||||
private int highScoresAchieved = 0;
|
private int highScoresAchieved = 0;
|
||||||
|
|
||||||
// One node for each tile-color combination, plus one for each upcoming
|
// One node for each tile-color combination.
|
||||||
// tile-color combination.
|
|
||||||
private final InputNode[] inputNode = new InputNode[(Board.NUM_COLS
|
private final InputNode[] inputNode = new InputNode[(Board.NUM_COLS
|
||||||
* Board.NUM_ROWS * (Board.TileColor.values().length - 1))];
|
* Board.NUM_ROWS * (Board.TileColor.values().length - 1))];
|
||||||
|
|
||||||
|
private final String name;
|
||||||
|
|
||||||
private int nextHighInGames = 0;
|
private int nextHighInGames = 0;
|
||||||
|
|
||||||
// One node for each tile plus four for the colors to be selected.
|
/*
|
||||||
// outputNode[0] is blue.
|
* One node for each tile plus four for the colors to be selected.
|
||||||
// outputNode[1] is green.
|
* outputNode[0] is blue. outputNode[1] is green. outputNode[2] is red.
|
||||||
// outputNode[2] is red.
|
* outputNode[3] is yellow. outputNode[4] through outputNode[n] represent
|
||||||
// outputNode[3] is yellow.
|
* grid spaces. A true means that the player is predicted to place on that
|
||||||
// outputNode[4] through outputNode[n] represent grid spaces. A true means
|
* tile. They should be read from the top-left to bottom-right, across rows.
|
||||||
// that the player is predicted to place on that tile.
|
* Ideally, the network should return only one true between 0 and 3 and only
|
||||||
// They should be read from the top-left to bottom-right, across rows.
|
* one true between 4 and n, representing one color and the tile in which it
|
||||||
// Ideally, the network should return only one true between 0 and 3 and only
|
* should be placed. See Referee.getMoveArray().
|
||||||
// one true between 4 and n, representing one color and the tile in which it
|
*/
|
||||||
// should be placed.
|
|
||||||
private final SigmoidNode[] outputNode = new SigmoidNode[(Board.NUM_COLS * Board.NUM_ROWS) + 4];
|
private final SigmoidNode[] outputNode = new SigmoidNode[(Board.NUM_COLS * Board.NUM_ROWS) + 4];
|
||||||
|
|
||||||
private final ArrayList<GameLog> scores = new ArrayList<GameLog>();
|
private final ArrayList<GameLog> scores = new ArrayList<GameLog>();
|
||||||
|
|
||||||
public PlayerModel() {
|
public PlayerModel(String nme) {
|
||||||
|
name = nme;
|
||||||
|
|
||||||
hiddenLayer = new SigmoidNode[inputNode.length
|
hiddenLayer = new SigmoidNode[inputNode.length
|
||||||
+ ((inputNode.length * 2) / 3)];
|
+ ((inputNode.length * 2) / 3)];
|
||||||
|
|
||||||
@@ -121,6 +120,18 @@ public class PlayerModel implements Serializable {
|
|||||||
return highScores;
|
return highScores;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getName() {
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getNumInputNodes() {
|
||||||
|
return inputNode.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getNumOutputNodes() {
|
||||||
|
return outputNode.length;
|
||||||
|
}
|
||||||
|
|
||||||
public boolean[] getPrediction(boolean[] input) {
|
public boolean[] getPrediction(boolean[] input) {
|
||||||
if (input.length == inputNode.length) {
|
if (input.length == inputNode.length) {
|
||||||
boolean[] prediction = new boolean[outputNode.length];
|
boolean[] prediction = new boolean[outputNode.length];
|
||||||
@@ -187,13 +198,17 @@ public class PlayerModel implements Serializable {
|
|||||||
public boolean save() {
|
public boolean save() {
|
||||||
FileOutputStream fout = null;
|
FileOutputStream fout = null;
|
||||||
ObjectOutputStream oout = null;
|
ObjectOutputStream oout = null;
|
||||||
|
String path = getPlayerPath(getName());
|
||||||
try {
|
try {
|
||||||
fout = new FileOutputStream(PLAYER_MODEL_PATH);
|
(new File(DATA_FOLDER)).mkdirs();
|
||||||
|
|
||||||
|
fout = new FileOutputStream(path);
|
||||||
oout = new ObjectOutputStream(fout);
|
oout = new ObjectOutputStream(fout);
|
||||||
oout.writeObject(this);
|
oout.writeObject(this);
|
||||||
oout.close();
|
oout.close();
|
||||||
return true;
|
return true;
|
||||||
} catch (IOException ex) {
|
} catch (IOException ex) {
|
||||||
|
ex.printStackTrace();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,28 +20,6 @@ public class HighScoreDialog extends JDialog {
|
|||||||
*/
|
*/
|
||||||
private static final long serialVersionUID = 1L;
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
public static void main(String[] args) {
|
|
||||||
JFrame x = new JFrame();
|
|
||||||
x.setDefaultCloseOperation(EXIT_ON_CLOSE);
|
|
||||||
x.setVisible(true);
|
|
||||||
|
|
||||||
PlayerModel pm = new PlayerModel();
|
|
||||||
pm.logGame(100);
|
|
||||||
pm.logGame(90);
|
|
||||||
pm.logGame(80);
|
|
||||||
pm.logGame(70);
|
|
||||||
pm.logGame(60);
|
|
||||||
pm.logGame(50);
|
|
||||||
pm.logGame(40);
|
|
||||||
pm.logGame(30);
|
|
||||||
pm.logGame(20);
|
|
||||||
pm.logGame(10);
|
|
||||||
pm.logGame(98);
|
|
||||||
pm.logGame(105);
|
|
||||||
|
|
||||||
new HighScoreDialog(x, pm);
|
|
||||||
}
|
|
||||||
|
|
||||||
public HighScoreDialog(JFrame owner, PlayerModel pm) {
|
public HighScoreDialog(JFrame owner, PlayerModel pm) {
|
||||||
super(owner, "Game over!", true);
|
super(owner, "Game over!", true);
|
||||||
|
|
||||||
|
|||||||
@@ -14,8 +14,7 @@ public class MainFrame extends JFrame {
|
|||||||
private static final long serialVersionUID = 1L;
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
public static void main(String[] args) {
|
public static void main(String[] args) {
|
||||||
MainFrame mainFrame = new MainFrame();
|
new UserChooserFrame();
|
||||||
mainFrame.playGame();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private BoardPanel bp;
|
private BoardPanel bp;
|
||||||
@@ -24,9 +23,10 @@ public class MainFrame extends JFrame {
|
|||||||
private TileSelectionPanel tp;
|
private TileSelectionPanel tp;
|
||||||
ScorePanel sp;
|
ScorePanel sp;
|
||||||
|
|
||||||
public MainFrame() {
|
public MainFrame(String name) {
|
||||||
super("CS8803 Project 4");
|
super("CS8803 Project 4");
|
||||||
referee = new Referee(this);
|
|
||||||
|
referee = new Referee(this, name);
|
||||||
|
|
||||||
init();
|
init();
|
||||||
|
|
||||||
@@ -35,6 +35,7 @@ public class MainFrame extends JFrame {
|
|||||||
setLocationRelativeTo(null);
|
setLocationRelativeTo(null);
|
||||||
setVisible(true);
|
setVisible(true);
|
||||||
|
|
||||||
|
playGame();
|
||||||
}
|
}
|
||||||
|
|
||||||
public ScorePanel getScorePanel() {
|
public ScorePanel getScorePanel() {
|
||||||
|
|||||||
164
src/view/UserChooserFrame.java
Normal file
164
src/view/UserChooserFrame.java
Normal file
@@ -0,0 +1,164 @@
|
|||||||
|
package view;
|
||||||
|
|
||||||
|
import java.awt.GridBagConstraints;
|
||||||
|
import java.awt.GridBagLayout;
|
||||||
|
import java.awt.Insets;
|
||||||
|
import java.awt.event.ActionEvent;
|
||||||
|
import java.awt.event.ActionListener;
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileInputStream;
|
||||||
|
import java.io.FileOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.ObjectInputStream;
|
||||||
|
import java.io.ObjectOutputStream;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
|
||||||
|
import javax.swing.JButton;
|
||||||
|
import javax.swing.JComboBox;
|
||||||
|
import javax.swing.JFrame;
|
||||||
|
import javax.swing.JLabel;
|
||||||
|
|
||||||
|
import model.playerModel.PlayerModel;
|
||||||
|
|
||||||
|
public class UserChooserFrame extends JFrame {
|
||||||
|
|
||||||
|
public static final String PLAYER_LIST_FILE = "players.dat";
|
||||||
|
public static final JLabel RULES_TEXT = new JLabel(
|
||||||
|
"Here should go some rules text. Lorem ipsum and blah blah blah.");
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
private final JButton playButton = new JButton("Play!");
|
||||||
|
private final JComboBox<String> userNameBox = new JComboBox<String>();
|
||||||
|
private ArrayList<String> users;
|
||||||
|
|
||||||
|
public UserChooserFrame() {
|
||||||
|
initLayout();
|
||||||
|
initActions();
|
||||||
|
|
||||||
|
pack();
|
||||||
|
setResizable(false);
|
||||||
|
setDefaultCloseOperation(EXIT_ON_CLOSE);
|
||||||
|
setLocationRelativeTo(null);
|
||||||
|
setVisible(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getUserListPath() {
|
||||||
|
return PlayerModel.DATA_FOLDER + PLAYER_LIST_FILE;
|
||||||
|
}
|
||||||
|
|
||||||
|
private ArrayList<String> getUsers() {
|
||||||
|
FileInputStream fin = null;
|
||||||
|
ObjectInputStream oin = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
fin = new FileInputStream(getUserListPath());
|
||||||
|
oin = new ObjectInputStream(fin);
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
ArrayList<String> list = (ArrayList<String>) oin.readObject();
|
||||||
|
|
||||||
|
oin.close();
|
||||||
|
return list;
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
return new ArrayList<String>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void initActions() {
|
||||||
|
userNameBox.setEditable(true);
|
||||||
|
|
||||||
|
users = getUsers();
|
||||||
|
Collections.sort(users);
|
||||||
|
|
||||||
|
for (int i = 0; i < users.size(); i++) {
|
||||||
|
userNameBox.addItem(users.get(i));
|
||||||
|
}
|
||||||
|
|
||||||
|
playButton.addActionListener(new ActionListener() {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void actionPerformed(ActionEvent arg0) {
|
||||||
|
String name = ((String) userNameBox.getSelectedItem());
|
||||||
|
name = name == null ? name : name.trim().toLowerCase();
|
||||||
|
|
||||||
|
if (name != null && name.compareTo("") != 0) {
|
||||||
|
UserChooserFrame.this.setVisible(false);
|
||||||
|
|
||||||
|
boolean found = false;
|
||||||
|
for (int i = 0; !found && i < users.size(); i++) {
|
||||||
|
if (name.compareTo(users.get(i)) == 0) {
|
||||||
|
found = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!found) {
|
||||||
|
users.add(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
saveUserList();
|
||||||
|
new MainFrame(name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void initLayout() {
|
||||||
|
GridBagLayout gbl = new GridBagLayout();
|
||||||
|
|
||||||
|
GridBagConstraints con = new GridBagConstraints();
|
||||||
|
con.fill = GridBagConstraints.BOTH;
|
||||||
|
con.gridheight = 1;
|
||||||
|
con.gridwidth = 5;
|
||||||
|
con.gridx = 0;
|
||||||
|
con.gridy = 0;
|
||||||
|
con.insets = new Insets(20, 20, 5, 20);
|
||||||
|
con.weightx = 1;
|
||||||
|
con.weighty = 1;
|
||||||
|
gbl.setConstraints(RULES_TEXT, con);
|
||||||
|
|
||||||
|
con = new GridBagConstraints();
|
||||||
|
con.fill = GridBagConstraints.BOTH;
|
||||||
|
con.gridheight = 1;
|
||||||
|
con.gridwidth = 5;
|
||||||
|
con.gridx = 0;
|
||||||
|
con.gridy = 1;
|
||||||
|
con.insets = new Insets(5, 20, 5, 20);
|
||||||
|
con.weightx = 1;
|
||||||
|
con.weighty = 1;
|
||||||
|
gbl.setConstraints(userNameBox, con);
|
||||||
|
|
||||||
|
con = new GridBagConstraints();
|
||||||
|
con.fill = GridBagConstraints.BOTH;
|
||||||
|
con.gridheight = 1;
|
||||||
|
con.gridwidth = 1;
|
||||||
|
con.gridx = 2;
|
||||||
|
con.gridy = 2;
|
||||||
|
con.insets = new Insets(5, 20, 20, 20);
|
||||||
|
con.weightx = 1;
|
||||||
|
con.weighty = 1;
|
||||||
|
gbl.setConstraints(playButton, con);
|
||||||
|
|
||||||
|
setLayout(gbl);
|
||||||
|
add(RULES_TEXT);
|
||||||
|
add(userNameBox);
|
||||||
|
add(playButton);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void saveUserList() {
|
||||||
|
FileOutputStream fout = null;
|
||||||
|
ObjectOutputStream oout = null;
|
||||||
|
String path = getUserListPath();
|
||||||
|
try {
|
||||||
|
(new File(PlayerModel.DATA_FOLDER)).mkdirs();
|
||||||
|
|
||||||
|
fout = new FileOutputStream(path);
|
||||||
|
oout = new ObjectOutputStream(fout);
|
||||||
|
oout.writeObject(users);
|
||||||
|
oout.close();
|
||||||
|
} catch (IOException ex) {
|
||||||
|
ex.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -14,7 +14,7 @@ import model.playerModel.PlayerModel;
|
|||||||
public class Tests extends TestCase {
|
public class Tests extends TestCase {
|
||||||
|
|
||||||
public static PlayerModel getFakePlayerModel() {
|
public static PlayerModel getFakePlayerModel() {
|
||||||
PlayerModel pm = new PlayerModel();
|
PlayerModel pm = new PlayerModel("marshall");
|
||||||
|
|
||||||
pm.logGame(25);
|
pm.logGame(25);
|
||||||
pm.logGame(30);
|
pm.logGame(30);
|
||||||
|
|||||||
Reference in New Issue
Block a user