Added simple GUI for 9x9 go. Use HUMAN_GUI player option.
This commit is contained in:
1
README.txt
Normal file
1
README.txt
Normal file
@@ -0,0 +1 @@
|
||||
Wood texture is from open source Wordpress resource site wpliving.net.
|
||||
@@ -3,6 +3,8 @@ package net.woodyfolsom.msproj;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import net.woodyfolsom.msproj.gui.Goban;
|
||||
import net.woodyfolsom.msproj.policy.HumanGuiInput;
|
||||
import net.woodyfolsom.msproj.policy.HumanKeyboardInput;
|
||||
import net.woodyfolsom.msproj.policy.MonteCarloUCT;
|
||||
import net.woodyfolsom.msproj.policy.Policy;
|
||||
@@ -10,8 +12,12 @@ import net.woodyfolsom.msproj.policy.RandomMovePolicy;
|
||||
import net.woodyfolsom.msproj.policy.RootParallelization;
|
||||
|
||||
public class StandAloneGame {
|
||||
private static final double DEFAULT_KOMI = 5.5;
|
||||
private static final int DEFAULT_NUM_GAMES = 1;
|
||||
private static final int DEFAULT_SIZE = 9;
|
||||
|
||||
enum PLAYER_TYPE {
|
||||
HUMAN, ROOT_PAR, UCT_FAST, UCT_SLOW
|
||||
HUMAN, HUMAN_GUI, ROOT_PAR, UCT_FAST, UCT_SLOW
|
||||
};
|
||||
|
||||
public static void main(String[] args) {
|
||||
@@ -21,8 +27,23 @@ public class StandAloneGame {
|
||||
System.out
|
||||
.println("For example to play 10 games against MC UCT w/ slow moves: StandAloneGame UCT_SLOW HUMAN 10");
|
||||
}
|
||||
int nGames = DEFAULT_NUM_GAMES;
|
||||
int size = DEFAULT_SIZE;
|
||||
double komi = DEFAULT_KOMI;
|
||||
|
||||
switch (args.length) {
|
||||
case 5:
|
||||
nGames = Integer.valueOf(args[4]);
|
||||
case 4:
|
||||
komi = Double.valueOf(args[3]);
|
||||
case 3:
|
||||
size = Integer.valueOf(args[2]);
|
||||
break;
|
||||
default:
|
||||
System.out.println("Arguments #3-5 not specified. Using default size=" + size +", komi = " + komi +", nGames=" + nGames +".");
|
||||
}
|
||||
new StandAloneGame().playGame(parsePlayerType(args[0]),
|
||||
parsePlayerType(args[1]), Integer.valueOf(args[2]));
|
||||
parsePlayerType(args[1]), size, komi, nGames);
|
||||
}
|
||||
|
||||
private static PLAYER_TYPE parsePlayerType(String playerTypeStr) {
|
||||
@@ -35,23 +56,25 @@ public class StandAloneGame {
|
||||
return PLAYER_TYPE.UCT_SLOW;
|
||||
} else if ("HUMAN".equalsIgnoreCase(playerTypeStr)) {
|
||||
return PLAYER_TYPE.HUMAN;
|
||||
} else if ("HUMAN_GUI".equalsIgnoreCase(playerTypeStr)) {
|
||||
return PLAYER_TYPE.HUMAN_GUI;
|
||||
} else {
|
||||
throw new RuntimeException("Unknown player type: " + playerTypeStr);
|
||||
}
|
||||
}
|
||||
|
||||
public void playGame(PLAYER_TYPE playerType1, PLAYER_TYPE playerType2,
|
||||
int rounds) {
|
||||
int size, double komi, int rounds) {
|
||||
|
||||
Policy player1 = getPolicy(playerType1);
|
||||
Policy player2 = getPolicy(playerType2);
|
||||
GameConfig gameConfig = new GameConfig(size);
|
||||
gameConfig.setKomi(komi);
|
||||
|
||||
Referee referee = new Referee();
|
||||
referee.setPolicy(Player.BLACK, player1);
|
||||
referee.setPolicy(Player.WHITE, player2);
|
||||
referee.setPolicy(Player.BLACK, getPolicy(playerType1, gameConfig, Player.BLACK));
|
||||
referee.setPolicy(Player.WHITE, getPolicy(playerType2, gameConfig, Player.WHITE));
|
||||
|
||||
List<GameResult> results = new ArrayList<GameResult>();
|
||||
GameConfig gameConfig = new GameConfig(9);
|
||||
|
||||
for (int round = 0; round < rounds; round++) {
|
||||
results.add(referee.play(gameConfig));
|
||||
}
|
||||
@@ -63,12 +86,14 @@ public class StandAloneGame {
|
||||
}
|
||||
}
|
||||
|
||||
private Policy getPolicy(PLAYER_TYPE playerType) {
|
||||
private Policy getPolicy(PLAYER_TYPE playerType, GameConfig gameConfig, Player player) {
|
||||
switch (playerType) {
|
||||
case HUMAN:
|
||||
return new HumanKeyboardInput();
|
||||
case HUMAN_GUI:
|
||||
return new HumanGuiInput(new Goban(gameConfig, player));
|
||||
case ROOT_PAR:
|
||||
return new RootParallelization(3, 6000L);
|
||||
return new RootParallelization(4, 6000L);
|
||||
case UCT_SLOW:
|
||||
return new MonteCarloUCT(new RandomMovePolicy(), 4000L);
|
||||
case UCT_FAST:
|
||||
|
||||
13
src/net/woodyfolsom/msproj/gui/BoardState.java
Normal file
13
src/net/woodyfolsom/msproj/gui/BoardState.java
Normal file
@@ -0,0 +1,13 @@
|
||||
package net.woodyfolsom.msproj.gui;
|
||||
|
||||
public class BoardState {
|
||||
|
||||
/**
|
||||
* @param args
|
||||
*/
|
||||
public static void main(String[] args) {
|
||||
// TODO Auto-generated method stub
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
6
src/net/woodyfolsom/msproj/gui/Collision.java
Normal file
6
src/net/woodyfolsom/msproj/gui/Collision.java
Normal file
@@ -0,0 +1,6 @@
|
||||
package net.woodyfolsom.msproj.gui;
|
||||
|
||||
public class Collision {
|
||||
|
||||
|
||||
}
|
||||
75
src/net/woodyfolsom/msproj/gui/Goban.java
Normal file
75
src/net/woodyfolsom/msproj/gui/Goban.java
Normal file
@@ -0,0 +1,75 @@
|
||||
package net.woodyfolsom.msproj.gui;
|
||||
|
||||
import java.awt.BorderLayout;
|
||||
import java.awt.event.KeyEvent;
|
||||
import java.awt.event.KeyListener;
|
||||
|
||||
import javax.swing.JFrame;
|
||||
|
||||
import net.woodyfolsom.msproj.Action;
|
||||
import net.woodyfolsom.msproj.GameConfig;
|
||||
import net.woodyfolsom.msproj.GameState;
|
||||
import net.woodyfolsom.msproj.Player;
|
||||
|
||||
public class Goban extends JFrame implements KeyListener {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
//private GameState gameState;
|
||||
private GridPanel gridPanel;
|
||||
|
||||
/*
|
||||
public static void main(String[] args) {
|
||||
Goban goban = new Goban();
|
||||
goban.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
|
||||
goban.setVisible(true);
|
||||
goban.pack();
|
||||
}*/
|
||||
|
||||
public Goban(GameConfig gameConfig, Player guiPlayer) {
|
||||
setLayout(new BorderLayout());
|
||||
this.gridPanel = new GridPanel(gameConfig, guiPlayer);
|
||||
add(gridPanel,BorderLayout.CENTER);
|
||||
addKeyListener(this);
|
||||
|
||||
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
|
||||
setVisible(true);
|
||||
pack();
|
||||
}
|
||||
|
||||
public Action getAction() {
|
||||
return gridPanel.getAction();
|
||||
}
|
||||
|
||||
public void setGameState(GameState gameState) {
|
||||
gridPanel.setGameState(gameState);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void keyPressed(KeyEvent arg0) {
|
||||
// TODO Auto-generated method stub
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void keyReleased(KeyEvent arg0) {
|
||||
// TODO Auto-generated method stub
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void keyTyped(KeyEvent arg0) {
|
||||
switch (arg0.getKeyChar())
|
||||
{
|
||||
/*
|
||||
case 't' :
|
||||
case 'T' :
|
||||
//Player currentPlayer = GoGame.getNextPlayer(gameState.getPlayerToMove());
|
||||
//System.out.println("Switching players. Current player is now " + currentPlayer);
|
||||
//gameState.
|
||||
break;*/
|
||||
default :
|
||||
System.out.println("Ignoring unbound key: " + arg0.getKeyChar());
|
||||
}
|
||||
}
|
||||
}
|
||||
284
src/net/woodyfolsom/msproj/gui/GridPanel.java
Normal file
284
src/net/woodyfolsom/msproj/gui/GridPanel.java
Normal file
@@ -0,0 +1,284 @@
|
||||
package net.woodyfolsom.msproj.gui;
|
||||
|
||||
import java.awt.Color;
|
||||
import java.awt.Dimension;
|
||||
import java.awt.Graphics;
|
||||
import java.awt.Image;
|
||||
import java.awt.event.MouseEvent;
|
||||
import java.awt.event.MouseListener;
|
||||
import java.awt.event.MouseMotionListener;
|
||||
import java.net.URL;
|
||||
import java.util.concurrent.LinkedBlockingQueue;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import javax.swing.ImageIcon;
|
||||
import javax.swing.JPanel;
|
||||
|
||||
import net.woodyfolsom.msproj.Action;
|
||||
import net.woodyfolsom.msproj.GameBoard;
|
||||
import net.woodyfolsom.msproj.GameConfig;
|
||||
import net.woodyfolsom.msproj.GameState;
|
||||
import net.woodyfolsom.msproj.Player;
|
||||
|
||||
public class GridPanel extends JPanel implements MouseListener,
|
||||
MouseMotionListener {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private static final int BOARD_HEIGHT = 300;
|
||||
private static final int BOARD_WIDTH = 300;
|
||||
private static final int BORDER_WIDTH = 10;
|
||||
|
||||
private static final int DOT_WIDTH = 5;
|
||||
private static final int STONE_WIDTH = DOT_WIDTH * 2;
|
||||
|
||||
private Image backgroundImg;
|
||||
private int boardSize;
|
||||
|
||||
private int mouseX = 0;
|
||||
private int mouseY = 0;
|
||||
|
||||
//
|
||||
private char[][] board;
|
||||
//private static final char WHITE = 'O';
|
||||
//private static final char BLACK = 'X';
|
||||
private static final char EMPTY = '.';
|
||||
//
|
||||
|
||||
private GameState gameState;
|
||||
private LinkedBlockingQueue<Action> actionQueue = new LinkedBlockingQueue<Action>();
|
||||
|
||||
private Player guiPlayer;
|
||||
|
||||
private String[] COLS = { "A", "B", "C", "D", "E", "F", "G", "H", "J" };
|
||||
private String[] ROWS;
|
||||
|
||||
public GridPanel(GameConfig gameConfig, Player guiPlayer) {
|
||||
this.guiPlayer = guiPlayer;
|
||||
|
||||
this.boardSize = gameConfig.getSize();
|
||||
|
||||
initRows(this.boardSize);
|
||||
|
||||
this.setPreferredSize(new Dimension(BOARD_WIDTH + BORDER_WIDTH * 2,
|
||||
BOARD_HEIGHT + BORDER_WIDTH * 2));
|
||||
URL urlBackgroundImg = getClass().getResource("wood3.jpg");
|
||||
this.backgroundImg = new ImageIcon(urlBackgroundImg).getImage();
|
||||
this.addMouseListener(this);
|
||||
this.addMouseMotionListener(this);
|
||||
|
||||
//
|
||||
board = new char[boardSize][boardSize];
|
||||
for (int i = 0; i < boardSize; i++) {
|
||||
for (int j = 0; j < boardSize; j++) {
|
||||
board[i][j] = EMPTY;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public Action getAction() {
|
||||
int timeLimit = 10;
|
||||
TimeUnit timeUnit = TimeUnit.SECONDS;
|
||||
|
||||
try {
|
||||
Action action = actionQueue.poll(timeLimit, timeUnit);
|
||||
if (action == null) {
|
||||
return Action.NONE;
|
||||
}
|
||||
return action;
|
||||
} catch (InterruptedException ie) {
|
||||
System.out.println("Interrupted while waiting " + timeLimit + " " + timeUnit + "s for player to move.");
|
||||
return Action.NONE;
|
||||
}
|
||||
}
|
||||
|
||||
private void initRows(int boardSize) {
|
||||
ROWS = new String[boardSize];
|
||||
for (int i = 0; i < boardSize; i++) {
|
||||
ROWS[i] = Integer.valueOf(boardSize - i).toString();
|
||||
}
|
||||
}
|
||||
|
||||
// Given a pair of (x,y) coordinates (pixels), returns a row index in the
|
||||
// range [0..(boardSize-1)].
|
||||
private int getColumn(int x, int y) {
|
||||
int intersectionSize = BOARD_WIDTH / boardSize;
|
||||
|
||||
return (x - BORDER_WIDTH/*- centerOffset*/) / intersectionSize;
|
||||
}
|
||||
|
||||
// Given a pair of (x,y) coordinates (pixels), returns a row index in the
|
||||
// range [0..(boardSize-1)].
|
||||
private int getRow(int x, int y) {
|
||||
int intersectionSize = BOARD_WIDTH / boardSize;
|
||||
|
||||
return (y - BORDER_WIDTH/*- centerOffset*/) / intersectionSize;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void paintComponent(Graphics g) {
|
||||
super.paintComponent(g);
|
||||
|
||||
// tile wood texture
|
||||
int iw = backgroundImg.getWidth(this);
|
||||
int ih = backgroundImg.getHeight(this);
|
||||
if (iw > 0 && ih > 0) {
|
||||
for (int x = 0; x < getWidth(); x += iw) {
|
||||
for (int y = 0; y < getHeight(); y += ih) {
|
||||
g.drawImage(backgroundImg, x, y, iw, ih, this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int intersectionSize = BOARD_WIDTH / boardSize;
|
||||
int centerOffset = intersectionSize / 2;
|
||||
|
||||
g.translate(BORDER_WIDTH, BORDER_WIDTH);
|
||||
|
||||
for (int row = 0; row < boardSize; row++) {
|
||||
for (int col = 0; col < boardSize; col++) {
|
||||
g.fillOval(intersectionSize * col + centerOffset - DOT_WIDTH,
|
||||
intersectionSize * row + centerOffset - DOT_WIDTH,
|
||||
DOT_WIDTH * 2, DOT_WIDTH * 2);
|
||||
}
|
||||
}
|
||||
|
||||
for (int row = 0; row < boardSize; row++) {
|
||||
g.drawLine(centerOffset, centerOffset + row * intersectionSize,
|
||||
intersectionSize * boardSize - centerOffset, centerOffset
|
||||
+ row * intersectionSize);
|
||||
}
|
||||
|
||||
for (int col = 0; col < boardSize; col++) {
|
||||
g.drawLine(centerOffset + col * intersectionSize, centerOffset,
|
||||
centerOffset + col * intersectionSize, intersectionSize
|
||||
* boardSize - centerOffset);
|
||||
}
|
||||
|
||||
Color defaultColor = g.getColor();
|
||||
g.setColor(Color.YELLOW);
|
||||
|
||||
int stoneX = getColumn(mouseX, mouseY) * intersectionSize
|
||||
+ centerOffset;
|
||||
int stoneY = getRow(mouseX, mouseY) * intersectionSize + centerOffset;
|
||||
|
||||
g.drawOval(stoneX - STONE_WIDTH, stoneY - STONE_WIDTH, STONE_WIDTH * 2,
|
||||
STONE_WIDTH * 2);
|
||||
|
||||
g.setColor(defaultColor);
|
||||
|
||||
for (int col = 0; col < boardSize; col++) {
|
||||
// g.drawLine(centerOffset + col * intersectionSize, centerOffset,
|
||||
// centerOffset + col * intersectionSize, intersectionSize *
|
||||
// boardSize - centerOffset);
|
||||
g.drawBytes(COLS[col].getBytes(), 0, 1, centerOffset + col
|
||||
* intersectionSize - 5, BOARD_WIDTH + 5);
|
||||
}
|
||||
|
||||
for (int row = 0; row < boardSize; row++) {
|
||||
// g.drawLine(centerOffset + col * intersectionSize, centerOffset,
|
||||
// centerOffset + col * intersectionSize, intersectionSize *
|
||||
// boardSize - centerOffset);
|
||||
g.drawBytes(ROWS[row].getBytes(), 0, 1, BOARD_WIDTH - 5,
|
||||
centerOffset + row * intersectionSize + 5);
|
||||
}
|
||||
|
||||
if (gameState != null) {
|
||||
GameBoard gameBoard = gameState.getGameBoard();
|
||||
|
||||
for (int row = 0; row < boardSize; row++) {
|
||||
for (int col = 0; col < boardSize; col++) {
|
||||
|
||||
char symbol = gameBoard.getSymbolAt(col,boardSize - row - 1);
|
||||
|
||||
switch (symbol) {
|
||||
case 'X' :
|
||||
g.setColor(Color.BLACK);
|
||||
break;
|
||||
case 'O' :
|
||||
g.setColor(Color.WHITE);
|
||||
break;
|
||||
default :
|
||||
continue;
|
||||
}
|
||||
|
||||
g.fillOval(intersectionSize * col + centerOffset - STONE_WIDTH,
|
||||
intersectionSize * row + centerOffset - STONE_WIDTH,
|
||||
STONE_WIDTH * 2, STONE_WIDTH * 2);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void setGameState(GameState gameState) {
|
||||
this.gameState = gameState;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void mouseDragged(MouseEvent arg0) {
|
||||
// TODO Auto-generated method stub
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void mouseMoved(MouseEvent arg0) {
|
||||
this.mouseX = arg0.getX();
|
||||
this.mouseY = arg0.getY();
|
||||
|
||||
this.repaint();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void mouseClicked(MouseEvent arg0) {
|
||||
if (gameState == null) {
|
||||
System.out.println("Wait your turn!");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!actionQueue.isEmpty()) {
|
||||
System.out.println("You already moved. Be patient!");
|
||||
return;
|
||||
}
|
||||
// ystem.out.println("Mouse clicked: " + arg0);
|
||||
int row = getRow(mouseX, mouseY);
|
||||
int column = getColumn(mouseX, mouseY);
|
||||
|
||||
Player currentPlayer = gameState.getPlayerToMove();
|
||||
if (currentPlayer == guiPlayer) {
|
||||
// gameState.playStone(guiPlayer, Action.getInstance(COLS[row] +
|
||||
// ROWS[column]));
|
||||
Action action = Action.getInstance(COLS[column] + ROWS[row]);
|
||||
System.out.println("Made move: " + action);
|
||||
actionQueue.add(action);
|
||||
} else {
|
||||
System.out.println("Not your turn!");
|
||||
}
|
||||
|
||||
this.repaint();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void mouseEntered(MouseEvent arg0) {
|
||||
// TODO Auto-generated method stub
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void mouseExited(MouseEvent arg0) {
|
||||
// TODO Auto-generated method stub
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void mousePressed(MouseEvent arg0) {
|
||||
// TODO Auto-generated method stub
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void mouseReleased(MouseEvent arg0) {
|
||||
// TODO Auto-generated method stub
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
6
src/net/woodyfolsom/msproj/gui/Stone.java
Normal file
6
src/net/woodyfolsom/msproj/gui/Stone.java
Normal file
@@ -0,0 +1,6 @@
|
||||
package net.woodyfolsom.msproj.gui;
|
||||
|
||||
public class Stone {
|
||||
|
||||
|
||||
}
|
||||
BIN
src/net/woodyfolsom/msproj/gui/wood3.jpg
Normal file
BIN
src/net/woodyfolsom/msproj/gui/wood3.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 8.0 KiB |
54
src/net/woodyfolsom/msproj/policy/HumanGuiInput.java
Normal file
54
src/net/woodyfolsom/msproj/policy/HumanGuiInput.java
Normal file
@@ -0,0 +1,54 @@
|
||||
package net.woodyfolsom.msproj.policy;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Collection;
|
||||
|
||||
import net.woodyfolsom.msproj.Action;
|
||||
import net.woodyfolsom.msproj.GameConfig;
|
||||
import net.woodyfolsom.msproj.GameState;
|
||||
import net.woodyfolsom.msproj.Player;
|
||||
import net.woodyfolsom.msproj.gui.Goban;
|
||||
|
||||
public class HumanGuiInput implements Policy {
|
||||
private Goban goban;
|
||||
|
||||
public HumanGuiInput(Goban goban) {
|
||||
this.goban = goban;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Action getAction(GameConfig gameConfig, GameState gameState,
|
||||
Player player) {
|
||||
Action action = null;
|
||||
String input = "";
|
||||
|
||||
do {
|
||||
System.out.println(player
|
||||
+ " to move: (Use GUI)");
|
||||
|
||||
goban.setGameState(gameState);
|
||||
action = goban.getAction();
|
||||
|
||||
if (action.isNone()) {
|
||||
System.out.println("No move was made within 10 seconds. Hurry up!");
|
||||
System.out.println(gameState);
|
||||
continue;
|
||||
}
|
||||
} while (action == null);
|
||||
|
||||
return action;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Action getAction(GameConfig gameConfig, GameState gameState,
|
||||
Collection<Action> prohibitedActions, Player player) {
|
||||
throw new UnsupportedOperationException("Not implemented");
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getNumStateEvaluations() {
|
||||
// TODO Auto-generated method stub
|
||||
return 1;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -13,7 +13,7 @@ import net.woodyfolsom.msproj.tree.GameTreeNode;
|
||||
import net.woodyfolsom.msproj.tree.MonteCarloProperties;
|
||||
|
||||
public abstract class MonteCarlo implements Policy {
|
||||
protected static final int ROLLOUT_DEPTH_LIMIT = 100;
|
||||
protected static final int ROLLOUT_DEPTH_LIMIT = 400;
|
||||
|
||||
protected int numStateEvaluations = 0;
|
||||
protected Policy movePolicy;
|
||||
|
||||
10
src/net/woodyfolsom/msproj/policy/MonteCarloAMAF.java
Normal file
10
src/net/woodyfolsom/msproj/policy/MonteCarloAMAF.java
Normal file
@@ -0,0 +1,10 @@
|
||||
package net.woodyfolsom.msproj.policy;
|
||||
|
||||
|
||||
public class MonteCarloAMAF extends MonteCarloUCT {
|
||||
|
||||
public MonteCarloAMAF(Policy movePolicy, long searchTimeLimit) {
|
||||
super(movePolicy, searchTimeLimit);
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user