Last active
April 6, 2024 09:42
-
-
Save PlusLake/755f1610303b94fd7225c7e4a9bed4ee to your computer and use it in GitHub Desktop.
七並べ(Java Swing)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import javax.swing.*; | |
import java.awt.*; | |
import java.awt.event.*; | |
import java.awt.geom.*; | |
import java.util.*; | |
import java.util.List; | |
import java.util.concurrent.atomic.*; | |
import java.util.function.*; | |
import java.util.stream.*; | |
import static java.awt.RenderingHints.*; | |
public class SevenCard { | |
public static final Dimension SIZE = new Dimension(800, 800); | |
public static void main(String[] args) { | |
frame(panel()); | |
} | |
static void frame(JPanel panel) { | |
JFrame frame = new JFrame("七並べ"); | |
frame.setResizable(false); | |
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); | |
frame.setContentPane(panel); | |
frame.pack(); | |
frame.setLocationRelativeTo(null); | |
frame.setVisible(true); | |
} | |
static JPanel panel() { | |
AtomicReference<Game> game = new AtomicReference<>(new Game()); | |
List<Entry<Shape, Runnable>> callbacks = new ArrayList<>(); | |
JPanel panel = new JPanel(null) { | |
public void paintComponent(Graphics graphics) { | |
render((Graphics2D) graphics, game.get(), callbacks); | |
} | |
}; | |
panel.addMouseListener(new MouseAdapter() { | |
public void mousePressed(MouseEvent event) { | |
if (event.getButton() == 3) { | |
game.set(new Game()); | |
panel.repaint(); | |
} | |
for (Entry<Shape, Runnable> callback : callbacks) { | |
if (callback.getKey().contains(event.getX(), event.getY())) { | |
callback.getValue().run(); | |
panel.repaint(); | |
break; | |
} | |
} | |
} | |
}); | |
panel.setPreferredSize(SIZE); | |
return panel; | |
} | |
static void render(Graphics2D graphics, Game game, List<Entry<Shape, Runnable>> callbacks) { | |
callbacks.clear(); | |
graphics.setRenderingHint(KEY_ANTIALIASING, VALUE_ANTIALIAS_ON); | |
graphics.setRenderingHint(KEY_TEXT_ANTIALIASING, VALUE_TEXT_ANTIALIAS_ON); | |
graphics.setColor(new Color(64, 128, 64)); | |
graphics.fillRect(0, 0, SIZE.width, SIZE.height); | |
graphics.setFont(graphics.getFont().deriveFont(16f)); | |
renderPlayerCard(graphics, game, callbacks); | |
renderBoard(graphics, game); | |
if (game.gameOver) { | |
callbacks.clear(); | |
graphics.setColor(new Color(0, 0, 0, 128)); | |
graphics.fillRect(0, 0, SIZE.width, SIZE.height); | |
graphics.setColor(Color.white); | |
graphics.drawString("Right click to restart", 100, 480); | |
graphics.setFont(graphics.getFont().deriveFont(256f)); | |
graphics.drawString("Game", 40, 200); | |
graphics.drawString("Over", 40, 450); | |
return; | |
} | |
callbacks.forEach(entry -> { | |
graphics.setColor(new Color(255, 128, 128, 64)); | |
graphics.fill(entry.getKey()); | |
}); | |
} | |
static void renderPlayerCard(Graphics2D graphics, Game game, List<Entry<Shape, Runnable>> callbacks) { | |
final int RADIUS = 20; | |
final int INTERVAL = 40; | |
final int WIDTH = 100; | |
final int HEIGHT = 200; | |
List<Card> usableCards = game.usableCards(); | |
for (int j = 0; j < 4; j++) { | |
if (j == game.currentPlayer) { | |
graphics.setColor(new Color(128, 160, 128)); | |
Rectangle pass = new Rectangle(200, 675, 50, 20); | |
graphics.fill(pass); | |
callbacks.add(Map.entry(graphics.getTransform().createTransformedShape(pass), game::pass)); | |
graphics.setColor(new Color(16, 16, 16)); | |
graphics.drawString("PASS", 205, 692); | |
} | |
graphics.setColor(new Color(16, 16, 16)); | |
String message = String.format("Player %d (pass left: %d)", j, game.pass[j]); | |
graphics.drawString(message, 5, 695); | |
for (int i = 0; i < game.cards.get(j).size(); i++) { | |
Card card = game.cards.get(j).get(i); | |
AffineTransform transform = AffineTransform.getTranslateInstance(INTERVAL * i, 700); | |
graphics.transform(transform); | |
graphics.setColor(Color.white); | |
graphics.fillRoundRect(0, 0, WIDTH, HEIGHT, RADIUS, RADIUS); | |
graphics.setColor(Color.black); | |
graphics.drawRoundRect(0, 0, WIDTH, HEIGHT, RADIUS, RADIUS); | |
graphics.setColor(card.suit.color); | |
graphics.drawString(card.toString(), 5, 20); | |
if (j != game.currentPlayer) { | |
graphics.setColor(new Color(0, 0, 0, 128)); | |
graphics.fillRoundRect(0, 0, WIDTH, HEIGHT, RADIUS, RADIUS); | |
} | |
if (j == game.currentPlayer && usableCards.contains(card)) { | |
int width = i + 1 == game.cards.get(j).size() ? WIDTH : INTERVAL; | |
Rectangle rectangle = new Rectangle(0, 0, width, HEIGHT); | |
Shape shape = graphics.getTransform().createTransformedShape(rectangle); | |
callbacks.add(Map.entry(shape, () -> game.useCard(card))); | |
} | |
Try.wrap(() -> graphics.transform(transform.createInverse())).run(); | |
} | |
graphics.rotate(-Math.PI / 2); | |
graphics.translate(-800, 0); | |
} | |
} | |
static void renderBoard(Graphics2D graphics, Game game) { | |
final int RADIUS = 20; | |
final int INTERVAL_X = 4; | |
final int INTERVAL_Y = 4; | |
final int WIDTH = 40; | |
final int HEIGHT = 60; | |
AffineTransform transformOuter = AffineTransform.getTranslateInstance( | |
SIZE.width / 2.0 - WIDTH * 6.5 - INTERVAL_X * 6, | |
SIZE.height / 2.0 - HEIGHT * 2 - INTERVAL_Y * 1.5 | |
); | |
graphics.transform(transformOuter); | |
for (int j = 0; j < 4; j++) { | |
Suit suit = Suit.values()[j]; | |
for (int i = game.board[j][0] - 1; i <= game.board[j][1] - 1; i++) { | |
AffineTransform transformInner = AffineTransform.getTranslateInstance( | |
(WIDTH + INTERVAL_X) * i, | |
(HEIGHT + INTERVAL_Y) * j | |
); | |
graphics.transform(transformInner); | |
graphics.setColor(Color.white); | |
graphics.fillRoundRect(0, 0, WIDTH, HEIGHT, RADIUS, RADIUS); | |
graphics.setColor(Color.black); | |
graphics.drawRoundRect(0, 0, WIDTH, HEIGHT, RADIUS, RADIUS); | |
Card card = new Card(suit, i + 1); | |
graphics.setColor(suit.color); | |
graphics.drawString(card.toString(), 3, 20); | |
Try.wrap(() -> graphics.transform(transformInner.createInverse())).run(); | |
} | |
} | |
Try.wrap(() -> graphics.transform(transformOuter.createInverse())).run(); | |
} | |
static class Game { | |
int currentPlayer = 0; | |
int[][] board = {{ 7, 7 }, { 7, 7 }, { 7, 7 }, { 7, 7 }}; | |
int[] pass = { 3, 3, 3, 3 }; | |
boolean gameOver = false; | |
List<List<Card>> cards; | |
Game() { | |
AtomicInteger counter = new AtomicInteger(); | |
List<Card> deck = new ArrayList<>(deck()); | |
Collections.shuffle(deck); | |
cards = deck | |
.stream() | |
.collect(Collectors.groupingBy(it -> counter.getAndIncrement() / 12)) | |
.values() | |
.stream() | |
.toList(); | |
} | |
List<Card> usableCards() { | |
Predicate<Card> isUsable = card -> | |
board[card.suit.ordinal()][0] - 1 == card.number || | |
board[card.suit.ordinal()][1] + 1 == card.number; | |
return cards | |
.get(currentPlayer) | |
.stream() | |
.filter(isUsable) | |
.toList(); | |
} | |
void useCard(Card card) { | |
if (!usableCards().contains(card)) return; | |
int[] boardLocal = board[card.suit.ordinal()]; | |
if (boardLocal[0] - 1 == card.number) | |
boardLocal[0] = card.number; | |
if (boardLocal[1] + 1 == card.number) | |
boardLocal[1] = card.number; | |
cards.get(currentPlayer).remove(card); | |
if (cards.get(currentPlayer).isEmpty()) | |
gameOver = true; | |
currentPlayer = ++currentPlayer % 4; | |
} | |
void pass() { | |
if (--pass[currentPlayer] < 0) | |
gameOver = true; | |
currentPlayer = ++currentPlayer % 4; | |
} | |
} | |
record Card(Suit suit, int number) { | |
static final String[] CHARACTERS = {"A", "2", "3", "4", "5", "6", "7", "8", "9", "10", "J", "Q", "K"}; | |
public String toString() { | |
return suit.character + CHARACTERS[number - 1]; | |
} | |
} | |
static List<Card> deck() { | |
Function<Suit, Stream<Card>> mapper = suit -> IntStream | |
.rangeClosed(1, 13) | |
.filter(i -> i != 7) | |
.mapToObj(i -> new Card(suit, i)); | |
return Stream | |
.of(Suit.values()) | |
.flatMap(mapper) | |
.toList(); | |
} | |
enum Suit { | |
SPADE(Color.black, "♠"), | |
HEART(Color.red, "♡"), | |
CLUB(Color.black, "♧"), | |
DIAMOND(Color.red, "♢"); | |
final Color color; | |
final String character; | |
Suit(Color color, String character) { | |
this.color = color; | |
this.character = character; | |
} | |
} | |
static class Try { | |
static Runnable wrap(RunnableWithException runnable) { | |
return () -> { | |
try { | |
runnable.run(); | |
} catch (Exception e) { | |
throw new RuntimeException(e); | |
} | |
}; | |
} | |
} | |
interface RunnableWithException { | |
void run() throws Exception; | |
} | |
} |
seven.mp4
※ 動画に映ってる黒い十字線はレイアウト調整用で、本 gist に含まれません。
Kotlin 版も作りました
import java.awt.*
import javax.swing.*
import java.awt.RenderingHints.*
import java.awt.event.*
import java.awt.geom.AffineTransform
val SIZE = Dimension(800, 800)
typealias ClickCallbacks = MutableList<Pair<Shape, () -> Unit>>
enum class Suit(val color: Color, val character: String) {
SPADE(Color.black, "♠"),
HEART(Color.red, "♡"),
CLUB(Color.black, "♧"),
DIAMOND(Color.red, "♢")
}
data class Card(val suit: Suit, val number: Int) {
val CHARACTERS = arrayOf("A", "2", "3", "4", "5", "6", "7", "8", "9", "10", "J", "Q", "K")
override fun toString(): String {
return suit.character + CHARACTERS[number - 1]
}
}
data class Game(
var currentPlayer: Int = 0,
val passLeft: IntArray = intArrayOf(3, 3, 3, 3),
val board: Array<Array<Int>> = arrayOf(arrayOf(7, 7), arrayOf(7, 7), arrayOf(7, 7), arrayOf(7, 7)),
var gameOver: Boolean = false,
val cards: MutableList<List<Card>> = deck().shuffled().chunked(12).toMutableList()
)
fun Game.usableCards() = cards[currentPlayer].filter {
board[it.suit.ordinal][0] - 1 == it.number ||
board[it.suit.ordinal][1] + 1 == it.number
}
fun Game.useCard(card: Card) {
if (!usableCards().contains(card)) return
val boardLocal = board[card.suit.ordinal]
if (boardLocal[0] - 1 == card.number)
boardLocal[0] = card.number
if (boardLocal[1] + 1 == card.number)
boardLocal[1] = card.number
cards[currentPlayer] = cards[currentPlayer] - card
if (cards[currentPlayer].isEmpty()) gameOver = true
currentPlayer = ++currentPlayer % 4
}
fun Game.pass() {
if (--passLeft[currentPlayer] < 0) gameOver = true
currentPlayer = ++currentPlayer % 4
}
fun deck() = Suit
.entries
.flatMap { suit -> (1..13).filter { it != 7 }.map { Card(suit, it) } }
fun main() {
frame(panel())
}
fun frame(panel: JPanel) = with (JFrame("七並べ")) {
defaultCloseOperation = JFrame.EXIT_ON_CLOSE
isResizable = false
contentPane = panel
setLocationRelativeTo(null)
pack()
isVisible = true
}
fun panel(): JPanel {
var game = Game()
val clickCallbacks: ClickCallbacks = mutableListOf()
val panel = object: JPanel() {
override fun paintComponent(graphics: Graphics) {
(graphics as Graphics2D).render(game, clickCallbacks)
}
}
panel.addMouseListener(object: MouseAdapter() {
override fun mousePressed(event: MouseEvent) {
if (event.button == 3) {
game = Game()
panel.repaint()
return
}
for (callback in clickCallbacks) {
if (callback.first.contains(event.point)) {
callback.second()
panel.repaint()
break
}
}
}
})
panel.preferredSize = SIZE
return panel
}
fun Graphics2D.render(game: Game, callbacks: ClickCallbacks) {
callbacks.clear()
setRenderingHint(KEY_ANTIALIASING, VALUE_ANTIALIAS_ON)
setRenderingHint(KEY_TEXT_ANTIALIASING, VALUE_TEXT_ANTIALIAS_ON)
color = Color(64, 128, 64)
fillRect(0, 0, SIZE.width, SIZE.height)
font = font.deriveFont(16f)
renderPlayers(game, callbacks)
renderBoard(game)
if (game.gameOver) {
callbacks.clear()
color = Color(0, 0, 0, 128)
fillRect(0, 0, SIZE.width, SIZE.height)
color = Color.WHITE
drawString("Right click to restart", 100, 480)
font = font.deriveFont(256f)
drawString("Game", 40, 200)
drawString("Over", 40, 450)
return
}
callbacks.forEach {
color = Color(255, 128, 128, 64)
fill(it.first)
}
}
fun Graphics2D.renderPlayers(game: Game, callbacks: ClickCallbacks) {
val RADIUS = 20
val INTERVAL = 40
val WIDTH = 100
val HEIGHT = 200
val usableCards = game.usableCards()
for (i in 0..<4) {
val currentPlayer = i == game.currentPlayer
if (currentPlayer) {
Rectangle(200, 675, 50, 20).also {
color = Color(128, 160, 128)
fill(it)
callbacks.addLast(Pair(transform.createTransformedShape(it), game::pass))
color = Color(16, 16, 16)
drawString("PASS", 205, 692)
}
}
color = Color(16, 16, 16)
drawString("Player $i (pass left: ${game.passLeft[i]})", 5, 695)
for (j in 0..<game.cards[i].size) {
val card = game.cards[i][j]
val transform = AffineTransform.getTranslateInstance((INTERVAL * j).toDouble(), 700.0)
transform(transform)
color = Color.WHITE
fillRoundRect(0, 0, WIDTH, HEIGHT, RADIUS, RADIUS)
color = Color.BLACK
drawRoundRect(0, 0, WIDTH, HEIGHT, RADIUS, RADIUS)
color = card.suit.color
drawString(card.toString(), 5, 20)
if (!currentPlayer) {
color = Color(0, 0, 0, 128)
fillRoundRect(0, 0, WIDTH, HEIGHT, RADIUS, RADIUS)
}
if (currentPlayer && usableCards.contains(card)) {
val width = if (j + 1 == game.cards[i].size) WIDTH else INTERVAL
val shape = getTransform().createTransformedShape(Rectangle(0, 0, width, HEIGHT))
callbacks.addLast(Pair(shape) { game.useCard(card) })
}
transform(transform.createInverse())
}
rotate(-Math.PI / 2)
translate(-SIZE.width, 0)
}
}
fun Graphics2D.renderBoard(game: Game) {
val RADIUS = 20
val INTERVAL_X = 4
val INTERVAL_Y = 4
val WIDTH = 40
val HEIGHT = 60
val outerTransform = AffineTransform.getTranslateInstance(
SIZE.width / 2.0 - WIDTH * 6.5 - INTERVAL_X * 6,
SIZE.height / 2.0 - HEIGHT * 2 - INTERVAL_Y * 1.5
)
transform(outerTransform)
for (i in 0..<4) {
val suit = Suit.entries[i]
for (j in game.board[i][0] - 1..<game.board[i][1]) {
val innerTransform = AffineTransform.getTranslateInstance(
((WIDTH + INTERVAL_X) * j).toDouble(),
((HEIGHT + INTERVAL_Y) * i).toDouble(),
)
transform(innerTransform)
color = Color.WHITE
fillRoundRect(0, 0, WIDTH, HEIGHT, RADIUS, RADIUS)
color = Color.BLACK
drawRoundRect(0, 0, WIDTH, HEIGHT, RADIUS, RADIUS)
color = suit.color
drawString(Card(suit, j + 1).toString(), 3, 20)
transform(innerTransform.createInverse())
}
}
transform(outerTransform.createInverse())
}
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
元ネタ(?)
https://twitter.com/kis/status/1773111925095702930