mirror of
https://github.com/Aviortheking/games.git
synced 2025-04-25 12:12:08 +00:00
TicTacTicTac c'est un TicTacToe 😄
Signed-off-by: Avior <florian.bouillon@delta-wings.net>
This commit is contained in:
parent
095dfddfb8
commit
7384ffc5b7
BIN
public/assets/tictactoe/O.png
Normal file
BIN
public/assets/tictactoe/O.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 594 B |
BIN
public/assets/tictactoe/X.png
Normal file
BIN
public/assets/tictactoe/X.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 542 B |
BIN
public/assets/tictactoe/bip.wav
Normal file
BIN
public/assets/tictactoe/bip.wav
Normal file
Binary file not shown.
BIN
public/assets/tictactoe/victory.wav
Normal file
BIN
public/assets/tictactoe/victory.wav
Normal file
Binary file not shown.
245
src/GameEngine.ts
Normal file
245
src/GameEngine.ts
Normal file
@ -0,0 +1,245 @@
|
||||
/* eslint-disable max-classes-per-file */
|
||||
export default class GameEngine {
|
||||
public ctx: CanvasRenderingContext2D
|
||||
public canvas: HTMLCanvasElement
|
||||
public caseSize: [number, number] = [1, 1]
|
||||
public cursor: {
|
||||
x: number
|
||||
y: number
|
||||
isDown: boolean
|
||||
wasDown: boolean
|
||||
} = {
|
||||
x: 0,
|
||||
y: 0,
|
||||
isDown: false,
|
||||
wasDown: false
|
||||
}
|
||||
private currentScene?: Scene
|
||||
private isRunning = false
|
||||
|
||||
public constructor(
|
||||
private id: string,
|
||||
private options?: {
|
||||
caseCount?: number | [number, number]
|
||||
background?: string
|
||||
}
|
||||
) {
|
||||
const canvas = document.querySelector<HTMLCanvasElement>(id)
|
||||
if (!canvas) {
|
||||
throw new Error('Error, canvas not found!')
|
||||
}
|
||||
this.canvas = canvas
|
||||
if (this.options?.caseCount) {
|
||||
this.caseSize = [
|
||||
// @ts-expect-error idc
|
||||
this.canvas.width / ((typeof this.options.caseCount) !== 'number' ? this.options.caseCount[0] : this.options.caseCount ),
|
||||
// @ts-expect-error idc2 lol
|
||||
this.canvas.height / ((typeof this.options.caseCount) !== 'number' ? this.options.caseCount[1] : this.options.caseCount)
|
||||
]
|
||||
}
|
||||
|
||||
const ctx = canvas.getContext('2d')
|
||||
if (!ctx) {
|
||||
throw new Error('Error, Context could not get found!')
|
||||
}
|
||||
ctx.imageSmoothingEnabled = false
|
||||
this.ctx = ctx
|
||||
}
|
||||
|
||||
public start() {
|
||||
if (this.isRunning) {
|
||||
console.warn('Game is already running')
|
||||
return
|
||||
}
|
||||
this.isRunning = true
|
||||
requestAnimationFrame(() => {
|
||||
this.update()
|
||||
})
|
||||
document.addEventListener('mousemove', (ev) => {
|
||||
this.cursor.x = ev.clientX
|
||||
this.cursor.y = ev.clientY
|
||||
if (this.cursor.isDown) {
|
||||
this.cursor.wasDown = true
|
||||
}
|
||||
})
|
||||
document.addEventListener('mousedown', () => {
|
||||
this.cursor.isDown = true
|
||||
})
|
||||
document.addEventListener('mouseup', () => {
|
||||
this.cursor.isDown = false
|
||||
this.cursor.wasDown = false
|
||||
})
|
||||
}
|
||||
|
||||
public pause() {
|
||||
this.isRunning = false
|
||||
}
|
||||
|
||||
public setScene(scene: Scene | string) {
|
||||
this.currentScene = typeof scene === 'string' ? Scene.scenes[scene] : scene
|
||||
this.currentScene.setGameEngine(this)
|
||||
}
|
||||
|
||||
|
||||
private update() {
|
||||
if (!this.isRunning) {
|
||||
return
|
||||
}
|
||||
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height)
|
||||
if (this.options?.background) {
|
||||
this.ctx.fillStyle = this.options.background
|
||||
this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height)
|
||||
}
|
||||
this.currentScene?.update()
|
||||
setTimeout(() => {
|
||||
this.update()
|
||||
}, 0)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export class SoundManager {
|
||||
private audio: HTMLAudioElement
|
||||
public constructor(path: string) {
|
||||
this.audio = new Audio(path)
|
||||
this.audio.load()
|
||||
this.audio.volume = .8
|
||||
}
|
||||
|
||||
public play() {
|
||||
this.audio.play()
|
||||
}
|
||||
|
||||
public end() {
|
||||
this.audio.pause()
|
||||
}
|
||||
}
|
||||
|
||||
export class AssetsManager {
|
||||
|
||||
public static assets: Record<string, AssetsManager> = {}
|
||||
|
||||
public isLoaded = false
|
||||
|
||||
private image: HTMLImageElement
|
||||
|
||||
private constructor(
|
||||
private path: string
|
||||
) {
|
||||
this.image = new Image()
|
||||
}
|
||||
|
||||
public static init(path: string) {
|
||||
if (!this.assets[path]) {
|
||||
this.assets[path] = new AssetsManager(path)
|
||||
}
|
||||
return this.assets[path]
|
||||
}
|
||||
|
||||
public async load() {
|
||||
return new Promise<void>((res, rej) => {
|
||||
this.image.src = this.path
|
||||
this.image.onload = () => {
|
||||
this.isLoaded = true
|
||||
res()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
public async get() {
|
||||
if (!this.isLoaded) {
|
||||
await this.load()
|
||||
}
|
||||
return this.image
|
||||
}
|
||||
}
|
||||
|
||||
export class Scene {
|
||||
public static scenes: Record<string, Scene> = {}
|
||||
|
||||
public background?: string
|
||||
|
||||
private components: Array<Component2D> = []
|
||||
private ge!: GameEngine
|
||||
|
||||
public constructor(sceneId: string) {
|
||||
Scene.scenes[sceneId] = this
|
||||
}
|
||||
|
||||
public addComponent(...cp: Array<Component2D>) {
|
||||
return this.components.push(...cp)
|
||||
}
|
||||
|
||||
public setGameEngine(ge: GameEngine) {
|
||||
this.ge = ge
|
||||
}
|
||||
|
||||
public async init() {
|
||||
this.components.forEach((v) => {
|
||||
if (v.init) {
|
||||
v.init()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
public async update() {
|
||||
this.components.forEach(async (v) => {
|
||||
const state: Partial<ComponentState> = {}
|
||||
const width = (v.size?.width ?? 1) * this.ge.caseSize[0]
|
||||
const height = (v.size?.height ?? 1) * this.ge.caseSize[1]
|
||||
if (v.pos) {
|
||||
const ax = v.pos.x * this.ge.caseSize[0]
|
||||
const ay = v.pos.y * this.ge.caseSize[1]
|
||||
state.mouseHovering =
|
||||
this.ge.cursor.x >= ax && this.ge.cursor.x < (ax + width) &&
|
||||
this.ge.cursor.y >= ay && this.ge.cursor.y < (ay + height)
|
||||
state.mouseClicking = state.mouseHovering && this.ge.cursor.isDown
|
||||
state.mouseClicked = state.mouseClicking && !this.ge.cursor.wasDown
|
||||
}
|
||||
if (v.display && v.pos) {
|
||||
const ax = v.pos.x * this.ge.caseSize[0]
|
||||
const ay = v.pos.y * this.ge.caseSize[1]
|
||||
if (v.display.type === 'color') {
|
||||
this.ge.ctx.fillStyle = v.display.source
|
||||
this.ge.ctx.fillRect(
|
||||
ax,
|
||||
ay,
|
||||
width,
|
||||
height
|
||||
)
|
||||
} else if (v.display.type === 'image') {
|
||||
this.ge.ctx.drawImage(
|
||||
await AssetsManager.init(v.display.source).get(),
|
||||
ax,
|
||||
ay,
|
||||
width,
|
||||
height
|
||||
)
|
||||
}
|
||||
}
|
||||
if (v.update) {
|
||||
v.update(state as ComponentState)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export interface ComponentState {
|
||||
mouseHovering: boolean
|
||||
mouseClicking: boolean
|
||||
mouseClicked: boolean
|
||||
}
|
||||
|
||||
export abstract class Component2D {
|
||||
public id?: number
|
||||
public display?: {
|
||||
type: 'image' | 'color'
|
||||
source: string
|
||||
}
|
||||
public pos?: {x: number, y: number, z?: number, rotation?: number}
|
||||
public size?: {width: number, height: number}
|
||||
|
||||
public init?(): Promise<void> | void
|
||||
|
||||
public update?(state: ComponentState): Promise<void> | void
|
||||
}
|
@ -9,6 +9,9 @@ export default class Index extends React.Component {
|
||||
<Text>
|
||||
<Link href="/pokemon-shuffle">Pokémon Shuffle</Link>
|
||||
</Text>
|
||||
<Text>
|
||||
<Link href="/tictactoe">TicTacToe mais ou il y a eu beaucoup trop de temps de passé sur le jeux</Link>
|
||||
</Text>
|
||||
</main>
|
||||
)
|
||||
}
|
||||
|
161
src/pages/tictactoe/Game.ts
Normal file
161
src/pages/tictactoe/Game.ts
Normal file
@ -0,0 +1,161 @@
|
||||
/* eslint-disable max-classes-per-file */
|
||||
import GameEngine, { Component2D, ComponentState, Scene, SoundManager } from 'GameEngine'
|
||||
|
||||
const globalState: {
|
||||
playerTurn: 'X' | 'O'
|
||||
gameState: [
|
||||
[string, string, string],
|
||||
[string, string, string],
|
||||
[string, string, string],
|
||||
]
|
||||
} = {
|
||||
playerTurn: 'X',
|
||||
gameState: [
|
||||
['', '', ''],
|
||||
['', '', ''],
|
||||
['', '', '']
|
||||
]
|
||||
}
|
||||
|
||||
class Item extends Component2D {
|
||||
|
||||
public size = {
|
||||
width: .9,
|
||||
height: .9
|
||||
}
|
||||
|
||||
private x: number
|
||||
private y: number
|
||||
|
||||
public constructor(index: number) {
|
||||
super()
|
||||
this.x = Math.trunc(index % 3)
|
||||
this.y = Math.trunc(index / 3)
|
||||
this.pos = {
|
||||
x: this.x + this.x * .05,
|
||||
y: this.y + this.y * .05
|
||||
}
|
||||
console.log(this.pos, index)
|
||||
}
|
||||
|
||||
public init() {
|
||||
console.log('item initialized')
|
||||
}
|
||||
|
||||
public update(state: ComponentState) {
|
||||
// console.log(state)
|
||||
const value: '' | 'X' | 'O' = globalState.gameState[this.x][this.y] as '' | 'X' | 'O'
|
||||
if (state.mouseClicked && value === '') {
|
||||
// console.log('hovering')
|
||||
this.onClick()
|
||||
}
|
||||
|
||||
if (value === 'X') {
|
||||
this.display = {
|
||||
type: 'image',
|
||||
source: '/assets/tictactoe/X.png'
|
||||
}
|
||||
} else if(value === 'O') {
|
||||
this.display = {
|
||||
type: 'image',
|
||||
source: '/assets/tictactoe/O.png'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private onClick() {
|
||||
const clickSound = new SoundManager('/assets/tictactoe/bip.wav')
|
||||
clickSound.play()
|
||||
globalState.gameState[this.x][this.y] = globalState.playerTurn
|
||||
console.log(this.checkVictory())
|
||||
if (this.checkVictory()) {
|
||||
new SoundManager('/assets/tictactoe/victory.wav').play()
|
||||
// globalState.gameState = [
|
||||
// ['', '', ''],
|
||||
// ['', '', ''],
|
||||
// ['', '', '']
|
||||
// ]
|
||||
}
|
||||
globalState.playerTurn = globalState.playerTurn === 'X' ? 'O' : 'X'
|
||||
}
|
||||
|
||||
// uh MAYBE refactor lol
|
||||
private checkVictory() {
|
||||
if(globalState.gameState[0][0] === globalState.playerTurn) {
|
||||
if(globalState.gameState[1][0] === globalState.playerTurn) {
|
||||
if(globalState.gameState[2][0] === globalState.playerTurn) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
if(globalState.gameState[0][1] === globalState.playerTurn) {
|
||||
if(globalState.gameState[0][2] === globalState.playerTurn) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
if(globalState.gameState[1][1] === globalState.playerTurn) {
|
||||
if(globalState.gameState[2][2] === globalState.playerTurn) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
if(globalState.gameState[2][2] === globalState.playerTurn) {
|
||||
if(globalState.gameState[2][1] === globalState.playerTurn) {
|
||||
if(globalState.gameState[2][0] === globalState.playerTurn) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
if(globalState.gameState[1][2] === globalState.playerTurn) {
|
||||
if(globalState.gameState[0][2] === globalState.playerTurn) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
if(globalState.gameState[1][1] === globalState.playerTurn) {
|
||||
if(globalState.gameState[0][1] === globalState.playerTurn) {
|
||||
if(globalState.gameState[2][1] === globalState.playerTurn) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
if(globalState.gameState[1][0] === globalState.playerTurn) {
|
||||
if(globalState.gameState[1][2] === globalState.playerTurn) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
class Line extends Component2D {
|
||||
|
||||
public constructor(direction: number, index: number) {
|
||||
super()
|
||||
this.display = {
|
||||
type: 'color',
|
||||
source: 'orange'
|
||||
}
|
||||
this.pos = {
|
||||
x: direction ? index ? 1.95 : .9 : 0,
|
||||
y: direction ? 0 : index ? .9 : 1.95
|
||||
}
|
||||
this.size = {
|
||||
width: direction ? .15 : 3,
|
||||
height: direction ? 3 : .15
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
const ge = new GameEngine('#test', {
|
||||
caseCount: 3,
|
||||
background: 'blue'
|
||||
})
|
||||
const scene = new Scene('TicTacToe')
|
||||
scene.addComponent(
|
||||
...Array.from(new Array(2)).map((_, index) => new Line(0, index)),
|
||||
...Array.from(new Array(2)).map((_, index) => new Line(1, index)),
|
||||
...Array.from(new Array(9)).map((_, index) => new Item(index)),
|
||||
)
|
||||
|
||||
ge.start()
|
||||
ge.setScene(scene)
|
15
src/pages/tictactoe/index.tsx
Normal file
15
src/pages/tictactoe/index.tsx
Normal file
@ -0,0 +1,15 @@
|
||||
import { Text, Link } from '@dzeio/components'
|
||||
import React from 'react'
|
||||
export default class Snake extends React.PureComponent {
|
||||
public async componentDidMount() {
|
||||
await import('./Game')
|
||||
|
||||
}
|
||||
public render = () => (
|
||||
<>
|
||||
<canvas id="test" width="300" height="300"></canvas>
|
||||
<Text>Bienvenue sur le TicTacToe le plus Overengineered du monde xd<br /> Avec un moteur de jeux complètement fait maison et bien plus encore en préparation ! (vieille version en 75 lignes, nouvelle en plus de 200 lol)</Text>
|
||||
<Text>Version faites il y a 4 ans encore disponible sur Github lol <Link href="https://github.com/Aviortheking/TicTacToe">https://github.com/Aviortheking/TicTacToe</Link></Text>
|
||||
</>
|
||||
)
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user