最终整理版
This commit is contained in:
@@ -0,0 +1,359 @@
|
||||
extends NpcScript
|
||||
class_name TicTacToeGlobal
|
||||
|
||||
#
|
||||
static var BOARD_POSITION : Vector2i = Vector2i(120, 80) * 32 + Vector2i(16, 32) # Tile 120,80 with an offset of 16,32 pixels
|
||||
static var CELL_ID : int = "TicTacToeCell".hash()
|
||||
const CELL_COUNT : int = 9
|
||||
const WINNING_LINES : Array[Array] = [
|
||||
[0, 1, 2],
|
||||
[3, 4, 5],
|
||||
[6, 7, 8],
|
||||
[0, 3, 6],
|
||||
[1, 4, 7],
|
||||
[2, 5, 8],
|
||||
[0, 4, 8],
|
||||
[2, 4, 6],
|
||||
]
|
||||
const IDLE_TIMEOUT : float = 30.0
|
||||
const NPC_DELAY_MIN : float = 0.2
|
||||
const NPC_DELAY_MAX : float = 1.0
|
||||
|
||||
#
|
||||
enum State
|
||||
{
|
||||
NONE = 0,
|
||||
X,
|
||||
O
|
||||
}
|
||||
|
||||
var boardStates : Array[State] = [
|
||||
State.NONE, State.NONE, State.NONE,
|
||||
State.NONE, State.NONE, State.NONE,
|
||||
State.NONE, State.NONE, State.NONE
|
||||
]
|
||||
var boardNpcs : Array[NpcAgent] = [
|
||||
null, null, null,
|
||||
null, null, null,
|
||||
null, null, null
|
||||
]
|
||||
|
||||
var playerX : PlayerAgent = null
|
||||
var playerO : PlayerAgent = null
|
||||
var idleTimer : Timer = null
|
||||
|
||||
var startStep : State = State.NONE
|
||||
var currentTurn : State = State.NONE
|
||||
|
||||
# Board handling
|
||||
func SpawnCells():
|
||||
var map : WorldMap = WorldAgent.GetMapFromAgent(npc)
|
||||
if not map:
|
||||
return
|
||||
|
||||
var inst : WorldInstance = WorldAgent.GetInstanceFromAgent(npc)
|
||||
if not inst:
|
||||
return
|
||||
|
||||
var spawn : SpawnObject = SpawnObject.new()
|
||||
spawn.id = CELL_ID
|
||||
spawn.type = "Npc"
|
||||
spawn.state = ActorCommons.State.IDLE
|
||||
spawn.is_persistant = false
|
||||
spawn.map = map
|
||||
for cellIdx in CELL_COUNT:
|
||||
spawn.spawn_position = BOARD_POSITION + Vector2i((cellIdx % 3) * 32, int(cellIdx / 3.0) * 32)
|
||||
var spawnedAgent : BaseAgent = WorldAgent.CreateAgent(spawn, inst.id)
|
||||
if spawnedAgent and spawnedAgent is NpcAgent:
|
||||
boardNpcs[cellIdx] = spawnedAgent
|
||||
boardNpcs[cellIdx].interacted.connect(CellSelected.bind(boardNpcs[cellIdx]))
|
||||
|
||||
func CellSelected(playerAgent : BaseAgent, cellAgent : NpcAgent):
|
||||
if playerAgent is not PlayerAgent:
|
||||
return
|
||||
|
||||
if cellAgent:
|
||||
var cellIdx : int = boardNpcs.find(cellAgent)
|
||||
if cellIdx >= 0:
|
||||
MakeMove(playerAgent, cellIdx)
|
||||
|
||||
func DespawnCells():
|
||||
for cellIdx in CELL_COUNT:
|
||||
if boardNpcs[cellIdx]:
|
||||
if is_instance_valid(boardNpcs[cellIdx]):
|
||||
WorldAgent.RemoveAgent.call_deferred(boardNpcs[cellIdx])
|
||||
boardNpcs[cellIdx] = null
|
||||
|
||||
func DespawnUnusedCells():
|
||||
for cellIdx in CELL_COUNT:
|
||||
if boardStates[cellIdx] == State.NONE and boardNpcs[cellIdx] and is_instance_valid(boardNpcs[cellIdx]):
|
||||
WorldAgent.RemoveAgent.call_deferred(boardNpcs[cellIdx])
|
||||
boardNpcs[cellIdx] = null
|
||||
|
||||
# PvP matchmaking
|
||||
func StartPvP(player : PlayerAgent) -> State:
|
||||
if not player or not is_instance_valid(player):
|
||||
return State.NONE
|
||||
|
||||
if startStep != State.O:
|
||||
if not playerX or not is_instance_valid(playerX) or WorldAgent.GetInstanceFromAgent(playerX) != WorldAgent.GetInstanceFromAgent(player):
|
||||
playerX = player
|
||||
Callback.AddCallback(playerX.tree_exiting, LeaveQueue, [playerX], ConnectFlags.CONNECT_ONE_SHOT)
|
||||
startStep = State.X
|
||||
return startStep
|
||||
elif not playerO or not is_instance_valid(playerO) or WorldAgent.GetInstanceFromAgent(playerO) != WorldAgent.GetInstanceFromAgent(player):
|
||||
playerO = player
|
||||
startStep = State.O
|
||||
StartGame()
|
||||
return startStep
|
||||
return State.NONE
|
||||
|
||||
func LeavePvP(player : PlayerAgent):
|
||||
if not player or not is_instance_valid(player):
|
||||
if playerX and is_instance_valid(playerX):
|
||||
NpcCommons.PushNotification(playerX, "Your opponent left. You win!")
|
||||
elif playerO and is_instance_valid(playerO):
|
||||
NpcCommons.PushNotification(playerO, "Your opponent left. You win!")
|
||||
elif startStep == State.O and (player == playerX or player == playerO):
|
||||
var opponent : PlayerAgent = playerO if player == playerX else playerX
|
||||
if opponent and is_instance_valid(opponent):
|
||||
NpcCommons.PushNotification(opponent, "Your opponent left. You win!")
|
||||
EndGame()
|
||||
|
||||
func LeaveQueue(player : PlayerAgent):
|
||||
if player and is_instance_valid(player) and player == playerX:
|
||||
EndGame()
|
||||
|
||||
# Game management
|
||||
func StartGame():
|
||||
if currentTurn != State.NONE:
|
||||
return
|
||||
|
||||
startStep = State.O
|
||||
currentTurn = State.X
|
||||
|
||||
DespawnCells()
|
||||
SpawnCells()
|
||||
StartIdleTimer()
|
||||
|
||||
if playerX:
|
||||
Callback.RemoveCallback(playerX.tree_exiting, LeaveQueue)
|
||||
Callback.AddCallback(playerX.tree_exiting, LeavePvP, [playerX], ConnectFlags.CONNECT_ONE_SHOT)
|
||||
NpcCommons.PushNotification(playerX, "Tic Tac Toe started! You play X!")
|
||||
if playerO:
|
||||
Callback.AddCallback(playerO.tree_exiting, LeavePvP, [playerO], ConnectFlags.CONNECT_ONE_SHOT)
|
||||
NpcCommons.PushNotification(playerO, "Tic Tac Toe started! You play O!")
|
||||
|
||||
func StartPvE(player : PlayerAgent) -> bool:
|
||||
if startStep == State.NONE:
|
||||
playerX = player
|
||||
playerO = null
|
||||
StartGame()
|
||||
return startStep == State.O
|
||||
return false
|
||||
|
||||
# Move handling
|
||||
func MakeMove(player : PlayerAgent, cellIndex : int) -> bool:
|
||||
if currentTurn == State.NONE or cellIndex < 0 or cellIndex >= CELL_COUNT:
|
||||
return false
|
||||
|
||||
var mark : State = boardStates[cellIndex]
|
||||
if mark != State.NONE:
|
||||
return false
|
||||
|
||||
if player == playerX and currentTurn == State.X:
|
||||
mark = State.X
|
||||
elif player == playerO and currentTurn == State.O:
|
||||
mark = State.O
|
||||
else:
|
||||
return false
|
||||
|
||||
boardStates[cellIndex] = mark
|
||||
|
||||
UpdateCellVisual(cellIndex)
|
||||
currentTurn = State.X if currentTurn == State.O else State.O
|
||||
OnMovePlayed()
|
||||
StartIdleTimer()
|
||||
return true
|
||||
|
||||
func OnMovePlayed():
|
||||
if startStep != State.O:
|
||||
return
|
||||
|
||||
var result : State = CheckWin()
|
||||
if result != State.NONE or CheckDraw():
|
||||
AnnounceResult(result)
|
||||
elif not playerO:
|
||||
NotifyTurn()
|
||||
var delay : float = randf_range(NPC_DELAY_MIN, NPC_DELAY_MAX)
|
||||
Callback.SelfDestructTimer(npc, delay, PlayNPCMove)
|
||||
else:
|
||||
NotifyTurn()
|
||||
|
||||
func PlayNPCMove():
|
||||
if currentTurn != State.O or playerO:
|
||||
return
|
||||
|
||||
var npcMove : int = GetNPCMove()
|
||||
if npcMove >= 0:
|
||||
MakeMove(null, npcMove)
|
||||
|
||||
func NotifyTurn():
|
||||
if currentTurn == State.NONE:
|
||||
return
|
||||
|
||||
if playerO:
|
||||
var activePlayer : PlayerAgent = playerX if currentTurn == State.X else playerO
|
||||
var waitingPlayer : PlayerAgent = playerO if currentTurn == State.X else playerX
|
||||
if activePlayer and is_instance_valid(activePlayer):
|
||||
NpcCommons.PushNotification(activePlayer, "Your turn!")
|
||||
if waitingPlayer and is_instance_valid(waitingPlayer):
|
||||
NpcCommons.PushNotification(waitingPlayer, "Waiting for opponent...")
|
||||
else:
|
||||
if currentTurn == State.X:
|
||||
if playerX and is_instance_valid(playerX):
|
||||
NpcCommons.PushNotification(playerX, "Your turn!")
|
||||
else:
|
||||
if playerX and is_instance_valid(playerX):
|
||||
NpcCommons.PushNotification(playerX, "Waiting for opponent...")
|
||||
|
||||
func UpdateCellVisual(cellIndex : int):
|
||||
if cellIndex < 0 or cellIndex >= CELL_COUNT:
|
||||
return
|
||||
|
||||
var cellNpc : NpcAgent = boardNpcs[cellIndex]
|
||||
if not cellNpc or not is_instance_valid(cellNpc):
|
||||
return
|
||||
|
||||
match boardStates[cellIndex]:
|
||||
State.NONE:
|
||||
cellNpc.state = ActorCommons.State.IDLE
|
||||
State.X:
|
||||
cellNpc.state = ActorCommons.State.TRIGGER
|
||||
if cellNpc.interacted.is_connected(CellSelected):
|
||||
cellNpc.interacted.disconnect(CellSelected)
|
||||
State.O:
|
||||
cellNpc.state = ActorCommons.State.SIT
|
||||
if cellNpc.interacted.is_connected(CellSelected):
|
||||
cellNpc.interacted.disconnect(CellSelected)
|
||||
cellNpc.set_physics_process(true)
|
||||
cellNpc.requireFullUpdate = true
|
||||
|
||||
# Idle timer
|
||||
func StartIdleTimer():
|
||||
StopIdleTimer()
|
||||
idleTimer = Callback.SelfDestructTimer(npc, IDLE_TIMEOUT, IdleTimeout)
|
||||
|
||||
func StopIdleTimer():
|
||||
if idleTimer and is_instance_valid(idleTimer) and not idleTimer.is_queued_for_deletion():
|
||||
idleTimer.stop()
|
||||
idleTimer.queue_free()
|
||||
idleTimer = null
|
||||
|
||||
func IdleTimeout():
|
||||
idleTimer = null
|
||||
if currentTurn == State.NONE:
|
||||
return
|
||||
|
||||
var loser : State = currentTurn
|
||||
var winner : State = State.X if loser == State.O else State.O
|
||||
|
||||
var loserPlayer : PlayerAgent = playerX if loser == State.X else playerO
|
||||
if loserPlayer and is_instance_valid(loserPlayer):
|
||||
NpcCommons.PushNotification(loserPlayer, "You took too long! You lose.")
|
||||
|
||||
AnnounceResult(winner)
|
||||
|
||||
# Game end
|
||||
func EndGame():
|
||||
StopIdleTimer()
|
||||
ClearPlayerCallbacks()
|
||||
DespawnUnusedCells()
|
||||
ResetBoard()
|
||||
|
||||
func ClearPlayerCallbacks():
|
||||
if playerX and is_instance_valid(playerX):
|
||||
Callback.RemoveCallback(playerX.tree_exiting, LeavePvP)
|
||||
if playerO and is_instance_valid(playerO):
|
||||
Callback.RemoveCallback(playerO.tree_exiting, LeavePvP)
|
||||
|
||||
func ResetBoard():
|
||||
playerX = null
|
||||
playerO = null
|
||||
startStep = State.NONE
|
||||
currentTurn = State.NONE
|
||||
for cellIdx in CELL_COUNT:
|
||||
boardStates[cellIdx] = State.NONE
|
||||
|
||||
# Win detection
|
||||
func CheckWin() -> State:
|
||||
for line in WINNING_LINES:
|
||||
var state : State = boardStates[line[0]]
|
||||
if state != State.NONE and state == boardStates[line[1]] and state == boardStates[line[2]]:
|
||||
return state
|
||||
return State.NONE
|
||||
|
||||
func CheckDraw() -> bool:
|
||||
for cell in boardStates:
|
||||
if cell == State.NONE:
|
||||
return false
|
||||
return true
|
||||
|
||||
func AnnounceResult(result : State):
|
||||
var msg : String = ""
|
||||
match result:
|
||||
State.NONE:
|
||||
msg = "It's a draw!"
|
||||
State.X:
|
||||
msg = "%s wins!" % (playerX.nick if playerX else "X")
|
||||
State.O:
|
||||
msg = "%s wins!" % (playerO.nick if playerO else npc.nick)
|
||||
|
||||
if playerX and is_instance_valid(playerX):
|
||||
NpcCommons.PushNotification(playerX, msg)
|
||||
if playerO and is_instance_valid(playerO):
|
||||
NpcCommons.PushNotification(playerO, msg)
|
||||
EndGame()
|
||||
|
||||
# NPC AI
|
||||
func GetNPCMove() -> int:
|
||||
var winMove : int = FindWinningMove(State.O)
|
||||
if winMove >= 0:
|
||||
return winMove
|
||||
|
||||
var blockMove : int = FindWinningMove(State.X)
|
||||
if blockMove >= 0:
|
||||
return blockMove
|
||||
|
||||
if boardStates[4] == State.NONE:
|
||||
return 4
|
||||
|
||||
var corners : Array[int] = [0, 2, 6, 8]
|
||||
corners.shuffle()
|
||||
for corner in corners:
|
||||
if boardStates[corner] == State.NONE:
|
||||
return corner
|
||||
|
||||
var edges : Array[int] = [1, 3, 5, 7]
|
||||
edges.shuffle()
|
||||
for edge in edges:
|
||||
if boardStates[edge] == State.NONE:
|
||||
return edge
|
||||
|
||||
return -1
|
||||
|
||||
func FindWinningMove(state : State) -> int:
|
||||
for line : Array in WINNING_LINES:
|
||||
var totalPointInLine : int = 0
|
||||
var firstEmptyIdx : int = -1
|
||||
|
||||
for idx : int in line:
|
||||
if boardStates[idx] == state:
|
||||
totalPointInLine += 1
|
||||
elif boardStates[idx] == State.NONE:
|
||||
firstEmptyIdx = idx
|
||||
|
||||
# Winning move found if 2 out of 3 point in a line are already matching the given state and if an empty index is available
|
||||
if totalPointInLine == 2 and firstEmptyIdx >= 0:
|
||||
return firstEmptyIdx
|
||||
return -1
|
||||
Reference in New Issue
Block a user