Skip to content

Instantly share code, notes, and snippets.

@PlusLake
Last active April 6, 2024 09:42
Show Gist options
  • Save PlusLake/755f1610303b94fd7225c7e4a9bed4ee to your computer and use it in GitHub Desktop.
Save PlusLake/755f1610303b94fd7225c7e4a9bed4ee to your computer and use it in GitHub Desktop.
七並べ(Java Swing)
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;
}
}
@PlusLake
Copy link
Author

@PlusLake
Copy link
Author

seven.mp4

※ 動画に映ってる黒い十字線はレイアウト調整用で、本 gist に含まれません。

@PlusLake
Copy link
Author

PlusLake commented Apr 6, 2024

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