-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: use golang to generate connect4 image
- Loading branch information
Showing
16 changed files
with
380 additions
and
152 deletions.
There are no files selected for viewing
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,5 @@ | ||
# Application | ||
draft/ | ||
data/ | ||
|
||
.DS_Store | ||
|
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,32 +1,50 @@ | ||
# syntax=docker/dockerfile-upstream:master-labs | ||
|
||
FROM python:3.12-alpine AS build | ||
ARG EXTRA_DEBUG="" | ||
ARG UV_EXTRA_ARGS="" | ||
COPY --from=ghcr.io/astral-sh/uv:latest /uv /bin/uv | ||
WORKDIR /app | ||
RUN --mount=type=cache,target=/var/cache/apk \ | ||
--mount=type=cache,target=/root/.cache/uv \ | ||
--mount=type=bind,source=uv.lock,target=uv.lock \ | ||
--mount=type=bind,source=pyproject.toml,target=pyproject.toml \ | ||
--mount=type=bind,source=python/uv.lock,target=uv.lock \ | ||
--mount=type=bind,source=python/pyproject.toml,target=pyproject.toml \ | ||
: \ | ||
&& apk update && apk add gcc musl-dev linux-headers git \ | ||
&& uv sync --no-dev --locked $EXTRA_DEBUG \ | ||
&& uv sync --no-dev --locked $UV_EXTRA_ARGS \ | ||
&& : | ||
|
||
FROM golang:1.23-alpine AS buildgo | ||
WORKDIR /app | ||
RUN --mount=type=cache,target=/var/cache/apk \ | ||
--mount=type=cache,target=/go/pkg/mod/ \ | ||
--mount=type=bind,source=golang/go.sum,target=go.sum \ | ||
--mount=type=bind,source=golang/go.mod,target=go.mod \ | ||
--mount=type=bind,source=golang/src,target=src \ | ||
: \ | ||
&& apk update && apk add gcc musl-dev \ | ||
&& go mod download -x \ | ||
&& CGO_ENABLED=1 go build -o connect4img.so -buildmode=c-shared src/connect4img.go \ | ||
&& : | ||
|
||
FROM python:3.12-alpine AS base | ||
# https://docs.docker.com/reference/dockerfile/#copy---parents | ||
RUN apk add --no-cache musl-dev libc6-compat file | ||
WORKDIR /app | ||
COPY --from=buildgo /app/connect4img.so /app/shared/ | ||
COPY --parents --from=build /app/.venv / | ||
COPY --parents ./resources ./ | ||
COPY ./src ./ | ||
COPY ./python/src ./ | ||
ENV PATH="/app/.venv/bin:$PATH" | ||
ENV PYTHONUNBUFFERED=0 | ||
|
||
FROM base AS production | ||
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "80"] | ||
|
||
FROM base AS debug | ||
COPY ./debug_pages /app/debug_pages | ||
COPY --parents ./scripts/readme_generator.py ./templates/README.md ./ | ||
RUN python3 scripts/readme_generator.py http://localhost/ | ||
ENV DEBUG=1 | ||
ENV LOG_LEVEL=DEBUG | ||
RUN ls shared | ||
RUN file shared/connect4img.so | ||
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "80", "--reload"] |
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
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
module readmeconnect4 | ||
|
||
go 1.23 | ||
|
||
toolchain go1.23.3 | ||
|
||
require ( | ||
git.sr.ht/~sbinet/gg v0.6.0 // indirect | ||
github.com/campoy/embedmd v1.0.0 // indirect | ||
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect | ||
github.com/pmezard/go-difflib v1.0.0 // indirect | ||
golang.org/x/image v0.23.0 // indirect | ||
golang.org/x/text v0.21.0 // indirect | ||
) |
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
git.sr.ht/~sbinet/gg v0.6.0 h1:RIzgkizAk+9r7uPzf/VfbJHBMKUr0F5hRFxTUGMnt38= | ||
git.sr.ht/~sbinet/gg v0.6.0/go.mod h1:uucygbfC9wVPQIfrmwM2et0imr8L7KQWywX0xpFMm94= | ||
github.com/campoy/embedmd v1.0.0 h1:V4kI2qTJJLf4J29RzI/MAt2c3Bl4dQSYPuflzwFH2hY= | ||
github.com/campoy/embedmd v1.0.0/go.mod h1:oxyr9RCiSXg0M3VJ3ks0UGfp98BpSSGr0kpiX3MzVl8= | ||
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g= | ||
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= | ||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= | ||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= | ||
golang.org/x/image v0.23.0 h1:HseQ7c2OpPKTPVzNjG5fwJsOTCiiwS4QdsYi5XU6H68= | ||
golang.org/x/image v0.23.0/go.mod h1:wJJBTdLfCCf3tiHa1fNxpZmUI4mmoZvwMCPP0ddoNKY= | ||
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= | ||
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= |
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,116 @@ | ||
package main | ||
|
||
import ( | ||
"image/color" | ||
"log" | ||
|
||
"git.sr.ht/~sbinet/gg" | ||
"golang.org/x/image/font" | ||
|
||
"C" | ||
) | ||
|
||
const TOKEN_DIAMETER int = 70 | ||
const INTERSPACE int = 10 | ||
const BOX_HEIGHT int = 140 | ||
const BOX_BORDER int = 10 | ||
|
||
const FONT_SIZE int = 40 | ||
|
||
var BG_COLOR = color.RGBA{100, 100, 255, 255} | ||
var BOX_COLOR = color.RGBA{70, 70, 255, 255} | ||
var TOKENS_COLORS = []color.RGBA{ | ||
{255, 255, 255, 255}, | ||
{255, 100, 100, 255}, | ||
{255, 255, 100, 255}, | ||
} | ||
var HIGHLIGHT_WIN_COLOR = color.RGBA{100, 255, 100, 255} | ||
|
||
var FACE font.Face | ||
|
||
func init() { | ||
var err error | ||
FACE, err = gg.LoadFontFace("./resources/fonts/Roboto-Regular.ttf", float64(FONT_SIZE)) | ||
if err != nil { | ||
log.Fatalf("could not load font face: %+v", err) | ||
} | ||
} | ||
|
||
//export GenerateBoard | ||
func GenerateBoard(board [][]int, isOver bool, winner int, turn int, winPositions [][]int) { | ||
log.Print(winPositions) | ||
const width = 7*(TOKEN_DIAMETER+INTERSPACE) + INTERSPACE | ||
const heigh = 6*(TOKEN_DIAMETER+INTERSPACE) + INTERSPACE + BOX_HEIGHT | ||
dc := gg.NewContext( | ||
width, heigh, | ||
) | ||
dc.SetColor(BG_COLOR) | ||
dc.DrawRectangle(0, 0, float64(width), float64(heigh)) | ||
dc.Fill() | ||
|
||
// Draw message | ||
var msg string | ||
if isOver { | ||
if winner == 0 { | ||
msg = "It's a tie!" | ||
} else { | ||
if turn == 1 { | ||
msg = "Winner : red" | ||
} else { | ||
msg = "Winner : yellow" | ||
} | ||
} | ||
} else { | ||
if turn == 1 { | ||
msg = "Player turn : red" | ||
} else { | ||
msg = "Player turn : yellow" | ||
} | ||
} | ||
|
||
dc.SetColor(color.Black) | ||
dc.SetFontFace(FACE) | ||
dc.DrawStringAnchored( | ||
msg, | ||
float64((7*(TOKEN_DIAMETER+INTERSPACE)+INTERSPACE)/2), | ||
float64(6*(TOKEN_DIAMETER+INTERSPACE)+INTERSPACE+(BOX_HEIGHT)/2), | ||
0.5, | ||
0.5, | ||
) | ||
|
||
// Draw tokens | ||
for i := 0; i < 6; i++ { | ||
for j := 0; j < 7; j++ { | ||
caseVal := board[i][j] | ||
x := float64(INTERSPACE + j*(TOKEN_DIAMETER+INTERSPACE)) | ||
y := float64(INTERSPACE + i*(TOKEN_DIAMETER+INTERSPACE)) | ||
|
||
if contains(winPositions, [2]int{i, j}) { | ||
log.Println(true) | ||
dc.SetColor(HIGHLIGHT_WIN_COLOR) | ||
dc.DrawCircle(x+float64(TOKEN_DIAMETER)/2, y+float64(TOKEN_DIAMETER)/2, float64(TOKEN_DIAMETER)/2+float64(INTERSPACE)/2) | ||
dc.Fill() | ||
} | ||
|
||
dc.SetColor(TOKENS_COLORS[caseVal]) | ||
dc.DrawCircle(x+float64(TOKEN_DIAMETER)/2, y+float64(TOKEN_DIAMETER)/2, float64(TOKEN_DIAMETER)/2) | ||
dc.Fill() | ||
} | ||
} | ||
|
||
err := dc.SavePNG("./data/connect4.png") // TODO | ||
if err != nil { | ||
log.Fatalf("could not save to file: %+v", err) | ||
} | ||
} | ||
|
||
func contains(points [][]int, point [2]int) bool { | ||
for _, p := range points { | ||
if [2]int(p) == point { | ||
return true | ||
} | ||
} | ||
return false | ||
} | ||
|
||
func main() {} |
File renamed without changes.
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
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
import os | ||
from functools import partial | ||
from pathlib import Path | ||
from time import time | ||
|
||
from connect4 import ColumnFull, Connect4 | ||
from fastapi import APIRouter, Query | ||
from fastapi.responses import FileResponse, RedirectResponse | ||
|
||
from .go_library import generate_board | ||
|
||
GITHUB_PROFILE_URL = os.environ["GITHUB_PROFILE_URL"] | ||
IMAGE_PATH = Path("./data/connect4.png") | ||
|
||
|
||
last_play_time = time() | ||
c4 = Connect4() | ||
router = APIRouter(prefix="/connect4", on_startup=[partial(generate_board, c4)]) | ||
|
||
|
||
@router.get("/image") | ||
def get_image(): | ||
return FileResponse(IMAGE_PATH, headers={"Cache-Control": "no-cache, max-age=0"}) | ||
|
||
|
||
@router.get("/play") | ||
def play(column: int = Query(title="The column ID you want to play to.", ge=0, le=6)): | ||
global last_play_time | ||
|
||
if not c4.is_over: | ||
try: | ||
c4.play(column) | ||
except ColumnFull: | ||
pass | ||
else: | ||
generate_board(c4) | ||
last_play_time = time() | ||
return RedirectResponse(GITHUB_PROFILE_URL) | ||
|
||
|
||
@router.get("/reset") | ||
def reset(): | ||
if not c4.is_over and time() - last_play_time < 300: | ||
return RedirectResponse(GITHUB_PROFILE_URL) | ||
|
||
c4.reset() | ||
generate_board(c4) | ||
return RedirectResponse(GITHUB_PROFILE_URL) |
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,83 @@ | ||
from __future__ import annotations | ||
|
||
import os | ||
from collections.abc import Sequence | ||
from ctypes import CDLL, POINTER, Structure, c_longlong, c_uint8, c_void_p | ||
from typing import TYPE_CHECKING, ClassVar | ||
|
||
if TYPE_CHECKING: | ||
from connect4 import Connect4 | ||
|
||
|
||
_path = os.path.abspath("./golang/connect4img.so") | ||
|
||
try: | ||
_lib = CDLL(_path, mode=0x8) | ||
print("Library loaded successfully!") | ||
except OSError as e: | ||
print(f"Failed to load library: {e}") | ||
raise | ||
except UnicodeDecodeError as e: | ||
print(f"Unicode decode error: {e}") | ||
raise | ||
|
||
|
||
def _slice_type(type: type): | ||
class GoSlice(Structure): | ||
_fields_: ClassVar = [("data", POINTER(type)), ("len", c_longlong), ("cap", c_longlong)] | ||
|
||
return GoSlice | ||
|
||
|
||
_GoIntSlice = _slice_type(c_void_p) | ||
_GoIntSliceSlice = _slice_type(_GoIntSlice) | ||
|
||
|
||
_lib.GenerateBoard.argtypes = [_GoIntSliceSlice, c_uint8, c_longlong, c_longlong, _GoIntSliceSlice] | ||
_lib.GenerateBoard.restype = None | ||
|
||
|
||
def _to_go_int_slice_slice(value: Sequence[Sequence[int]]): | ||
inner_length = len(value[0]) | ||
if not all(len(row) == inner_length for row in value): | ||
raise ValueError("All rows must have the same length") | ||
|
||
rows = [_to_go_int_slice(row) for row in value] | ||
|
||
length = len(value) | ||
return _GoIntSliceSlice((_GoIntSlice * length)(*rows), length, length) | ||
|
||
|
||
def _to_go_int_slice(value: Sequence[int]): | ||
length = len(value) | ||
return _GoIntSlice((c_void_p * length)(*value), length, length) | ||
|
||
|
||
def _to_uint8(value: bool): | ||
return (c_uint8)(value) | ||
|
||
|
||
def _to_longlong(value: int): | ||
return (c_longlong)(value) | ||
|
||
|
||
def _connect4_export_types(c4: Connect4): | ||
board = _to_go_int_slice_slice([[p.value for p in row] for row in c4.board]) | ||
|
||
is_over = _to_uint8(c4.is_over) | ||
winner = _to_longlong(-1 if c4.winner is None else c4.winner.value) | ||
turn = _to_longlong(c4.turn.value) | ||
|
||
win_points = c4._win_points # type: ignore | ||
win_pos = len(win_points) | ||
winner_positions = _GoIntSliceSlice( | ||
(_GoIntSlice * win_pos)(*[_GoIntSlice((c_void_p * 2)(*xy), 2, 2) for xy in win_points]), | ||
win_pos, | ||
win_pos, | ||
) | ||
|
||
return board, is_over, winner, turn, winner_positions | ||
|
||
|
||
def generate_board(c4: Connect4): | ||
_lib.GenerateBoard(*_connect4_export_types(c4)) |
File renamed without changes.
File renamed without changes.
Oops, something went wrong.