mirror of
https://github.com/JohnBreaux/Boat-Battle.git
synced 2025-02-04 12:28:35 +00:00
Compare commits
No commits in common. "main" and "v1.0.0-alpha" have entirely different histories.
main
...
v1.0.0-alp
@ -1,34 +0,0 @@
|
|||||||
# Boat Battle - Player's Guide
|
|
||||||
|
|
||||||
Boat Battle is a limited information game where two opposing players compete to sink each others' ships.
|
|
||||||
|
|
||||||
### Starting a Multiplayer Game
|
|
||||||
1. From the Title Screen, select "Start" to bring you to the Lobby.
|
|
||||||
2. Select "Host Game".
|
|
||||||
3. Give the other player your IP address, shown on screen, and wait for them to join.
|
|
||||||
4. When everyone's joined, click Start Game to start the game!
|
|
||||||
|
|
||||||
### Joining a Multiplayer Game
|
|
||||||
1. From the Title Screen, select "Start" to bring you to the Lobby.
|
|
||||||
2. Select "Join Game".
|
|
||||||
3. In the "Join Game" text-entry box, type the host's IP address, and press the Enter or Return key
|
|
||||||
4. Wait for the host to start the game!
|
|
||||||
|
|
||||||
### Playing the Game
|
|
||||||
1. Place your ships:
|
|
||||||
- Move your mouse over the center of each ship, marked by a pin
|
|
||||||
- Click and drag it onto free spaces on the board
|
|
||||||
- Right click the pin to rotate it!
|
|
||||||
- Be wary when rotating ships: if they bump into each other, one will get knocked off the board!
|
|
||||||
|
|
||||||
2. Confirm your ship placement:
|
|
||||||
- Once your ships are all on the board, press the "Confirm Placement" button to lock them in place
|
|
||||||
|
|
||||||
3. When it's your turn to play, Fire!
|
|
||||||
- When your turn starts, the Fire menu pops up.
|
|
||||||
- Move your mouse to a space on the board, and click a spot to place the cross-hair
|
|
||||||
- To move the cross-hair, click a new space on the board.
|
|
||||||
- With the cross-hair placed, click the "FIRE" button to fire on your opponent
|
|
||||||
- You'll either see a gratifying explosion, or a sad, sad sploosh.
|
|
||||||
|
|
||||||
4. When you win, or lose, you'll be greeted with your opponent's ships, so you can see just how close to annihilating them you got!
|
|
12
README.md
12
README.md
@ -1,13 +1 @@
|
|||||||
# Group12
|
# Group12
|
||||||
|
|
||||||
## Boat Battle
|
|
||||||
### The classic pen and paper game brought to life
|
|
||||||
|
|
||||||
Enjoy minutes of fun and engaging multiplayer strategy gameplay in this recreation of the classic pen and paper Boat Battling game.
|
|
||||||
|
|
||||||
Made using Godot Engine v3.3.4
|
|
||||||
|
|
||||||
With music by Kevin MacLeod:
|
|
||||||
"Captain Scurvy" Kevin MacLeod (incompetech.com)
|
|
||||||
Licensed under Creative Commons: By Attribution 4.0 License
|
|
||||||
http://creativecommons.org/licenses/by/4.0/
|
|
||||||
|
@ -39,28 +39,3 @@ application/product_name="Ship Battle"
|
|||||||
application/file_description="A game made by four very tired students."
|
application/file_description="A game made by four very tired students."
|
||||||
application/copyright="2021 Group 12 Industries"
|
application/copyright="2021 Group 12 Industries"
|
||||||
application/trademarks=""
|
application/trademarks=""
|
||||||
|
|
||||||
[preset.1]
|
|
||||||
|
|
||||||
name="Linux/X11"
|
|
||||||
platform="Linux/X11"
|
|
||||||
runnable=true
|
|
||||||
custom_features=""
|
|
||||||
export_filter="all_resources"
|
|
||||||
include_filter=""
|
|
||||||
exclude_filter=""
|
|
||||||
export_path="../../Boat Battle.x86_64"
|
|
||||||
script_export_mode=1
|
|
||||||
script_encryption_key=""
|
|
||||||
|
|
||||||
[preset.1.options]
|
|
||||||
|
|
||||||
custom_template/debug=""
|
|
||||||
custom_template/release=""
|
|
||||||
binary_format/64_bits=true
|
|
||||||
binary_format/embed_pck=true
|
|
||||||
texture_format/bptc=true
|
|
||||||
texture_format/s3tc=true
|
|
||||||
texture_format/etc=false
|
|
||||||
texture_format/etc2=false
|
|
||||||
texture_format/no_bptc_fallbacks=true
|
|
||||||
|
@ -4,95 +4,50 @@
|
|||||||
[ext_resource path="res://assets/font/Minecraft.ttf" type="DynamicFontData" id=2]
|
[ext_resource path="res://assets/font/Minecraft.ttf" type="DynamicFontData" id=2]
|
||||||
|
|
||||||
[sub_resource type="DynamicFont" id=1]
|
[sub_resource type="DynamicFont" id=1]
|
||||||
size = 48
|
size = 40
|
||||||
outline_size = 2
|
|
||||||
outline_color = Color( 0, 0, 0, 1 )
|
|
||||||
use_mipmaps = true
|
|
||||||
extra_spacing_char = 2
|
|
||||||
font_data = ExtResource( 2 )
|
font_data = ExtResource( 2 )
|
||||||
|
|
||||||
[node name="Victory" type="Control"]
|
[node name="Victory" type="Control"]
|
||||||
anchor_right = 1.0
|
anchor_right = 1.0
|
||||||
anchor_bottom = 1.0
|
anchor_bottom = 1.0
|
||||||
margin_top = -0.471924
|
|
||||||
margin_bottom = -0.471924
|
|
||||||
mouse_filter = 2
|
mouse_filter = 2
|
||||||
script = ExtResource( 1 )
|
script = ExtResource( 1 )
|
||||||
__meta__ = {
|
__meta__ = {
|
||||||
"_edit_use_anchors_": false
|
"_edit_use_anchors_": false
|
||||||
}
|
}
|
||||||
|
|
||||||
[node name="Board Placeholder" type="Control" parent="."]
|
[node name="exit_to_main" type="Button" parent="."]
|
||||||
margin_left = 18.0
|
margin_left = 541.0
|
||||||
margin_top = 18.0
|
margin_top = 327.85
|
||||||
margin_right = 342.0
|
margin_right = 636.0
|
||||||
margin_bottom = 342.0
|
margin_bottom = 353.85
|
||||||
|
text = "Exit to Main"
|
||||||
__meta__ = {
|
__meta__ = {
|
||||||
"_edit_use_anchors_": false
|
"_edit_use_anchors_": false
|
||||||
}
|
}
|
||||||
|
|
||||||
[node name="Center Buttons" type="CenterContainer" parent="."]
|
[node name="Button2" type="Button" parent="."]
|
||||||
margin_left = 342.0
|
visible = false
|
||||||
margin_top = 180.0
|
margin_left = 2.22023
|
||||||
margin_right = 622.0
|
margin_top = 337.41
|
||||||
margin_bottom = 342.0
|
margin_right = 63.2202
|
||||||
grow_horizontal = 2
|
margin_bottom = 357.41
|
||||||
grow_vertical = 2
|
text = "Restart"
|
||||||
__meta__ = {
|
|
||||||
"_edit_use_anchors_": false
|
|
||||||
}
|
|
||||||
|
|
||||||
[node name="Buttons" type="VBoxContainer" parent="Center Buttons"]
|
[node name="Victory" type="Label" parent="."]
|
||||||
margin_left = 60.0
|
margin_left = 380.0
|
||||||
margin_top = 57.0
|
margin_top = 44.5109
|
||||||
margin_right = 220.0
|
margin_right = 260.32
|
||||||
margin_bottom = 105.0
|
margin_bottom = 84.5109
|
||||||
rect_min_size = Vector2( 160, 0 )
|
size_flags_vertical = 0
|
||||||
custom_constants/separation = 8
|
|
||||||
|
|
||||||
[node name="Restart" type="Button" parent="Center Buttons/Buttons"]
|
|
||||||
margin_right = 160.0
|
|
||||||
margin_bottom = 20.0
|
|
||||||
rect_min_size = Vector2( 160, 0 )
|
|
||||||
text = "Play Again"
|
|
||||||
__meta__ = {
|
|
||||||
"_edit_use_anchors_": false
|
|
||||||
}
|
|
||||||
|
|
||||||
[node name="Exit to Title" type="Button" parent="Center Buttons/Buttons"]
|
|
||||||
margin_top = 28.0
|
|
||||||
margin_right = 160.0
|
|
||||||
margin_bottom = 48.0
|
|
||||||
text = "Return to Title"
|
|
||||||
__meta__ = {
|
|
||||||
"_edit_use_anchors_": false
|
|
||||||
}
|
|
||||||
|
|
||||||
[node name="Center Text" type="CenterContainer" parent="."]
|
|
||||||
margin_left = 342.0
|
|
||||||
margin_top = 18.0
|
|
||||||
margin_right = 622.0
|
|
||||||
margin_bottom = 180.0
|
|
||||||
__meta__ = {
|
|
||||||
"_edit_use_anchors_": false
|
|
||||||
}
|
|
||||||
|
|
||||||
[node name="Text" type="Label" parent="Center Text"]
|
|
||||||
margin_left = 50.0
|
|
||||||
margin_top = 57.0
|
|
||||||
margin_right = 230.0
|
|
||||||
margin_bottom = 105.0
|
|
||||||
size_flags_stretch_ratio = 0.0
|
|
||||||
custom_fonts/font = SubResource( 1 )
|
custom_fonts/font = SubResource( 1 )
|
||||||
custom_colors/font_color = Color( 1, 1, 1, 1 )
|
|
||||||
custom_colors/font_outline_modulate = Color( 0, 0, 0, 1 )
|
|
||||||
custom_colors/font_color_shadow = Color( 0, 0, 0, 1 )
|
|
||||||
custom_constants/shadow_offset_x = 0
|
|
||||||
custom_constants/shadow_offset_y = 6
|
|
||||||
text = "Victory"
|
text = "Victory"
|
||||||
|
align = 1
|
||||||
|
valign = 1
|
||||||
__meta__ = {
|
__meta__ = {
|
||||||
"_edit_use_anchors_": false
|
"_edit_use_anchors_": false
|
||||||
}
|
}
|
||||||
|
|
||||||
[connection signal="pressed" from="Center Buttons/Buttons/Restart" to="." method="_on_Restart_pressed"]
|
[connection signal="button_down" from="exit_to_main" to="." method="_on_exit_to_main_button_down"]
|
||||||
[connection signal="pressed" from="Center Buttons/Buttons/Exit to Title" to="." method="_on_Exit_to_Title_pressed"]
|
[connection signal="pressed" from="exit_to_main" to="." method="_on_Button_pressed"]
|
||||||
|
[connection signal="button_down" from="Button2" to="." method="_on_restart_button_down"]
|
||||||
|
@ -42,7 +42,7 @@ text = "Single Player"
|
|||||||
[node name="Multiplayer" type="Button" parent="VBoxContainer"]
|
[node name="Multiplayer" type="Button" parent="VBoxContainer"]
|
||||||
margin_right = 160.0
|
margin_right = 160.0
|
||||||
margin_bottom = 24.0
|
margin_bottom = 24.0
|
||||||
text = "Start"
|
text = "Multiplayer"
|
||||||
|
|
||||||
[node name="Options" type="Button" parent="VBoxContainer"]
|
[node name="Options" type="Button" parent="VBoxContainer"]
|
||||||
margin_top = 28.0
|
margin_top = 28.0
|
||||||
|
@ -357,27 +357,14 @@ func listify_string(string):
|
|||||||
res = string.split(' ', true, 0)
|
res = string.split(' ', true, 0)
|
||||||
return res
|
return res
|
||||||
|
|
||||||
# file_exists: checks if a file exists at path
|
|
||||||
# params: path: string denoting the path to a file
|
|
||||||
# returns: bool denoting file's presence at path
|
|
||||||
func file_exists(path):
|
|
||||||
var D = Directory.new()
|
|
||||||
return D.file_exists(path)
|
|
||||||
|
|
||||||
|
|
||||||
# Commands. All commands take in a parameter called command,
|
# Commands. All commands take in a parameter called command,
|
||||||
# which contains a partially tokenized command
|
# which contains a partially tokenized command
|
||||||
# start: Loads scene from res://scenes/*.tscn by filename, and starts it
|
# start: Loads scene from res://scenes/*.tscn by filename, and starts it
|
||||||
func command_start (command):
|
func command_start (command):
|
||||||
if command.size() > 1:
|
if command.size() > 1:
|
||||||
var path = "res://scenes/%s.tscn" % command[1]
|
var pack = load("res://scenes/%s.tscn" % command[1])
|
||||||
var pack = load(path) if file_exists(path) else null
|
get_pwn().add_child(pack.instance());
|
||||||
# Check if the resource was opened
|
debug_print_line("started '%s'\n" % command[1])
|
||||||
if pack:
|
|
||||||
get_pwn().add_child(pack.instance());
|
|
||||||
debug_print_line("started '%s'\n" % command[1])
|
|
||||||
else:
|
|
||||||
debug_print_line("Path not found: %s\n" % "res://scenes/%s.tscn" % command[1])
|
|
||||||
else:
|
else:
|
||||||
debug_print_line(get_usage(command[0]))
|
debug_print_line(get_usage(command[0]))
|
||||||
|
|
||||||
@ -387,7 +374,7 @@ func command_kill (command):
|
|||||||
var node = get_pwn().find_node(command[1], false, false)
|
var node = get_pwn().find_node(command[1], false, false)
|
||||||
if node:
|
if node:
|
||||||
if String(node.get_path()).match("*Debug*"):
|
if String(node.get_path()).match("*Debug*"):
|
||||||
debug_print_line("I'm sorry, Dave. I'm afraid I can't do that.\n")
|
debug_print_line("YOU DIDN'T SAY THE MAGIC WORD!\n")
|
||||||
else:
|
else:
|
||||||
node.queue_free()
|
node.queue_free()
|
||||||
debug_print_line("%s killed\n" % command[1])
|
debug_print_line("%s killed\n" % command[1])
|
||||||
|
@ -25,6 +25,7 @@ var network_id
|
|||||||
|
|
||||||
# Called when the node enters the scene tree for the first time.
|
# Called when the node enters the scene tree for the first time.
|
||||||
func _ready():
|
func _ready():
|
||||||
|
|
||||||
get_node("Forfeit Confirmation").get_ok().text = "Yes"
|
get_node("Forfeit Confirmation").get_ok().text = "Yes"
|
||||||
get_node("Forfeit Confirmation").get_cancel().text = "No"
|
get_node("Forfeit Confirmation").get_cancel().text = "No"
|
||||||
get_node("Forfeit Confirmation").get_ok().rect_min_size.x = 100
|
get_node("Forfeit Confirmation").get_ok().rect_min_size.x = 100
|
||||||
@ -77,6 +78,7 @@ remote func state_check(pos):
|
|||||||
LOST:
|
LOST:
|
||||||
# the other player wins
|
# the other player wins
|
||||||
rpc("state_win", player.board.ship_data)
|
rpc("state_win", player.board.ship_data)
|
||||||
|
victory_screen(null, false)
|
||||||
SUNK, HIT:
|
SUNK, HIT:
|
||||||
# Hit
|
# Hit
|
||||||
rpc("state_fire")
|
rpc("state_fire")
|
||||||
@ -85,19 +87,11 @@ remote func state_check(pos):
|
|||||||
state_fire()
|
state_fire()
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# state_win: The winning state. If you reach here, you've won.
|
# state_win: The winning state. If you reach here, someone's won.
|
||||||
# ships: The opponent's ship data, so that their board can be shown
|
# ships: The opponent's ship data, so that their board can be shown
|
||||||
remote func state_win(ships):
|
remote func state_win(ships):
|
||||||
# Send ships back to the opponent:
|
victory_screen(ships)
|
||||||
rpc("state_lose", player.board.ship_data)
|
pass
|
||||||
# Show the victory screen
|
|
||||||
victory_screen(ships, true)
|
|
||||||
|
|
||||||
# state_lose: The losing state. If you reach here, you've lost.
|
|
||||||
# ships: The opponent's ship data, so that their board can be shown
|
|
||||||
remote func state_lose(ships):
|
|
||||||
# Show the not-victory screen
|
|
||||||
victory_screen(ships, false)
|
|
||||||
|
|
||||||
# play_hit_sound: Play a hit sound depending on the severity of the hit
|
# play_hit_sound: Play a hit sound depending on the severity of the hit
|
||||||
# value: Lost/Sunk/Hit/Miss
|
# value: Lost/Sunk/Hit/Miss
|
||||||
@ -155,22 +149,17 @@ func _on_player_ready():
|
|||||||
|
|
||||||
# victory_screen: display the victory screen
|
# victory_screen: display the victory screen
|
||||||
func victory_screen(ships, winner = true):
|
func victory_screen(ships, winner = true):
|
||||||
# Stop listening for opponent disconnections
|
if winner:
|
||||||
Net.disconnect("disconnected", self, "connection_error")
|
# Hide the buttons
|
||||||
# Hide the buttons
|
get_node("Buttons").hide()
|
||||||
get_node("Buttons").hide()
|
# Create a new Victory screen
|
||||||
# Create a new Victory screen
|
var victory = Victory.instance()
|
||||||
var victory = Victory.instance()
|
# Give it the ships received from the opponent
|
||||||
# Allow victory to end the game
|
|
||||||
victory.connect("end_game", self, "end")
|
|
||||||
# Tell victory whether we've won or lost
|
|
||||||
victory.set_win(winner)
|
|
||||||
# If we were given ships to display
|
|
||||||
if ships:
|
|
||||||
# Give victory the ships
|
|
||||||
victory.reveal_ships(ships)
|
victory.reveal_ships(ships)
|
||||||
# Add victory to the scene tree
|
# Add victory to the scene tree
|
||||||
add_child(victory)
|
add_child(victory)
|
||||||
|
else:
|
||||||
|
end()
|
||||||
|
|
||||||
# _on_Forfeit_pressed: Handle forfeit button press
|
# _on_Forfeit_pressed: Handle forfeit button press
|
||||||
func _on_Forfeit_pressed():
|
func _on_Forfeit_pressed():
|
||||||
@ -179,8 +168,6 @@ func _on_Forfeit_pressed():
|
|||||||
|
|
||||||
# end: end the Game
|
# end: end the Game
|
||||||
sync func end():
|
sync func end():
|
||||||
# Return to the lobby
|
|
||||||
MessageBus.emit_signal("change_scene", "Multiplayer")
|
|
||||||
queue_free()
|
queue_free()
|
||||||
|
|
||||||
|
|
||||||
@ -199,10 +186,9 @@ func _on_Forfeit_Confirmation_confirmed():
|
|||||||
if Net.connected:
|
if Net.connected:
|
||||||
# Send forfeit request to all users
|
# Send forfeit request to all users
|
||||||
rpc("end")
|
rpc("end")
|
||||||
else:
|
end()
|
||||||
end()
|
|
||||||
|
|
||||||
func _on_Connection_Error_confirmed():
|
func _on_Connection_Error_confirmed():
|
||||||
# End the game
|
# End the game
|
||||||
end()
|
queue_free()
|
||||||
|
|
||||||
|
@ -3,9 +3,6 @@ extends Control
|
|||||||
# Path to Board class, for instantiating new Boards in code
|
# Path to Board class, for instantiating new Boards in code
|
||||||
var Board = preload("res://scenes/Game/Board.tscn")
|
var Board = preload("res://scenes/Game/Board.tscn")
|
||||||
|
|
||||||
# Sidnals
|
|
||||||
# request to return to lobby
|
|
||||||
signal end_game
|
|
||||||
# Called when the node enters the scene tree for the first time.
|
# Called when the node enters the scene tree for the first time.
|
||||||
func _ready():
|
func _ready():
|
||||||
pass # Replace with function body.
|
pass # Replace with function body.
|
||||||
@ -17,26 +14,17 @@ func reveal_ships(ships:Array):
|
|||||||
for ship in ships:
|
for ship in ships:
|
||||||
board.callv("place_ship", ship)
|
board.callv("place_ship", ship)
|
||||||
|
|
||||||
func set_win(won:bool):
|
|
||||||
var Text = find_node("Text")
|
|
||||||
if won:
|
|
||||||
Text.text = "You win!"
|
|
||||||
else:
|
|
||||||
Text.text = "You lose"
|
|
||||||
|
|
||||||
# Called every frame. 'delta' is the elapsed time since the previous frame.
|
# Called every frame. 'delta' is the elapsed time since the previous frame.
|
||||||
#func _process(delta):
|
#func _process(delta):
|
||||||
# pass
|
# pass
|
||||||
|
|
||||||
func _on_Restart_pressed():
|
func _on_restart_button_down():
|
||||||
AudioBus.emit_signal("button_clicked")
|
AudioBus.emit_signal("button_clicked")
|
||||||
emit_signal("end_game")
|
MessageBus.emit_signal("change_scene", "Multiplayer")
|
||||||
|
MessageBus.emit_signal("kill_scene", "Game")
|
||||||
|
|
||||||
|
|
||||||
# returns player(s) back to main menu
|
# returns player(s) back to main menu
|
||||||
func _on_Exit_to_Title_pressed():
|
func _on_exit_to_main_button_down():
|
||||||
AudioBus.emit_signal("button_clicked")
|
AudioBus.emit_signal("button_clicked")
|
||||||
# Disconnect from peer
|
|
||||||
Net.disconnect_host()
|
|
||||||
# Force return to title
|
|
||||||
MessageBus.emit_signal("return_to_title")
|
MessageBus.emit_signal("return_to_title")
|
||||||
|
@ -27,19 +27,9 @@ func set_IP_Address_text(show):
|
|||||||
func _ready():
|
func _ready():
|
||||||
Net.connect("peers_updated", self, "_on_peers_updated")
|
Net.connect("peers_updated", self, "_on_peers_updated")
|
||||||
Net.connect("disconnected", self, "_on_Net_disconnected")
|
Net.connect("disconnected", self, "_on_Net_disconnected")
|
||||||
# Let the player name default to hostname
|
|
||||||
name_popup.get_node("Name Entry").text = Net.get_hostname()
|
name_popup.get_node("Name Entry").text = Net.get_hostname()
|
||||||
# Update the peers list
|
|
||||||
_on_peers_updated()
|
_on_peers_updated()
|
||||||
# Set the keyboard-control focus to the first valid focus
|
pass
|
||||||
find_next_valid_focus().grab_focus()
|
|
||||||
# Resume a connection, if coming to this screen from a connected state (i.e. "restart gane"
|
|
||||||
if Net.hosting:
|
|
||||||
# Show the host IP address
|
|
||||||
set_IP_Address_text(true)
|
|
||||||
if Net.connected:
|
|
||||||
# Show "Connected Options"
|
|
||||||
show_Connected_Options()
|
|
||||||
|
|
||||||
func show_Connected_Options():
|
func show_Connected_Options():
|
||||||
# [Hide]/Show the host options
|
# [Hide]/Show the host options
|
||||||
|
@ -62,8 +62,8 @@ func send(id, mail, mail_type = REPLY):
|
|||||||
# Host
|
# Host
|
||||||
# start_host: Host the game
|
# start_host: Host the game
|
||||||
# port: TCP port
|
# port: TCP port
|
||||||
# max_players: Largest number of players allowed to connect at a time (the host does not count)
|
# max_players: Largest number of players allowed to connect at a time
|
||||||
func start_host(port = DEFAULT_PORT, max_players = 1):
|
func start_host(port = DEFAULT_PORT, max_players = 2):
|
||||||
get_hostname()
|
get_hostname()
|
||||||
peer_info[1] = local_info
|
peer_info[1] = local_info
|
||||||
# Notify that peer list has updated
|
# Notify that peer list has updated
|
||||||
@ -150,10 +150,16 @@ func _ready():
|
|||||||
func _peer_connected(id):
|
func _peer_connected(id):
|
||||||
# Send peer info to remote peer
|
# Send peer info to remote peer
|
||||||
rpc_id(id, "register_peer", local_info)
|
rpc_id(id, "register_peer", local_info)
|
||||||
|
if hosting and peer_info.size() >= 2:
|
||||||
|
pass
|
||||||
|
pass
|
||||||
|
|
||||||
func _peer_disconnected(id):
|
func _peer_disconnected(id):
|
||||||
# Unregister the peer locally
|
# Unregister the peer locally
|
||||||
unregister_peer(id)
|
unregister_peer(id)
|
||||||
|
if hosting and peer_info.size() < 2:
|
||||||
|
pass
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
func _host_connected():
|
func _host_connected():
|
||||||
|
Loading…
x
Reference in New Issue
Block a user