diff --git a/.dockerignore b/.dockerignore
deleted file mode 100644
index 07a1c07..0000000
--- a/.dockerignore
+++ /dev/null
@@ -1,3 +0,0 @@
-.git
-.gitignore
-static
\ No newline at end of file
diff --git a/Dockerfile b/Dockerfile
index e69de29..cd293b4 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -0,0 +1,42 @@
+# ============================
+# Стадия 1: Сборка
+# ============================
+FROM python:3.12-slim AS builder
+
+# Устанавливаем Go
+RUN apt-get update && \
+ apt-get install -y --no-install-recommends golang-go && \
+ rm -rf /var/lib/apt/lists/*
+
+# Копируем проект
+WORKDIR /app
+COPY . .
+
+# Генерация кода
+RUN python generate.py server ./server/toe/
+
+# Статическая сборка Go
+ENV CGO_ENABLED=0 \
+ GOOS=linux \
+ GOARCH=amd64
+
+# Если ты на Mac M1/M2/M3 — ставь arm64:
+# ENV GOARCH=arm64
+
+RUN go build -o /app/main ./server/main.go
+
+
+# ============================
+# Стадия 2: Запуск
+# ============================
+FROM debian:bookworm-slim
+
+# Копируем бинарник
+COPY --from=builder /app/main /app/main
+
+# Делаем исполняемым
+RUN chmod +x /app/main
+
+WORKDIR /app
+
+CMD ["/app/main"]
diff --git a/README.md b/README.md
index 0122c13..7f00d78 100644
--- a/README.md
+++ b/README.md
@@ -1,5 +1,5 @@

-# oxTicTacToe Generator
+# oxTicTacToe
## Описание
@@ -16,7 +16,7 @@
## Требования
-- Python 3.x
+- Python 3.12
- Golang 1.25
## Установка
diff --git a/generate.py b/generate.py
index b6b701c..25a4b9a 100644
--- a/generate.py
+++ b/generate.py
@@ -56,6 +56,46 @@ document.addEventListener('mouseover', e => {
}
});
""")
+websocket = minify("""
+(function() {
+ const ws = new WebSocket(`${window.location.origin.replace(/^http(s?)/, 'ws$1')}/ws${window.location.search}`);
+ ws.onopen = function(event) {
+ console.log('WebSocket connected');
+ };
+ ws.onmessage = function(event) {
+ console.log('Received:', event.data);
+ if (document.getElementsByName("g")[0].src != event.data) {
+ document.getElementsByName("g")[0].src = event.data;
+ }
+ };
+ ws.onclose = function(event) {
+ console.log('WebSocket disconnected:', event.code, event.reason);
+ };
+ ws.onerror = function(error) {
+ console.error('WebSocket error:', error);
+ };
+ window.wsSend = function(text) {
+ if (ws.readyState === WebSocket.OPEN) {
+ ws.send(text);
+ console.log('Sent:', text);
+ } else {
+ console.error('WebSocket not connected. Current status:', ws.readyState);
+ }
+ };
+})();
+
+document.addEventListener('DOMContentLoaded', function() {
+ const iframe = document.getElementsByName("g")[0];
+ if (iframe) {
+ iframe.addEventListener('load', () => {
+ iframe.contentDocument.querySelectorAll(`a[href="../../../multiplayer"]`).forEach(link => link.remove());
+ wsSend(iframe.contentWindow.location.href);
+ });
+ } else {
+ console.error('Iframe with name "g" not found.');
+ }
+});
+""")
pages = {
'index':
minify(f"""
@@ -69,6 +109,7 @@ pages = {
scripts = {
's': apply_css,
+ 'w': websocket,
'p': preload,
'a': (apply_css+preload)
}
diff --git a/go.mod b/go.mod
index 1eaa642..6a7bf81 100644
--- a/go.mod
+++ b/go.mod
@@ -6,6 +6,7 @@ require (
github.com/gin-contrib/gzip v1.2.5
github.com/gin-gonic/gin v1.11.0
github.com/google/uuid v1.6.0
+ github.com/gorilla/websocket v1.5.3
)
require (
diff --git a/go.sum b/go.sum
index faa0e78..88f9ce9 100644
--- a/go.sum
+++ b/go.sum
@@ -34,6 +34,8 @@ github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
+github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
diff --git a/server/main.go b/server/main.go
index cef1774..b4a5904 100644
--- a/server/main.go
+++ b/server/main.go
@@ -11,11 +11,14 @@ import (
"github.com/gin-contrib/gzip"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
+ "github.com/gorilla/websocket"
)
//go:embed toe
var staticFiles embed.FS
+var waitingPlayers = make(map[string][]*websocket.Conn)
+
func SetCustomContentType() gin.HandlerFunc {
return func(c *gin.Context) {
requestPath := c.Request.URL.Path
@@ -35,6 +38,70 @@ func SetCustomContentType() gin.HandlerFunc {
}
}
+func multiplayerHandler(c *gin.Context) {
+ if c.GetHeader("Accept") == "*/*" {
+ c.String(http.StatusTeapot, "Preload not allowed")
+ }
+ if c.Query("g") == "" {
+ u, err := uuid.NewV7()
+ if err != nil {
+ fmt.Println("Error:", err)
+ return
+ }
+ c.Redirect(http.StatusPermanentRedirect, "/multiplayer?g="+u.String())
+ }
+ c.Header("Content-Type", "text/html; charset=UTF-8")
+ c.String(http.StatusOK, "")
+}
+
+var upgrader = websocket.Upgrader{
+ CheckOrigin: func(r *http.Request) bool {
+ origin := r.Header.Get("Origin")
+ host := r.Host
+ return origin == "http://"+host || origin == "https://"+host
+ },
+}
+
+func wsHandler(c *gin.Context) {
+ game_id := c.Query("g")
+ if game_id == "" {
+ c.String(http.StatusTeapot, "No game id")
+ return
+ }
+
+ conn, err := upgrader.Upgrade(c.Writer, c.Request, nil)
+ if err != nil {
+ fmt.Println("Upgrade error:", err)
+ return
+ }
+ fmt.Println("New client connected")
+
+ waitingPlayers[game_id] = append(waitingPlayers[game_id], conn)
+ defer func() {
+ conn.Close()
+ fmt.Println("Client disconnected")
+ }()
+
+ for {
+ messageType, message, err := conn.ReadMessage()
+ if err != nil {
+ fmt.Println("Read error:", err)
+ break
+ }
+ fmt.Printf("Received: %s\n", message)
+
+ // Рассылка всем клиентам
+ for _, c := range waitingPlayers[game_id] {
+ if c != conn {
+ err := c.WriteMessage(messageType, message)
+ if err != nil {
+ fmt.Println("Broadcast error:", err)
+ }
+ }
+ }
+ }
+}
+
var favicon = ""
func main() {
@@ -49,19 +116,8 @@ func main() {
router.GET("/favicon.ico", func(c *gin.Context) {
c.String(http.StatusOK, favicon)
})
- router.GET("/multiplayer", multiplayer)
+ router.GET("/ws", wsHandler)
+ router.GET("/multiplayer", multiplayerHandler)
router.StaticFS("/tic/tac", embeddedFilesSystem)
router.Run(":8080")
}
-
-func multiplayer(c *gin.Context) {
- if c.GetHeader("Accept") == "*/*" {
- c.String(http.StatusTeapot, "Preload not allowed")
- }
- u, err := uuid.NewV7()
- if err != nil {
- fmt.Println("Ошибка:", err)
- return
- }
- c.String(http.StatusOK, fmt.Sprintf("ToDo\nUUID7: %s", u.String()))
-}