23 Commits

Author SHA1 Message Date
Tobias Krause fbd2f5d4dd player_management: login loads existing player from file or creates new player 3 years ago
Tobias Krause 9dc32c8ff2 player_management: added layout for LoginPanel 3 years ago
Tobias Krause 24dc1a07d1 player_management: added LoginPanel to gameexplorer 3 years ago
Tobias Krause 4cf45ae7fb player_management: selectPlayer adds new players to player-list 3 years ago
Tobias Krause 0f95e6b4b2 player_management: savePlayers writes player list to file 3 years ago
Tobias Krause 6bad0a8b6e player_management: added method savePlayers 3 years ago
Tobias Krause d173c8c6d3 player_management: loadPlayers reads list of players from csv-file 3 years ago
Tobias Krause a5de8fe509 player_management: loadPlayers reads player from csv-file 3 years ago
Tobias Krause f78c286544 player_management: added method loadPlayers 3 years ago
Tobias Krause ca1185a335 player_management: returns player from list if existing 3 years ago
Tobias Krause 4ea7f2513c player_management: added method selectPlayer, added method equals and 3 years ago
Tobias Krause 64767b51c5 gameexplorer: each button opens a different (dummy) game panel 3 years ago
Tobias Krause ae886a415e gameexplorer: added action listeners for navigation between game and menu 3 years ago
Tobias Krause 94c64cc7e6 gameexplorer: added game panel 3 years ago
Tobias Krause 17dd6d2a9e gameexplorer: added layout for navigation panel 3 years ago
Tobias Krause 915fabc072 gameexplorer: added navigation panel + refactoring 3 years ago
Tobias Krause 55480d3112 gameexplorer: added gui layout 3 years ago
Tobias Krause e83a05d06b gameexplorer: added gui 3 years ago
Tobias Krause 2221c02714 gameexplorer: points cant be less than zero 3 years ago
Tobias Krause 3dea3b1948 gameexplorer: addPoints() increases / decreases points of player 3 years ago
Tobias Krause 8e8f4376ef gameexplorer: addPoints() overrides points of player 3 years ago
Tobias Krause 7ac7deb595 gameexplorer: added new class player 3 years ago
Lorenz Hohmann 5b72e67974 Fixed pom.xml: JUnit 4 & 5 Integration 3 years ago
  1. 22
      pom.xml
  2. 313
      src/main/java/de/tims/gameexplorer/GameExplorer.java
  3. 45
      src/main/java/de/tims/player_management/Player.java
  4. 95
      src/main/java/de/tims/player_management/PlayerManager.java
  5. 0
      src/main/java/resources/player_data.csv
  6. 110
      src/test/java/de/tims/player_management/PlayerManagerTest.java
  7. 32
      src/test/java/de/tims/player_management/PlayerTest.java
  8. 3
      src/test/java/resources/player_testdata.csv
  9. 2
      src/test/java/resources/player_testdata2.csv

22
pom.xml

@ -1,5 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion> <modelVersion>4.0.0</modelVersion>
<groupId>de.hs-fulda</groupId> <groupId>de.hs-fulda</groupId>
<artifactId>TIMS</artifactId> <artifactId>TIMS</artifactId>
@ -9,12 +10,13 @@
<url>http://maven.apache.org</url> <url>http://maven.apache.org</url>
<properties> <properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.target>11</maven.compiler.target>
<maven.compiler.source>11</maven.compiler.source>
<junit.jupiter.version>5.8.1</junit.jupiter.version>
<maven.compiler.target>1.8</maven.compiler.target>
<maven.compiler.source>1.8</maven.compiler.source>
<junit.jupiter.version>5.8.0</junit.jupiter.version>
<junit.platform.version>1.8.1</junit.platform.version> <junit.platform.version>1.8.1</junit.platform.version>
<assertj.version>3.21.0</assertj.version> <assertj.version>3.21.0</assertj.version>
<junit.version>3.8.1</junit.version>
<junit.version>4.12</junit.version>
<junit-vintage-engine>4.12.1</junit-vintage-engine>
<mockito.core.version>4.1.0</mockito.core.version> <mockito.core.version>4.1.0</mockito.core.version>
<mockito.junit.jupiter.version>4.1.0</mockito.junit.jupiter.version> <mockito.junit.jupiter.version>4.1.0</mockito.junit.jupiter.version>
<junit.platform.surefire.provider.version>1.0.1</junit.platform.surefire.provider.version> <junit.platform.surefire.provider.version>1.0.1</junit.platform.surefire.provider.version>
@ -33,6 +35,11 @@
<version>${junit.jupiter.version}</version> <version>${junit.jupiter.version}</version>
<scope>test</scope> <scope>test</scope>
</dependency> </dependency>
<dependency>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
<version>${junit.jupiter.version}</version>
</dependency>
<dependency> <dependency>
<groupId>org.junit.jupiter</groupId> <groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-params</artifactId> <artifactId>junit-jupiter-params</artifactId>
@ -70,6 +77,11 @@
<artifactId>junit-platform-surefire-provider</artifactId> <artifactId>junit-platform-surefire-provider</artifactId>
<version>${junit.platform.surefire.provider.version}</version> <version>${junit.platform.surefire.provider.version}</version>
</dependency> </dependency>
<dependency>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
<version>${junit-vintage-engine}</version>
</dependency>
<dependency> <dependency>
<groupId>org.junit.jupiter</groupId> <groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId> <artifactId>junit-jupiter-engine</artifactId>

313
src/main/java/de/tims/gameexplorer/GameExplorer.java

@ -0,0 +1,313 @@
package de.tims.gameexplorer;
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
import de.tims.player_management.Player;
import de.tims.player_management.PlayerManager;
public class GameExplorer {
private JFrame frame;
private JPanel explorerPanel;
private JPanel loginPanel;
private JPanel gamePanel;
private JPanel navigationPanel;
private JPanel fleetstormPanel;
private JPanel fourwinsPanel;
private JPanel tictactoePanel;
private JPanel leaderboardPanel;
private JPanel border1;
private JPanel border2;
private JPanel border3;
private JPanel border4;
private JPanel border5;
private JPanel border6;
private JButton loginBtn;
private JButton fleetstormBtn;
private JButton fourwinsBtn;
private JButton tictactoeBtn;
private JButton leaderboardBtn;
private JButton backBtn;
private JLabel username;
private JLabel loginWarning;
private JLabel chosenGame;
private JTextField usernameInput;
private Dimension minSize;
private Dimension loginBtnSize;
private Dimension btnSize;
private GridBagConstraints gbc;
private static final String playerFile = "src/main/java/resources/player_data.csv";
private enum Game { FLEETSTORM, FOURWINS, TICTACTOE, LEADERBOARD };
private Game actualGame;
private PlayerManager manager;
private Player actualPlayer;
public GameExplorer() {
manager = new PlayerManager();
frame = new JFrame("1000 infomagische Spiele");
minSize = new Dimension(400, 300);
loginBtnSize = new Dimension(91, 20);
btnSize = new Dimension(160, 40);
gbc = new GridBagConstraints();
buildExplorerPanel();
buildLoginPanel();
buildNavigationPanel();
buildGamePanels();
frame.add(loginPanel);
frame.setMinimumSize(minSize);
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setSize(640, 480);
frame.setResizable(true);
frame.setVisible(true);
}
private void buildExplorerPanel() {
explorerPanel = new JPanel();
explorerPanel.setLayout(new GridBagLayout());
fleetstormBtn = new JButton("Schiffe versenken");
fleetstormBtn.setPreferredSize(btnSize);
fleetstormBtn.addActionListener(new GameAction());
fourwinsBtn = new JButton("Vier gewinnt");
fourwinsBtn.setPreferredSize(btnSize);
fourwinsBtn.addActionListener(new GameAction());
tictactoeBtn = new JButton("TicTacToe");
tictactoeBtn.setPreferredSize(btnSize);
tictactoeBtn.addActionListener(new GameAction());
leaderboardBtn = new JButton("Leaderboard");
leaderboardBtn.setPreferredSize(btnSize);
leaderboardBtn.addActionListener(new GameAction());
border1 = new JPanel();
border1.setOpaque(false);
border2 = new JPanel();
border2.setOpaque(false);
border3 = new JPanel();
border3.setOpaque(false);
border4 = new JPanel();
border4.setOpaque(false);
border5 = new JPanel();
border5.setOpaque(false);
gbc.gridx = 0;
gbc.gridy = 0;
gbc.weighty = 0.2;
explorerPanel.add(border1, gbc);
gbc.gridx = 0;
gbc.gridy = 1;
gbc.weighty = 0.0;
explorerPanel.add(fleetstormBtn, gbc);
gbc.gridx = 0;
gbc.gridy = 2;
gbc.weighty = 0.2;
explorerPanel.add(border2, gbc);
gbc.gridx = 0;
gbc.gridy = 3;
gbc.weighty = 0.0;
explorerPanel.add(fourwinsBtn, gbc);
gbc.gridx = 0;
gbc.gridy = 4;
gbc.weighty = 0.2;
explorerPanel.add(border3, gbc);
gbc.gridx = 0;
gbc.gridy = 5;
gbc.weighty = 0.0;
explorerPanel.add(tictactoeBtn, gbc);
gbc.gridx = 0;
gbc.gridy = 6;
gbc.weighty = 0.2;
explorerPanel.add(border4, gbc);
gbc.gridx = 0;
gbc.gridy = 7;
gbc.weighty = 0.0;
explorerPanel.add(leaderboardBtn, gbc);
gbc.gridx = 0;
gbc.gridy = 8;
gbc.weighty = 0.2;
explorerPanel.add(border5, gbc);
}
private void buildLoginPanel() {
loginPanel = new JPanel();
loginPanel.setLayout(new GridBagLayout());
loginBtn = new JButton("Login");
loginBtn.setPreferredSize(loginBtnSize);
loginBtn.addActionListener(new LoginAction());
username = new JLabel("Name eingeben:");
loginWarning = new JLabel();
usernameInput = new JTextField(8);
gbc.weighty = 0;
gbc.gridx = 0;
gbc.gridy = 0;
gbc.insets = new Insets(0, 0, 5, 0);
loginPanel.add(username, gbc);
gbc.gridx = 0;
gbc.gridy = 1;
gbc.insets = new Insets(5, 0, 5, 0);
loginPanel.add(usernameInput, gbc);
gbc.gridx = 0;
gbc.gridy = 2;
gbc.insets = new Insets(5, 0, 5, 0);
loginPanel.add(loginBtn, gbc);
gbc.gridx = 0;
gbc.gridy = 3;
gbc.insets = new Insets(5, 0, 0, 0);
loginPanel.add(loginWarning, gbc);
}
private void buildNavigationPanel() {
navigationPanel = new JPanel();
navigationPanel.setLayout(new GridBagLayout());
backBtn = new JButton("< Zurück");
backBtn.addActionListener(new BackAction());
chosenGame = new JLabel();
border6 = new JPanel();
border6.setOpaque(false);
gbc.weighty = 0.0;
gbc.gridx = 0;
gbc.gridy = 0;
gbc.weightx = 0.0;
gbc.insets = new Insets(5, 20, 5, 0);
navigationPanel.add(backBtn, gbc);
gbc.gridx = 1;
gbc.gridy = 0;
gbc.weightx = 1.0;
gbc.insets = new Insets(0, 0, 0, 0);
navigationPanel.add(border6, gbc);
gbc.gridx = 2;
gbc.gridy = 0;
gbc.weightx = 0.0;
gbc.insets = new Insets(5, 0, 5, 20);
navigationPanel.add(chosenGame, gbc);
}
private void buildGamePanels() {
gamePanel = new JPanel();
gamePanel.setLayout(new BorderLayout());
gamePanel.add(navigationPanel, BorderLayout.PAGE_START);
//use of dummy panels because real panels have not been implemented yet
fleetstormPanel = new JPanel();
fleetstormPanel.setBackground(Color.BLUE);
fourwinsPanel = new JPanel();
fourwinsPanel.setBackground(Color.GREEN);
tictactoePanel = new JPanel();
tictactoePanel.setBackground(Color.YELLOW);
leaderboardPanel = new JPanel();
leaderboardPanel.setBackground(Color.RED);
}
private class LoginAction implements ActionListener {
@Override
public void actionPerformed(ActionEvent e) {
String userInput = usernameInput.getText();
if (!userInput.equals("")) {
loginWarning.setText("");
manager.loadPlayers(playerFile);
actualPlayer = manager.selectPlayer(userInput);
frame.remove(loginPanel);
frame.add(explorerPanel);
frame.revalidate();
frame.repaint();
System.out.println("Actual Player: " + actualPlayer.getName() + ", Points: " + actualPlayer.getPoints());
} else {
loginWarning.setText("Kein Name eingegeben!");
}
}
}
private class GameAction implements ActionListener {
@Override
public void actionPerformed(ActionEvent e) {
//each button adds a different dummy panel to the gamePanel
if (e.getSource() == fleetstormBtn) {
actualGame = Game.FLEETSTORM;
chosenGame.setText("Schiffe versenken");
gamePanel.add(fleetstormPanel, BorderLayout.CENTER);
} else if (e.getSource() == fourwinsBtn) {
actualGame = Game.FOURWINS;
chosenGame.setText("Vier gewinnt");
gamePanel.add(fourwinsPanel, BorderLayout.CENTER);
} else if (e.getSource() == tictactoeBtn) {
actualGame = Game.TICTACTOE;
chosenGame.setText("TicTacToe");
gamePanel.add(tictactoePanel, BorderLayout.CENTER);
} else if (e.getSource() == leaderboardBtn) {
actualGame = Game.LEADERBOARD;
chosenGame.setText("Leaderboard");
gamePanel.add(leaderboardPanel, BorderLayout.CENTER);
}
frame.remove(explorerPanel);
frame.add(gamePanel);
frame.revalidate();
frame.repaint();
}
}
private class BackAction implements ActionListener {
@Override
public void actionPerformed(ActionEvent e) {
switch (actualGame) {
case FLEETSTORM:
gamePanel.remove(fleetstormPanel);
break;
case FOURWINS:
gamePanel.remove(fourwinsPanel);
break;
case TICTACTOE:
gamePanel.remove(tictactoePanel);
break;
case LEADERBOARD:
gamePanel.remove(leaderboardPanel);
}
frame.remove(gamePanel);
frame.add(explorerPanel);
frame.revalidate();
frame.repaint();
}
}
public static void main(String[] args) {
new GameExplorer();
}
}

45
src/main/java/de/tims/player_management/Player.java

@ -0,0 +1,45 @@
package de.tims.player_management;
public class Player {
private String name;
private int points;
public Player(String name, int points) {
this.name = name;
this.points = points;
}
public String getName() {
return name;
}
public int getPoints() {
return this.points;
}
public void addPoints(int pointsToAdd) {
this.points = (this.points + pointsToAdd > 0) ? this.points + pointsToAdd : 0;
}
@Override
public boolean equals(Object o) {
if (o == this) {
return true;
}
if ((o == null) || this.getClass() != o.getClass()) {
return false;
}
Player player2 = (Player) o;
return this.points == player2.points && this.name.equals(player2.getName());
}
@Override
public int hashCode() {
return points + name.hashCode();
}
}

95
src/main/java/de/tims/player_management/PlayerManager.java

@ -0,0 +1,95 @@
package de.tims.player_management;
import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.util.LinkedList;
import java.util.List;
public class PlayerManager {
private List<Player> players;
private static final int PLAYER_ATTRIBUTES = 2;
public List<Player> getPlayers() {
return players;
}
public void setPlayers(List<Player> players) {
this.players = players;
}
public Player selectPlayer(String playerName) {
for (Player p : this.players) {
if (playerName.equals(p.getName())) {
return p;
}
}
Player newPlayer = new Player(playerName, 0);
players.add(newPlayer);
return newPlayer;
}
public void loadPlayers(String fileName) {
players = new LinkedList<Player>();
File playerData = new File(fileName);
try {
FileReader fr = new FileReader(playerData);
int c = -1;
String playerName;
int playerPoints;
StringBuilder nameBuilder = new StringBuilder();
StringBuilder pointBuilder = new StringBuilder();
StringBuilder[] sb = {nameBuilder, pointBuilder};
do {
for (int i = 0; i < PLAYER_ATTRIBUTES; i++) {
do {
c = fr.read();
if (c != ';' && c != '\n' && c != -1) {
sb[i].append((char) c);
}
} while (c != ';' && c != '\n' && c != -1);
}
if (!nameBuilder.toString().equals("") && !pointBuilder.toString().equals("")) {
playerName = nameBuilder.toString();
playerPoints = Integer.parseInt(pointBuilder.toString());
players.add(new Player(playerName, playerPoints));
nameBuilder.delete(0, nameBuilder.length());
pointBuilder.delete(0, pointBuilder.length());
}
} while (c != -1);
fr.close();
} catch (IOException e) {
return;
}
}
public void savePlayers(String fileName) {
File playerData = new File(fileName);
try {
FileWriter fw = new FileWriter(playerData, false);
for (Player elem : players) {
fw.write(elem.getName() + ";" + elem.getPoints() + "\n");
}
fw.close();
} catch (IOException e) {
return;
}
}
}

0
src/main/java/resources/player_data.csv

110
src/test/java/de/tims/player_management/PlayerManagerTest.java

@ -0,0 +1,110 @@
package de.tims.player_management;
import static org.assertj.core.api.Assertions.*;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.util.LinkedList;
import java.util.List;
import java.util.stream.Stream;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
class PlayerManagerTest {
PlayerManager manager = new PlayerManager();
@ParameterizedTest
@MethodSource("testCasesForSelectPlayer")
void selectPlayerTest(String testName, List<Player> players, String playerName, Player expectedResult) {
manager.setPlayers(players);
Player calculatedResult = manager.selectPlayer(playerName);
assertThat(calculatedResult).describedAs(testName).isEqualTo(expectedResult);
}
private static Stream<Arguments> testCasesForSelectPlayer() {
return Stream.of(Arguments.of("NoPlayersYetReturnNewPlayer", new LinkedList<Player>(List.of()),
"Tobias", new Player("Tobias", 0)),
Arguments.of("NoPlayerWithNameInListReturnNewPlayer", new LinkedList<Player>(List.of(new Player("Steffen", 40), new Player("Lorenz", 60))),
"Tobias", new Player("Tobias", 0)),
Arguments.of("PlayerWithNameInListReturnPlayerInList", new LinkedList<Player>(List.of(new Player("Steffen", 40), new Player("Tobias", 50))),
"Tobias", new Player("Tobias", 50)));
}
@ParameterizedTest
@MethodSource("testCasesForLoadPlayers")
void loadPlayersTest(String testName, String fileContent, String fileName, List<Player> expectedResult) {
File playerData = new File(fileName);
try {
FileWriter fw = new FileWriter(playerData, false);
fw.write(fileContent);
fw.close();
} catch (IOException e) {
fail("Cannot open file");
}
manager.loadPlayers(fileName);
List<Player> calculatedResult = manager.getPlayers();
assertThat(calculatedResult).describedAs(testName).isEqualTo(expectedResult);
}
private static Stream<Arguments> testCasesForLoadPlayers() {
return Stream.of(Arguments.of("EmptyFileReturnsEmtpyList", "", "src/test/java/resources/player_testdata.csv", List.of()),
Arguments.of("OnePlayerInFileReturnsListWithOneElement", "Tobias;50", "src/test/java/resources/player_testdata.csv",
List.of(new Player("Tobias", 50))),
Arguments.of("MorePlayersInFileReturnLongerList", "Tobias;50\nLorenz;40\nSteffen;60", "src/test/java/resources/player_testdata.csv",
List.of(new Player("Tobias", 50), new Player("Lorenz", 40), new Player("Steffen", 60))));
}
@ParameterizedTest
@MethodSource("testCasesForSavePlayers")
void savePlayersTest(String testName, String fileName, List<Player> expectedResult) {
manager.setPlayers(expectedResult);
manager.savePlayers(fileName);
manager.loadPlayers(fileName);
List<Player> calculatedResult = manager.getPlayers();
assertThat(calculatedResult).describedAs(testName).isEqualTo(expectedResult);
}
private static Stream<Arguments> testCasesForSavePlayers() {
return Stream.of(Arguments.of("EmptyListIsWrittenAsEmptyFile", "src/test/java/resources/player_testdata2.csv", List.of()),
Arguments.of("WriteElementsOfListToFile", "src/test/java/resources/player_testdata2.csv",
List.of(new Player("Tobias", 50), new Player("Steffen", 60))));
}
@Test
void selectPlayerAddsNewPlayersToList() {
LinkedList<Player> players = new LinkedList<Player>();
players.add(new Player("Steffen", 40));
players.add(new Player("Max", 60));
manager.setPlayers(players);
manager.selectPlayer("Tobias");
List<Player> expectedResult = List.of(new Player("Steffen", 40), new Player("Max", 60), new Player("Tobias", 0));
List<Player> calculatedResult = manager.getPlayers();
assertThat(calculatedResult).describedAs("SelectNewPlayerAddsNewPlayerToPlayersList").isEqualTo(expectedResult);
}
@Test
void selectExistingPlayerDoesntChangeTheList() {
LinkedList<Player> players = new LinkedList<Player>();
players.add(new Player("Steffen", 40));
players.add(new Player("Max", 60));
manager.setPlayers(players);
manager.selectPlayer("Max");
List<Player> expectedResult = List.of(new Player("Steffen", 40), new Player("Max", 60));
List<Player> calculatedResult = manager.getPlayers();
assertThat(calculatedResult).describedAs("SelectNewPlayerAddsNewPlayerToPlayersList").isEqualTo(expectedResult);
}
}

32
src/test/java/de/tims/player_management/PlayerTest.java

@ -0,0 +1,32 @@
package de.tims.player_management;
import static org.assertj.core.api.Assertions.*;
import java.util.stream.Stream;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
class PlayerTest {
Player player;
@ParameterizedTest
@MethodSource("testCasesForAddPoints")
void addPointsTest(String testName, int pointsBefore, int pointsToAdd, int expectedResult) {
player = new Player("TestPlayer", pointsBefore);
player.addPoints(pointsToAdd);
int calculatedResult = player.getPoints();
assertThat(calculatedResult).describedAs(testName).isEqualTo(expectedResult);
}
private static Stream<Arguments> testCasesForAddPoints() {
return Stream.of(Arguments.of("NoPointsBeforeGet0Points", 0, 0, 0),
Arguments.of("NoPointsBeforeGet10Points", 0, 10, 10),
Arguments.of("10PointsBeforeAdd10Points", 10, 10, 20),
Arguments.of("10PointsBeforeLose10Points", 10, -10, 0),
Arguments.of("LoseMorePointsThanYouHave", 10, -20, 0));
}
}

3
src/test/java/resources/player_testdata.csv

@ -0,0 +1,3 @@
Tobias;50
Lorenz;40
Steffen;60

2
src/test/java/resources/player_testdata2.csv

@ -0,0 +1,2 @@
Tobias;50
Steffen;60
Loading…
Cancel
Save