import tkinter as tk
import math, random, time, threading, socket, sys
# ---------------- Configuration Constants ----------------
WINDOW_WIDTH = 1280
WINDOW_HEIGHT = 720
# Map dimensions in tiles
MAP_WIDTH = 300
MAP_HEIGHT = 300
BASE_TILE_SIZE = 16 # Base tile size at zoom=1
DEFAULT_VIEW_TILES_W = 60
DEFAULT_VIEW_TILES_H = 40
FPS = 20
ZOOM_STEP = 1.1
# Network defaults (for LAN multiplayer)
NET_PORT = 50007
# Use localhost for testing; change as necessary.
NET_HOST = "127.0.0.1"
# ---------------- Map Generation & 2.5D Terrain ----------------
def generate_map(width, height):
# Tiles: '.' = ground, 'M' = mountain, 'B' = building, 'K' = bunker, 'o' = crater
grid = []
for y in range(height):
row = []
for x in range(width):
r = random.random()
if r < 0.04:
row.append('M')
elif r < 0.08:
row.append('B')
elif r < 0.10:
row.append('K')
elif r < 0.12:
row.append('o')
else:
row.append('.')
grid.append(row)
return grid
# ---------------- Tank Models (ASCII Art) ----------------
PLAYER_MODEL = [
" ____ ",
" /____\\ ",
" | M4 | ",
" |_____| "
]
ENEMY_MODEL = [
" ____ ",
" /____\\ ",
" |T-34 | ",
" |_____| "
]
# Infantry will be drawn as a single character "I"
# ---------------- Helper Functions ----------------
def angle_between(x1, y1, x2, y2):
return math.degrees(math.atan2(y2 - y1, x2 - x1))
def distance(x1, y1, x2, y2):
return math.hypot(x2 - x1, y2 - y1)
# ---------------- Classes for Tanks, Shells, Explosions ----------------
class Tank:
def __init__(self, tank_id, country, name, model, max_health, speed, max_ammo, x, y, is_player=False):
self.id = tank_id
self.country = country
self.name = name
self.model = model # list of strings
self.max_health = max_health
self.health = max_health
self.speed = speed
self.max_ammo = max_ammo
self.ammo = max_ammo
self.x = x # tile coordinate (float)
self.y = y
self.turret_angle = 0 # in degrees; updated via mouse for player, auto for enemy
self.is_player = is_player
self.last_shot_time = 0
self.fire_delay = 0.5 # seconds between shots
self.kills = 0
def alive(self):
return self.health > 0
def is_damaged(self):
return self.health < self.max_health * 0.5
class Shell:
def __init__(self, x, y, angle, shell_type, owner_id):
self.x = x
self.y = y
self.angle = angle # in degrees
self.shell_type = shell_type # 'AP' or 'HE'
self.owner_id = owner_id
self.speed = 3.0
self.damage = 40 if shell_type == 'AP' else 20
def update(self):
rad = math.radians(self.angle)
self.x += math.cos(rad) * self.speed
self.y += math.sin(rad) * self.speed
class Explosion:
FRAMES = [
" ░ ",
" ▒░▒ ",
" ▒▓▓▓▒ ",
"▒▓██▓▒",
" ▒▓▓▓▒ ",
" ▒░▒ ",
" ░ "
]
def __init__(self, x, y):
self.x = x
self.y = y
self.frame_index = 0
self.frame_delay = 1
self.frame_timer = 0
def update(self):
self.frame_timer += 1
if self.frame_timer >= self.frame_delay:
self.frame_timer = 0
self.frame_index += 1
def finished(self):
return self.frame_index >= len(self.FRAMES)
def current_frame(self):
if self.frame_index < len(self.FRAMES):
return self.FRAMES[self.frame_index]
return ""
# ---------------- Network Code ----------------
# A minimal LAN multiplayer implementation.
# In this simple prototype, each client sends its player state to the other.
# For brevity, we implement a simple TCP connection that sends a text message every frame:
# Format: "ID;x;y;turret_angle;health;ammo;kills"
class NetworkHandler(threading.Thread):
def __init__(self, game, is_host, host_ip=NET_HOST):
super().__init__(daemon=True)
self.game = game
self.is_host = is_host
self.host_ip = host_ip
self.sock = None
self.running = True
def run(self):
if self.is_host:
self.run_server()
else:
self.run_client()
def run_server(self):
# Server listens and accepts one connection
server_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server_sock.bind(("", NET_PORT))
server_sock.listen(1)
self.sock, addr = server_sock.accept()
print(f"Client connected from {addr}")
while self.running:
try:
data = self.sock.recv(1024).decode()
if data:
self.process_message(data)
# Send our player state periodically
msg = self.game.serialize_state()
self.sock.sendall(msg.encode())
except Exception as e:
print("Server network error:", e)
break
server_sock.close()
def run_client(self):
# Client connects to server
self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
try:
self.sock.connect((self.host_ip, NET_PORT))
print("Connected to server.")
except Exception as e:
print("Unable to connect to server:", e)
return
while self.running:
try:
data = self.sock.recv(1024).decode()
if data:
self.process_message(data)
msg = self.game.serialize_state()
self.sock.sendall(msg.encode())
except Exception as e:
print("Client network error:", e)
break
self.sock.close()
def process_message(self, message):
# Process a state message, e.g., update enemy tank state.
# Expected format: "ID;x;y;turret_angle;health;ammo;kills"
try:
parts = message.strip().split(";")
if len(parts) != 7:
return
pid, x, y, t_angle, health, ammo, kills = parts
x = float(x)
y = float(y)
t_angle = float(t_angle)
health = float(health)
ammo = int(ammo)
kills = int(kills)
# If the received state is not for our own player, update enemy state.
if pid != self.game.player.id:
# If enemy does not exist, create one
if pid not in self.game.tanks:
self.game.tanks[pid] = Tank(pid, "USSR", "T-34", ENEMY_MODEL, 95, 1.0, 5, x, y, is_player=False)
enemy = self.game.tanks[pid]
enemy.x = x
enemy.y = y
enemy.turret_angle = t_angle
enemy.health = health
enemy.ammo = ammo
enemy.kills = kills
except Exception as e:
print("Error processing message:", e)
# ---------------- Main Game Class Using Tkinter ----------------
class TankGameApp:
def __init__(self, root, multiplayer=False, host_mode=False, host_ip=NET_HOST):
self.root = root
self.multiplayer = multiplayer
self.host_mode = host_mode # True if hosting, False if joining
self.root.title("ASCII Tank Game – LAN Edition")
self.canvas = tk.Canvas(root, width=WINDOW_WIDTH, height=WINDOW_HEIGHT, bg="black")
self.canvas.pack(fill=tk.BOTH, expand=True)
self.ui_panel = tk.Label(root, text="", font=("Courier", 16), bg="gray20", fg="white")
self.ui_panel.pack(fill=tk.X)
self.zoom = 1.0
self.cam_x = 0
self.cam_y = 0
# Generate the map
self.map = generate_map(MAP_WIDTH, MAP_HEIGHT)
# Create player tank at center
self.player = Tank("PLAYER", "USA", "m4 Sherman", PLAYER_MODEL, 100, 1.0, 5, MAP_WIDTH/2, MAP_HEIGHT/2, is_player=True)
# For multiplayer, enemy state will be updated via network.
# For offline mode, create a default enemy.
self.tanks = {self.player.id: self.player}
if not self.multiplayer:
enemy = Tank("ENEMY", "USSR", "T-34", ENEMY_MODEL, 95, 1.0, 5, MAP_WIDTH/2 + 30, MAP_HEIGHT/2, is_player=False)
self.tanks[enemy.id] = enemy
else:
# In multiplayer, if hosting, we keep enemy empty; if joining, our enemy state will be received.
if self.host_mode:
# As host, create a placeholder enemy that we update
enemy = Tank("ENEMY", "USSR", "T-34", ENEMY_MODEL, 95, 1.0, 5, MAP_WIDTH/2 + 30, MAP_HEIGHT/2, is_player=False)
self.tanks[enemy.id] = enemy
self.shells = []
self.explosions = []
self.last_time = time.time()
self.FPS = FPS
self.mouse_x = 0
self.mouse_y = 0
# Bind mouse and keyboard events
self.canvas.bind("<Motion>", self.on_mouse_move)
self.canvas.bind("<Button-1>", self.on_mouse_click)
self.root.bind("<Key>", self.on_key_press)
self.root.bind("<MouseWheel>", self.on_mouse_wheel)
# If in multiplayer, start network handler in a separate thread
if self.multiplayer:
self.net_handler = NetworkHandler(self, self.host_mode, host_ip)
self.net_handler.start()
self.game_loop()
# --------------- Event Handlers ---------------
def on_key_press(self, event):
if event.keysym.lower() == "w":
self.player.y -= self.player.speed
elif event.keysym.lower() == "s":
self.player.y += self.player.speed
elif event.keysym.lower() == "a":
self.player.x -= self.player.speed
elif event.keysym.lower() == "d":
self.player.x += self.player.speed
elif event.char == " ":
self.shoot(self.player)
elif event.char == "+":
self.zoom *= ZOOM_STEP
elif event.char == "-":
self.zoom /= ZOOM_STEP
self.clamp_player()
self.update_camera()
self.draw()
def on_mouse_move(self, event):
self.mouse_x = event.x
self.mouse_y = event.y
view_center_x = WINDOW_WIDTH / 2
view_center_y = WINDOW_HEIGHT / 2
dx = event.x - view_center_x
dy = event.y - view_center_y
self.player.turret_angle = math.degrees(math.atan2(dy, dx))
self.draw()
def on_mouse_click(self, event):
dx = event.x - (WINDOW_WIDTH/2)
dy = event.y - (WINDOW_HEIGHT/2)
angle = math.degrees(math.atan2(dy, dx))
self.player.turret_angle = angle
self.shoot(self.player)
self.draw()
def on_mouse_wheel(self, event):
if event.delta > 0:
self.zoom *= ZOOM_STEP
else:
self.zoom /= ZOOM_STEP
self.draw()
def clamp_player(self):
self.player.x = max(0, min(MAP_WIDTH-1, self.player.x))
self.player.y = max(0, min(MAP_HEIGHT-1, self.player.y))
def update_camera(self):
self.cam_x = self.player.x - (DEFAULT_VIEW_TILES_W / 2)
self.cam_y = self.player.y - (DEFAULT_VIEW_TILES_H / 2)
self.cam_x = max(0, min(MAP_WIDTH - DEFAULT_VIEW_TILES_W, self.cam_x))
self.cam_y = max(0, min(MAP_HEIGHT - DEFAULT_VIEW_TILES_H, self.cam_y))
# --------------- Game Logic ---------------
def shoot(self, tank):
now = time.time()
if now - tank.last_shot_time >= tank.fire_delay and tank.ammo > 0:
shell = Shell(tank.x, tank.y, tank.turret_angle, "AP", tank.id)
self.shells.append(shell)
tank.ammo -= 1
tank.last_shot_time = now
def update_game(self, dt):
shells_to_remove = []
for shell in self.shells:
shell.update()
if shell.x < 0 or shell.x >= MAP_WIDTH or shell.y < 0 or shell.y >= MAP_HEIGHT:
shells_to_remove.append(shell)
continue
# Check collision with enemy tank (simplified)
for tid, tank in self.tanks.items():
if tid == shell.owner_id or not tank.alive():
continue
if distance(shell.x, shell.y, tank.x, tank.y) < 2:
tank.health -= shell.damage
shells_to_remove.append(shell)
self.explosions.append(Explosion(shell.x, shell.y))
break
for s in shells_to_remove:
if s in self.shells:
self.shells.remove(s)
explosions_to_remove = []
for exp in self.explosions:
exp.update()
if exp.finished():
explosions_to_remove.append(exp)
for exp in explosions_to_remove:
if exp in self.explosions:
self.explosions.remove(exp)
# Update enemy behavior (for offline and host multiplayer, simple auto-targeting)
if "ENEMY" in self.tanks and self.tanks["ENEMY"].alive():
enemy = self.tanks["ENEMY"]
dx = self.player.x - enemy.x
dy = self.player.y - enemy.y
enemy.turret_angle = math.degrees(math.atan2(dy, dx))
# Enemy auto-shooting if in range
if distance(self.player.x, self.player.y, enemy.x, enemy.y) < 20 and time.time() - enemy.last_shot_time > 2:
self.shoot(enemy)
enemy.last_shot_time = time.time()
def serialize_state(self):
# Serialize player's state as: "ID;x;y;turret_angle;health;ammo;kills\n"
return f"{self.player.id};{self.player.x};{self.player.y};{self.player.turret_angle};{self.player.health};{self.player.ammo};{self.player.kills}\n"
# --------------- Drawing Functions ---------------
def draw(self):
self.canvas.delete("all")
tile_size = BASE_TILE_SIZE * self.zoom
# For a 2.5D effect, we tilt the map: offset each row slightly based on y.
tilt_offset = 4 * self.zoom
# Draw background (map) with gradient shading (simulate fake shadows/lighting)
for j in range(DEFAULT_VIEW_TILES_H):
for i in range(DEFAULT_VIEW_TILES_W):
map_x = int(self.cam_x) + i
map_y = int(self.cam_y) + j
if 0 <= map_x < MAP_WIDTH and 0 <= map_y < MAP_HEIGHT:
tile = self.map[map_y][map_x]
# Adjust brightness: lower rows appear darker.
if tile == '.':
color = "#88cc88"
elif tile == 'M':
color = "#aaaaaa"
elif tile == 'B':
color = "#ffcc00"
elif tile == 'K':
color = "#555555"
elif tile == 'o':
color = "#664422"
else:
color = "#88cc88"
x_px = i * tile_size + (j * tilt_offset * 0.2)
y_px = j * tile_size - (j * 0.5)
self.canvas.create_text(x_px, y_px, anchor="nw",
text=tile, fill=color,
font=("Courier", int(12 * self.zoom)))
# Draw all tanks with perspective scaling
for tank in self.tanks.values():
d = distance(self.player.x, self.player.y, tank.x, tank.y)
screen_x = (tank.x - self.cam_x) * tile_size
screen_y = (tank.y - self.cam_y) * tile_size
# For additional 2.5D feel, offset the tank drawing based on its y (simulate depth)
screen_x += (tank.y - self.cam_y) * (tilt_offset * 0.2)
if d > 10:
# Far: draw as a single, small character
self.canvas.create_text(screen_x, screen_y, text="T",
fill="cyan" if tank.is_player else "magenta",
font=("Courier", max(8, int(12 * self.zoom * 0.5))))
else:
# Near: draw full model
model = tank.model
for idx, line in enumerate(model):
self.canvas.create_text(screen_x, screen_y + idx * (12 * self.zoom),
anchor="nw",
text=line,
fill="yellow" if tank.is_player else "red",
font=("Courier", int(12 * self.zoom)))
# Draw turret indicator (simulate rotation animation by adjusting length slightly)
center_x = screen_x + (len(model[0]) * 6 * self.zoom) / 2
center_y = screen_y + (len(model) * 12 * self.zoom) / 2
turret_len = 20 * self.zoom
rad = math.radians(tank.turret_angle)
end_x = center_x + math.cos(rad) * turret_len
end_y = center_y + math.sin(rad) * turret_len
self.canvas.create_line(center_x, center_y, end_x, end_y, fill="cyan", width=2)
# Draw a fake shadow (a black offset) for extra depth:
self.canvas.create_text(screen_x+2, screen_y+2, anchor="nw",
text=model[0], fill="black",
font=("Courier", int(12 * self.zoom)))
# Draw shells
for shell in self.shells:
s_x = (shell.x - self.cam_x) * tile_size
s_y = (shell.y - self.cam_y) * tile_size
self.canvas.create_text(s_x, s_y, text="*", fill="white",
font=("Courier", int(12 * self.zoom)))
# Draw explosions
for exp in self.explosions:
exp_text = exp.current_frame()
e_x = (exp.x - self.cam_x) * tile_size
e_y = (exp.y - self.cam_y) * tile_size
self.canvas.create_text(e_x, e_y, text=exp_text, fill="orange",
font=("Courier", int(12 * self.zoom)))
# Draw crosshair at mouse position
self.canvas.create_oval(self.mouse_x - 5, self.mouse_y - 5, self.mouse_x + 5, self.mouse_y + 5,
outline="lime")
# Update UI panel
info_text = f"HP: {self.player.health} Ammo: {self.player.ammo} Kills: {self.player.kills} Zoom: {self.zoom:.2f}"
if self.multiplayer:
mode = "HOST" if self.host_mode else "CLIENT"
info_text += f" Mode: {mode}"
self.ui_panel.config(text=info_text)
# --------------- Main Game Loop ---------------
def game_loop(self):
now = time.time()
dt = now - self.last_time
self.last_time = now
self.update_game(dt)
self.update_camera()
self.draw()
self.root.after(int(1000 / self.FPS), self.game_loop)
def serialize_state(self):
return f"{self.player.id};{self.player.x};{self.player.y};{self.player.turret_angle};{self.player.health};{self.player.ammo};{self.player.kills}\n"
# ---------------- Main Program ----------------
def main():
# Ask user if multiplayer is desired and if hosting or joining:
mode_choice = input("Enter 'm' for multiplayer LAN or 'o' for offline mode: ").strip().lower()
multiplayer = (mode_choice == 'm')
host_mode = True
host_ip = NET_HOST
if multiplayer:
host_choice = input("Enter 'h' to host or 'j' to join: ").strip().lower()
host_mode = (host_choice == 'h')
if not host_mode:
host_ip = input("Enter host IP address: ").strip()
root = tk.Tk()
app = TankGameApp(root, multiplayer=multiplayer, host_mode=host_mode, host_ip=host_ip)
root.mainloop()
if __name__ == "__main__":
main()